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

Generated by cgit