#!/system/bin/sh # # Andbackup performs Android backups and restores. # # Copyright (C) 2017 Aaron Ball # # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation, either version 3 of the License, or # (at your option) any later version. # # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # # You should have received a copy of the GNU General Public License # along with this program. If not, see . # set -u backups=/sdcard/andbackup #backups=/storage/ext_sd/bk/andbackup/andbackupBK flag_delimiter="-" export ANDROID_DATA='/data/data' export ANDROID_CODE='/data/app' export ANDROID_SYSTEM='/system' usage() { # NOTE: The maximum width of this text should be 65 chars. This # will ensure it displays properly on most devices without # strange wrapping. # If using vim to reflow, execute ':set tw=65' out=" Andbackup is a basic Android application (or package) backup and restore script. It can backup a single application, or a a list of applications provided in a text file (one application name per line). It can do the same for application restore. The default backup path is '/sdcard/andbackup/'. Usage: andbackup.sh [opts] Commands: help Print this help text backup Back up a single package restore Restore a single package list List installed packages listbackup Backup packages using a list text file listrestore Restore packages from a list text file " printf "${out}" } log() { logtype=${1:-} logmsg=${2:-} logdate=$(date '+%T %F') printf "%s %s %s\n" "${logdate}" "${logtype}" "${logmsg}" } # Some useful macros linfo() { log "info " "${1:-}" } lerror() { log "error" "${1:-}" } lwarn() { log "warn " "${1:-}" } lfatal() { log "fatal" "${1:-}" } backup_app() { local app="${1}" local tar # Tar command for packaging the backup # Make sure app is specified if [[ -z ${app} ]]; then lerror "Application name required." return 1 fi #If app has the flag_delimiter that means options are passed in local preserveCache=false if [ 0 = $(echo ${app} | grep -q [${flag_delimiter}]; echo $?) ]; then linfo "This app $app has passed in options" if [ 0 = $(echo ${app} | grep -q "preserveCache"; echo $?) ]; then preserveCache=true fi #Need to get app back to std naming (without params) app=`echo ${app} | grep -o ^.*- | sed s/-//g | cat -` fi # Make sure app is installed if [ ! -d "${ANDROID_DATA}/${app}" ]; then lerror "Package ${app} appears to not be installed." return 2 fi linfo "Backing up ${app}" local apk="$(find ${ANDROID_CODE}/${app}-*/base.apk | head -n1)" local data="${ANDROID_DATA}/${app}" # No need to stop the application because we're in recovery mode # Create backup destination if not exist [ ! -d "${backups}" ] && mkdir -p "${backups}" # Backup the apk file if it exists if [ ! -z "${apk}" ]; then # Delete the old backup first # There is a permissions issue querk when the backup was originall taken in # recovery mode, but later is taken in android (Permission denied for no # apparent reason) [ -f "${backups}/${app}/base.apk" ] && rm -f "${backups}/${app}/base.apk" # Copy the installer to the backup dir cp -p "${apk}" "${backups}/${app}/base.apk" else linfo "${app} apk file could not be found. Skipping apk installer backup." fi # If the data directory is not found or the data variable is empty, skip # backup. This covers a weird edge case where an application is installed but # hasn't been launched (I think?). The data variable would be empty, causing # this script to backup /*. if [ -z "${data}" ] || [ ! -d "${data}" ]; then lwarn "No data directory for application '${app}' found. Skipping backup." return 2 fi # Stop the application if it is running # Don't try to stop it if the twrp binary is found. If we're in recovery # mode, the application already isn't running. if [ ! -f /sbin/twrp ]; then linfo "Stopping application ${app}" am force-stop "${app}" else linfo "Skipping application force stop while booted to recovery mode." fi tar="tar -C ${data} -c " # Delete cache directory if it exists # This will sometimes free up significant amounts of space if [[ ! -d "${data}/cache" ]]; then linfo "Cache doesn't exist for ${app}" elif ! $preserveCache; then linfo "Excluding cache for ${app}" tar="${tar} --exclude=cache" else linfo "Preserving cache for ${app}" fi # Compress the backup linfo "Compressing userdata for ${app}" ${tar} . | gzip -c > "${backups}/${app}/data.tar.gz" } list_apps() { pm list package | cut -d ':' -f 2 | sort } restore_app() { local app=${1:-} # Make sure app is specified if [ -z "${app}" ]; then lerror "Please specify an app to restore." return 1 fi # If twrp binary is found on the filesystem, do not attempt restore as # restores cannot be done without a running system (the various android # binaries don't work right). if [ -f /sbin/twrp ]; then printf "Cannot perform a %s while in recovery mode.\n" "${app}" return 128 fi # When restoring something with flags need to restore app alone if [[ 0 = $(echo ${app} | grep -q [${flag_delimiter}]; echo $?) ]]; then linfo "This app $app had passed in options" #Need to get app back to std naming (without params) app=`echo ${app} | grep -o ^.*- | sed s/-//g | cat -` linfo "Restoring to std name \"${app}\" while we restore" fi # Check that backup exists to be restored if [[ ! -d ${backups}/${app} ]]; then lerror "No backup for ${app} exists." return 1 fi linfo "Restoring ${app}" # Install app if it is not yet installed if [[ ! -f "${backups}/${app}/base.apk" ]]; then linfo "Installer for ${app} not found. Only restoring data." elif [[ $(dumpsys package ${app} | grep -c userId) -eq 0 ]]; then linfo "Installer detected but package ${app} is not installed. Installing" pm install ${backups}/${app}/base.apk fi # Stop the application if it is running am force-stop ${app} # Get pertinent metadata local owner=$(dumpsys package ${app} | grep userId | cut -d'=' -f2 | head -n1) local code=$(dumpsys package ${app} | grep codePath | cut -d'=' -f2 | head -n1) local data=$(dumpsys package ${app} | grep dataDir | cut -d'=' -f2 | head -n1) # Decompress backup linfo "Decompressing user data for ${app}." cd ${backups}/${app}/ gzip -d -c data.tar.gz | tar -x # Copy the user data in cp -rp ${backups}/${app}/data/* ${data}/ # Fix data permissions chown -R ${owner}:${owner} ${data} # Fix selinux labels linfo "Restoring SELinux contexs for ${app}" restorecon -R ${data} 2>/dev/null # Cleanup the extracted data so restores don't take too much space rm -rf ${backups}/${app}/data } backup_apps() { local apps=${@} [ -z "${apps}" ] && lerror "At least one app is required." && return 1 IFS=' ' for app in ${apps}; do backup_app "${app}" done } restore_apps() { local apps=${@} [ -z "${apps}" ] && log error "At least one app is required.\n" && return 1 IFS=' ' for app in ${apps}; do restore_app "${app}" done } list_backup_apps() { local list=${1:-} [[ -z ${list} ]] && printf "A backup list is required.\n" && return 1 for app in $(cat ${list}); do backup_app "${app}" done } list_restore_apps() { local list=${1} [[ -z ${list} ]] && printf "A backup list is required.\n" && return 1 for app in $(cat ${list}); do restore_app ${app} done } verify_system() { if [ ! -d "${ANDROID_DATA}" ]; then printf "Data directory (%s) not found.\n" ${ANDROID_DATA} >&2 printf "Is it mounted?\n" >&2 return 1 fi if [ ! -d "${ANDROID_CODE}" ]; then printf "Application code directory (%s) not found.\n" ${ANDROID_CODE} >&2 printf "Is it mounted?\n" >&2 return 1 fi if [ ! -d "${ANDROID_SYSTEM}/etc" ]; then printf "System partition (%s) is not mounted.\n" ${ANDROID_SYSTEM} >&2 return 1 fi return 0 } main() { local cmd=${1:-} shift # Ensure root is running this script if [ $(id -u) -gt 0 ]; then lerror "Script must be run as root (uid 0)." return 1 fi verify_system || return 1 if [ "${cmd}" = 'backup' ]; then backup_apps ${@} elif [ "${cmd}" = 'listbackup' ]; then list_backup_apps ${@} elif [ "${cmd}" = 'restore' ]; then restore_apps ${@} elif [ "${cmd}" = 'listrestore' ]; then list_restore_apps ${@} elif [ "${cmd}" = 'ls' ] || [ "${cmd}" = 'list' ]; then list_apps elif [ "${cmd}" = 'help' ]; then usage else lerror "Unknown command '${cmd}'" usage return 1 fi } main ${@}