summaryrefslogtreecommitdiff
path: root/andbackup.sh
blob: e7f2c1d9c36b93b679348eba778be9a9210174d7 (plain)
    1 #!/system/bin/sh
    2 #
    3 # Andbackup performs Android backups and restores.
    4 #
    5 # Copyright (C) 2017  Aaron Ball <nullspoon@oper.io>
    6 #
    7 # This program is free software: you can redistribute it and/or modify
    8 # it under the terms of the GNU General Public License as published by
    9 # the Free Software Foundation, either version 3 of the License, or
   10 # (at your option) any later version.
   11 #
   12 # This program is distributed in the hope that it will be useful,
   13 # but WITHOUT ANY WARRANTY; without even the implied warranty of
   14 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
   15 # GNU General Public License for more details.
   16 #
   17 # You should have received a copy of the GNU General Public License
   18 # along with this program.  If not, see <http://www.gnu.org/licenses/>.
   19 #
   20 set -u
   21 
   22 backups=/sdcard/andbackup
   23 #backups=/storage/ext_sd/bk/andbackup/andbackupBK
   24 flag_delimiter="-"
   25 
   26 export ANDROID_DATA='/data/data'
   27 export ANDROID_CODE='/data/app'
   28 export ANDROID_SYSTEM='/system'
   29 
   30 usage() {
   31   # NOTE: The maximum width of this text should be 65 chars. This
   32   #       will ensure it displays properly on most devices without
   33   #       strange wrapping.
   34   #       If using vim to reflow, execute ':set tw=65'
   35   out="
   36 Andbackup is a basic Android application (or package) backup and
   37 restore script. It can backup a single application, or a a list
   38 of applications provided in a text file (one application name per
   39 line). It can do the same for application restore.
   40 
   41 The default backup path is '/sdcard/andbackup/'.
   42 
   43 Usage:
   44   andbackup.sh <command> [opts]
   45 
   46 Commands:
   47   help                    Print this help text
   48   backup      <app_name>  Back up a single package
   49   restore     <app_name>  Restore a single package
   50   list                    List installed packages
   51   listbackup  <list_file> Backup packages using a list text file
   52   listrestore <list_file> Restore packages from a list text file
   53 
   54 "
   55   printf "${out}"
   56 }
   57 
   58 
   59 log() {
   60   logtype=${1:-}
   61   logmsg=${2:-}
   62   logdate=$(date '+%T %F')
   63 
   64   printf "%s   %s   %s\n" "${logdate}" "${logtype}" "${logmsg}"
   65 }
   66 
   67 # Some useful macros
   68 linfo() {
   69   log "info " "${1:-}"
   70 }
   71 
   72 lerror() {
   73   log "error" "${1:-}"
   74 }
   75 
   76 lwarn() {
   77   log "warn " "${1:-}"
   78 }
   79 
   80 lfatal() {
   81   log "fatal" "${1:-}"
   82 }
   83 
   84 
   85 backup_app() {
   86   local app="${1:-}"
   87   local args="${2:-}"
   88   local preserve_cache=0 # Whether to preserve cache in the backup. Can yield
   89                          # notably larger backups for some applications
   90   local origifs=${IFS}   # Input field seperator at runtime. Useful for
   91                          # reverting
   92 
   93   local tar  # Tar command for packaging the backup
   94 
   95   # Make sure app is specified
   96   if [[ -z ${app} ]]; then
   97     lerror "Application name required."
   98     return 1
   99   fi
  100 
  101   # Handle passed arguments
  102   IFS=' ' 
  103   for arg in ${args}; do
  104     # Don't clean up the cache if preserveCache is specified
  105     if [ "${arg}" = 'preserveCache' ]; then
  106       preserve_cache=true
  107     else
  108       # Do nothing with unknown arguments
  109       lwarn "${app}: Unknown backup argument '${arg}'"
  110     fi
  111   done
  112   IFS=${origifs}
  113 
  114   # Make sure app is installed
  115   if [ ! -d "${ANDROID_DATA}/${app}" ]; then
  116     lerror "Package ${app} appears to not be installed."
  117     return 2
  118   fi
  119 
  120   linfo "Backing up ${app}"
  121   local apk="$(find ${ANDROID_CODE}/${app}-*/base.apk | head -n1)"
  122   local data="${ANDROID_DATA}/${app}"
  123 
  124   # No need to stop the application because we're in recovery mode
  125 
  126   # Create backup destination if not exist
  127   [ ! -d "${backups}" ] && mkdir -p "${backups}"
  128 
  129   # Backup the apk file if it exists
  130   if [ ! -z "${apk}" ]; then
  131     # Delete the old backup first
  132     # There is a permissions issue querk when the backup was originall taken in
  133     # recovery mode, but later is taken in android (Permission denied for no
  134     # apparent reason)
  135     [ -f "${backups}/${app}/base.apk" ] && rm -f "${backups}/${app}/base.apk"
  136     # Copy the installer to the backup dir
  137     cp -p "${apk}" "${backups}/${app}/base.apk"
  138   else
  139     linfo "${app} apk file could not be found. Skipping apk installer backup."
  140   fi
  141 
  142   # If the data directory is not found or the data variable is empty, skip
  143   # backup. This covers a weird edge case where an application is installed but
  144   # hasn't been launched (I think?). The data variable would be empty, causing
  145   # this script to backup /*.
  146   if [ -z "${data}" ] || [ ! -d "${data}" ]; then
  147     lwarn "No data directory for application '${app}' found. Skipping backup."
  148     return 2
  149   fi
  150 
  151   # Stop the application if it is running
  152   # Don't try to stop it if the twrp binary is found. If we're in recovery
  153   # mode, the application already isn't running.
  154   if [ ! -f /sbin/twrp ]; then
  155     linfo "Stopping application ${app}"
  156     am force-stop "${app}"
  157   else
  158     linfo "Skipping application force stop while booted to recovery mode."
  159   fi
  160 
  161   tar="tar -C ${data} -c "
  162   # Delete cache directory if it exists
  163   # This will sometimes free up significant amounts of space
  164   if [[ ! -d "${data}/cache" ]]; then
  165     linfo "Cache doesn't exist for ${app}"
  166   elif [ "$preserve_cache" -eq 0 ]; then
  167     linfo "Excluding cache for ${app}"
  168     tar="${tar} --exclude=cache"
  169   else
  170     linfo "Preserving cache for ${app}"
  171   fi
  172 
  173   # Compress the backup
  174   linfo "Compressing userdata for ${app}"
  175   ${tar} . | gzip -c > "${backups}/${app}/data.tar.gz"
  176 }
  177 
  178 
  179 list_apps() {
  180   pm list package | cut -d ':' -f 2 | sort
  181 }
  182 
  183 
  184 restore_app() {
  185   local app=${1:-}
  186 
  187   # Make sure app is specified
  188   if [ -z "${app}" ]; then
  189     lerror "Please specify an app to restore."
  190     return 1
  191   fi
  192 
  193   # If twrp binary is found on the filesystem, do not attempt restore as
  194   # restores cannot be done without a running system (the various android
  195   # binaries don't work right).
  196   if [ -f /sbin/twrp ]; then
  197     printf "Cannot perform a %s while in recovery mode.\n" "${app}"
  198     return 128
  199   fi
  200 
  201   # When restoring something with flags need to restore app alone
  202   if [[ 0 = $(echo ${app} | grep -q [${flag_delimiter}]; echo $?) ]]; then
  203     linfo "This app $app had passed in options"
  204     #Need to get app back to std naming (without params)
  205     app=`echo ${app} | grep -o ^.*- | sed s/-//g | cat -`
  206     linfo "Restoring to std name \"${app}\" while we restore"
  207   fi
  208 
  209   # Check that backup exists to be restored
  210   if [[ ! -d ${backups}/${app} ]]; then
  211     lerror "No backup for ${app} exists."
  212     return 1
  213   fi
  214 
  215   linfo "Restoring ${app}"
  216 
  217   # Install app if it is not yet installed
  218   if [[ ! -f "${backups}/${app}/base.apk" ]]; then
  219     linfo "Installer for ${app} not found. Only restoring data."
  220   elif [[ $(dumpsys package ${app} | grep -c userId) -eq 0 ]]; then
  221     linfo "Installer detected but package ${app} is not installed. Installing"
  222     pm install ${backups}/${app}/base.apk
  223   fi
  224 
  225   # Stop the application if it is running
  226   am force-stop ${app}
  227 
  228   # Get pertinent metadata
  229   local owner=$(dumpsys package ${app} | grep userId | cut -d'=' -f2 | head -n1)
  230   local code=$(dumpsys package ${app} | grep codePath | cut -d'=' -f2 | head -n1)
  231   local data=$(dumpsys package ${app} | grep dataDir  | cut -d'=' -f2 | head -n1)
  232 
  233   # Decompress backup
  234   linfo "Decompressing user data for ${app}."
  235   cd ${backups}/${app}/
  236   gzip -d -c data.tar.gz | tar -x
  237 
  238   # Copy the user data in
  239   cp -rp ${backups}/${app}/data/* ${data}/
  240   # Fix data permissions
  241   chown -R ${owner}:${owner} ${data}
  242   # Fix selinux labels
  243   linfo "Restoring SELinux contexs for ${app}"
  244   restorecon -R ${data} 2>/dev/null
  245 
  246   # Cleanup the extracted data so restores don't take too much space
  247   rm -rf ${backups}/${app}/data
  248 }
  249 
  250 
  251 backup_apps() {
  252   local apps=${@}
  253   [ -z "${apps}" ] && lerror "At least one app is required." && return 1
  254 
  255   IFS=' '
  256   for app in ${apps}; do
  257     backup_app "${app}"
  258   done
  259 }
  260 
  261 
  262 restore_apps() {
  263   local apps=${@}
  264   [ -z "${apps}" ] && log error "At least one app is required.\n" && return 1
  265 
  266   IFS=' '
  267   for app in ${apps}; do
  268     restore_app "${app}"
  269   done
  270 }
  271 
  272 
  273 list_backup_apps() {
  274   local list=${1:-}
  275   [ -z "${list}" ] && printf "A backup list is required." && return 1
  276 
  277   local app  # Application mame
  278   local args # Application arguments
  279 
  280   export IFS=$'\n'
  281   for line in $(cat ${list}); do
  282     # Parse out the app name
  283     app="$(echo ${line} | tr -s ' ' | cut -d ' ' -f 1)"
  284     # Parse out the arguments (if any)
  285     args="$(echo ${line} | tr -s ' ' | cut -s -d ' ' -f 2-)"
  286     # Exceute the backup
  287     backup_app "${app}" "${args}"
  288   done
  289 }
  290 
  291 
  292 list_restore_apps() {
  293   local list=${1}
  294   [[ -z ${list} ]] && printf "A backup list is required.\n" && return 1
  295 
  296   for app in $(cat ${list}); do
  297     restore_app ${app}
  298   done
  299 }
  300 
  301 
  302 verify_system() {
  303   if [ ! -d "${ANDROID_DATA}" ]; then
  304     printf "Data directory (%s) not found.\n" ${ANDROID_DATA} >&2
  305     printf "Is it mounted?\n" >&2
  306     return 1
  307   fi
  308   if [ ! -d "${ANDROID_CODE}" ]; then
  309     printf "Application code directory (%s) not found.\n" ${ANDROID_CODE} >&2
  310     printf "Is it mounted?\n" >&2
  311     return 1
  312   fi
  313   if [ ! -d "${ANDROID_SYSTEM}/etc" ]; then
  314     printf "System partition (%s) is not mounted.\n" ${ANDROID_SYSTEM} >&2
  315     return 1
  316   fi
  317   return 0
  318 }
  319 
  320 
  321 main() {
  322   local cmd=${1:-}
  323   shift
  324 
  325   # Ensure root is running this script
  326   if [ $(id -u) -gt 0 ]; then
  327     lerror "Script must be run as root (uid 0)."
  328     return 1
  329   fi
  330 
  331   verify_system || return 1
  332 
  333   if [ "${cmd}" = 'backup' ]; then
  334     backup_apps ${@}
  335   elif [ "${cmd}" = 'listbackup' ]; then
  336     list_backup_apps ${@}
  337   elif [ "${cmd}" = 'restore' ]; then
  338     restore_apps ${@}
  339   elif [ "${cmd}" = 'listrestore' ]; then
  340     list_restore_apps ${@}
  341   elif [ "${cmd}" = 'ls' ] || [ "${cmd}" = 'list' ]; then
  342     list_apps
  343   elif [ "${cmd}" = 'help' ]; then
  344     usage
  345   else
  346     lerror "Unknown command '${cmd}'"
  347     usage
  348     return 1
  349   fi
  350 }
  351 
  352 main ${@}

Generated by cgit