summaryrefslogtreecommitdiff
path: root/andbackup.sh
blob: c48a86fbc5be2add9335a9e8382862edb2d4a79a (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}/${app}" ] && mkdir -p "${backups}/${app}"
  128 
  129   # Backup the apk file if it exists
  130   if [ ! -z "${apk}" ] && [ -f "${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   taropts=" -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     taropts="${taropts} --exclude=cache"
  169   else
  170     linfo "Preserving cache for ${app}"
  171   fi
  172 
  173   # Compress the backup
  174   linfo "Compressing userdata for ${app}"
  175   eval tar ${taropts} . | 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 data="/data/data/${app}"
  231 
  232   # Decompress data backup
  233   linfo "Decompressing user data for ${app}."
  234   gzip -d -c ${backups}/${app}/data.tar.gz | tar -x -C "${data}"
  235 
  236   # Fix data permissions
  237   chown -R ${owner}:${owner} "${data}"
  238   # Fix selinux labels
  239   linfo "Restoring SELinux contexs for ${app}"
  240   restorecon -R "${data}" 2>/dev/null
  241 }
  242 
  243 
  244 backup_apps() {
  245   local apps=${@}
  246   [ -z "${apps}" ] && lerror "At least one app is required." && return 1
  247 
  248   IFS=' '
  249   for app in ${apps}; do
  250     backup_app "${app}"
  251   done
  252 }
  253 
  254 
  255 restore_apps() {
  256   local apps=${@}
  257   [ -z "${apps}" ] && log error "At least one app is required.\n" && return 1
  258 
  259   IFS=' '
  260   for app in ${apps}; do
  261     restore_app "${app}"
  262   done
  263 }
  264 
  265 
  266 list_backup_apps() {
  267   local list=${1:-}
  268   [ -z "${list}" ] && printf "A backup list is required." && return 1
  269 
  270   local app  # Application mame
  271   local args # Application arguments
  272 
  273   export IFS=$'\n'
  274   for line in $(cat ${list}); do
  275     # Parse out the app name
  276     app="$(echo ${line} | tr -s ' ' | cut -d ' ' -f 1)"
  277     # Parse out the arguments (if any)
  278     args="$(echo ${line} | tr -s ' ' | cut -s -d ' ' -f 2-)"
  279     # Exceute the backup
  280     backup_app "${app}" "${args}"
  281   done
  282 }
  283 
  284 
  285 list_restore_apps() {
  286   local list=${1}
  287   [[ -z ${list} ]] && printf "A backup list is required.\n" && return 1
  288 
  289   for app in $(cat ${list}); do
  290     restore_app ${app}
  291   done
  292 }
  293 
  294 
  295 verify_system() {
  296   if [ ! -d "${ANDROID_DATA}" ]; then
  297     printf "Data directory (%s) not found.\n" ${ANDROID_DATA} >&2
  298     printf "Is it mounted?\n" >&2
  299     return 1
  300   fi
  301   if [ ! -d "${ANDROID_CODE}" ]; then
  302     printf "Application code directory (%s) not found.\n" ${ANDROID_CODE} >&2
  303     printf "Is it mounted?\n" >&2
  304     return 1
  305   fi
  306   if [ ! -d "${ANDROID_SYSTEM}/etc" ]; then
  307     printf "System partition (%s) is not mounted.\n" ${ANDROID_SYSTEM} >&2
  308     return 1
  309   fi
  310   return 0
  311 }
  312 
  313 
  314 main() {
  315   local cmd=${1:-}
  316   shift
  317 
  318   if [ "${cmd}" = 'help' ]; then
  319     usage
  320     return 0
  321   fi
  322 
  323   # Ensure root is running this script
  324   if [ $(id -u) -gt 0 ]; then
  325     lerror "Script must be run as root (uid 0)."
  326     return 1
  327   fi
  328 
  329   verify_system || return 1
  330 
  331   if [ "${cmd}" = 'backup' ]; then
  332     backup_apps ${@}
  333   elif [ "${cmd}" = 'listbackup' ]; then
  334     list_backup_apps ${@}
  335   elif [ "${cmd}" = 'restore' ]; then
  336     restore_apps ${@}
  337   elif [ "${cmd}" = 'listrestore' ]; then
  338     list_restore_apps ${@}
  339   elif [ "${cmd}" = 'ls' ] || [ "${cmd}" = 'list' ]; then
  340     list_apps
  341   else
  342     lerror "Unknown command '${cmd}'"
  343     usage
  344     return 1
  345   fi
  346 }
  347 
  348 main ${@}

Generated by cgit