summaryrefslogtreecommitdiff
path: root/andbackup.sh
blob: 6232fe290b27835e1755d9e813804573395e205d (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 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   # Make sure app is specified
   94   if [[ -z ${app} ]]; then
   95     lerror "Application name required."
   96     return 1
   97   fi
   98 
   99   # Handle passed arguments
  100   IFS=' ' 
  101   for arg in ${args}; do
  102     # Don't clean up the cache if preserveCache is specified
  103     if [ "${arg}" = 'preserveCache' ]; then
  104       preserve_cache=true
  105     else
  106       # Do nothing with unknown arguments
  107       lwarn "${app}: Unknown backup argument '${arg}'"
  108     fi
  109   done
  110   IFS=${origifs}
  111 
  112   # Make sure app is installed
  113   if [ ! -d "${ANDROID_DATA}/${app}" ]; then
  114     lerror "Package ${app} appears to not be installed."
  115     return 2
  116   fi
  117 
  118   linfo "Backing up ${app}"
  119   local apk="$(find ${ANDROID_CODE}/${app}-*/base.apk | head -n1)"
  120   local data="${ANDROID_DATA}/${app}"
  121 
  122   # No need to stop the application because we're in recovery mode
  123 
  124   # Create backup destination if not exist
  125   [ ! -d "${backups}/${app}" ] && mkdir -p "${backups}/${app}"
  126 
  127   # Backup the apk file if it exists
  128   if [ ! -z "${apk}" ] && [ -f "${apk}" ]; then
  129     # Delete the old backup first
  130     # There is a permissions issue querk when the backup was originally taken in
  131     # recovery mode, but later is taken in android (Permission denied for no
  132     # apparent reason)
  133     [ -f "${backups}/${app}/base.apk" ] && rm -f "${backups}/${app}/base.apk"
  134     # Copy the installer to the backup dir
  135     cp -p "${apk}" "${backups}/${app}/base.apk"
  136   else
  137     linfo "${app} apk file could not be found. Skipping apk installer backup."
  138   fi
  139 
  140   # If the data directory is not found or the data variable is empty, skip
  141   # backup. This covers a weird edge case where an application is installed but
  142   # hasn't been launched (I think?). The data variable would be empty, causing
  143   # this script to backup /*.
  144   if [ -z "${data}" ] || [ ! -d "${data}" ]; then
  145     lwarn "No data directory for application '${app}' found. Skipping backup."
  146     return 2
  147   fi
  148 
  149   # Stop the application if it is running
  150   # Don't try to stop it if the twrp binary is found. If we're in recovery
  151   # mode, the application already isn't running.
  152   if [ ! -f /sbin/twrp ]; then
  153     linfo "Stopping application ${app}"
  154     am force-stop "${app}"
  155   else
  156     linfo "Skipping application force stop while booted to recovery mode."
  157   fi
  158 
  159   taropts=" -C ${data} -c "
  160   # Decide if excluding cache or preserving
  161   if [[ ! -d "${data}/cache" ]]; then
  162     linfo "Cache doesn't exist for ${app}"
  163   elif [ "$preserve_cache" -eq 0 ]; then
  164     linfo "Excluding cache for ${app}"
  165     taropts="${taropts} --exclude=cache"
  166   else
  167     linfo "Preserving cache for ${app}"
  168   fi
  169 
  170   # Compress the backup
  171   linfo "Compressing userdata for ${app}"
  172   eval tar ${taropts} . | gzip -c > "${backups}/${app}/data.tar.gz"
  173 }
  174 
  175 
  176 list_apps() {
  177   for i in $(ls /data/app); do
  178     printf "%s\n" "${i%-*}"
  179   done
  180 }
  181 
  182 
  183 restore_app() {
  184   local app=${1:-}
  185 
  186   # Make sure app is specified
  187   if [ -z "${app}" ]; then
  188     lerror "Please specify an app to restore."
  189     return 1
  190   fi
  191 
  192   # If twrp binary is found on the filesystem, do not attempt restore as
  193   # restores cannot be done without a running system (the various android
  194   # binaries don't work right).
  195   if [ -f /sbin/twrp ]; then
  196     printf "Cannot perform a %s while in recovery mode.\n" "${app}"
  197     return 128
  198   fi
  199 
  200   # When restoring something with flags need to restore app alone
  201   if [[ 0 = $(echo ${app} | grep -q [${flag_delimiter}]; echo $?) ]]; then
  202     linfo "This app $app had passed in options"
  203     #Need to get app back to std naming (without params)
  204     app=`echo ${app} | grep -o ^.*- | sed s/-//g | cat -`
  205     linfo "Restoring to std name \"${app}\" while we restore"
  206   fi
  207 
  208   # Check that backup exists to be restored
  209   if [[ ! -d ${backups}/${app} ]]; then
  210     lerror "No backup for ${app} exists."
  211     return 1
  212   fi
  213 
  214   linfo "Restoring ${app}"
  215 
  216   # Install app if it is not yet installed
  217   if [[ ! -f "${backups}/${app}/base.apk" ]]; then
  218     linfo "Installer for ${app} not found. Only restoring data."
  219   elif [[ $(dumpsys package ${app} | grep -c userId) -eq 0 ]]; then
  220     linfo "Installer detected but package ${app} is not installed. Installing"
  221     pm install ${backups}/${app}/base.apk
  222   fi
  223 
  224   # Stop the application if it is running
  225   am force-stop ${app}
  226 
  227   # Get pertinent metadata
  228   local owner=$(dumpsys package ${app} | grep userId | cut -d'=' -f2 | head -n1)
  229   local data="/data/data/${app}"
  230 
  231   # Decompress data backup
  232   linfo "Decompressing user data for ${app}."
  233   gzip -d -c ${backups}/${app}/data.tar.gz | tar -x -C "${data}"
  234 
  235   # Fix data permissions
  236   chown -R ${owner}:${owner} "${data}"
  237   # Fix selinux labels
  238   linfo "Restoring SELinux contexs for ${app}"
  239   restorecon -R "${data}" 2>/dev/null
  240 }
  241 
  242 
  243 backup_apps() {
  244   local apps=${@}
  245   [ -z "${apps}" ] && lerror "At least one app is required." && return 1
  246 
  247   IFS=' '
  248   for app in ${apps}; do
  249     backup_app "${app}"
  250   done
  251 }
  252 
  253 
  254 restore_apps() {
  255   local apps=${@}
  256   [ -z "${apps}" ] && log error "At least one app is required.\n" && return 1
  257 
  258   IFS=' '
  259   for app in ${apps}; do
  260     restore_app "${app}"
  261   done
  262 }
  263 
  264 
  265 list_backup_apps() {
  266   local list=${1:-}
  267   [ -z "${list}" ] && printf "A backup list is required." && return 1
  268 
  269   local app  # Application mame
  270   local args # Application arguments
  271 
  272   export IFS=$'\n'
  273   for line in $(cat ${list}); do
  274     # Parse out the app name
  275     app="$(echo ${line} | tr -s ' ' | cut -d ' ' -f 1)"
  276     # Parse out the arguments (if any)
  277     args="$(echo ${line} | tr -s ' ' | cut -s -d ' ' -f 2-)"
  278     # Execute the backup
  279     backup_app "${app}" "${args}"
  280   done
  281 }
  282 
  283 
  284 list_restore_apps() {
  285   local list=${1}
  286   [[ -z ${list} ]] && printf "A backup list is required.\n" && return 1
  287 
  288   for app in $(cat ${list}); do
  289     restore_app ${app}
  290   done
  291 }
  292 
  293 
  294 verify_system() {
  295   if [ ! -d "${ANDROID_DATA}" ]; then
  296     printf "Data directory (%s) not found.\n" ${ANDROID_DATA} >&2
  297     printf "Is it mounted?\n" >&2
  298     return 1
  299   fi
  300   if [ ! -d "${ANDROID_CODE}" ]; then
  301     printf "Application code directory (%s) not found.\n" ${ANDROID_CODE} >&2
  302     printf "Is it mounted?\n" >&2
  303     return 1
  304   fi
  305   if [ ! -d "${ANDROID_SYSTEM}/etc" ]; then
  306     printf "System partition (%s) is not mounted.\n" ${ANDROID_SYSTEM} >&2
  307     return 1
  308   fi
  309   return 0
  310 }
  311 
  312 
  313 main() {
  314   local cmd="${1:-}"
  315 
  316   if [ -z "${cmd:-}" ]; then
  317     printf "Action required (backup, listbackup, restore, listrestore)\n"
  318     return 1
  319   fi
  320 
  321   shift
  322 
  323   if [ "${cmd}" = 'help' ]; then
  324     usage
  325     return 0
  326   fi
  327 
  328   # Ensure root is running this script
  329   if [ $(id -u) -gt 0 ]; then
  330     lerror "Script must be run as root (uid 0)."
  331     return 1
  332   fi
  333 
  334   verify_system || return 1
  335 
  336   if [ "${cmd}" = 'backup' ]; then
  337     backup_apps ${@}
  338   elif [ "${cmd}" = 'listbackup' ]; then
  339     list_backup_apps ${@}
  340   elif [ "${cmd}" = 'restore' ]; then
  341     restore_apps ${@}
  342   elif [ "${cmd}" = 'listrestore' ]; then
  343     list_restore_apps ${@}
  344   elif [ "${cmd}" = 'ls' ] || [ "${cmd}" = 'list' ]; then
  345     list_apps
  346   else
  347     lerror "Unknown command '${cmd}'"
  348     usage
  349     return 1
  350   fi
  351 }
  352 
  353 main ${@}

Generated by cgit