#!/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 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 args="${2:-}" local preserve_cache=0 # Whether to preserve cache in the backup. Can yield # notably larger backups for some applications local origifs=${IFS} # Input field seperator at runtime. Useful for # reverting # Make sure app is specified if [[ -z ${app} ]]; then lerror "Application name required." return 1 fi # Handle passed arguments IFS=' ' for arg in ${args}; do # Don't clean up the cache if preserveCache is specified if [ "${arg}" = 'preserveCache' ]; then preserve_cache=true else # Do nothing with unknown arguments lwarn "${app}: Unknown backup argument '${arg}'" fi done IFS=${origifs} # 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}/${app}" ] && mkdir -p "${backups}/${app}" # Backup the apk file if it exists if [ ! -z "${apk}" ] && [ -f "${apk}" ]; then # Delete the old backup first # There is a permissions issue querk when the backup was originally 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 taropts=" -C ${data} -c " # Decide if excluding cache or preserving if [[ ! -d "${data}/cache" ]]; then linfo "Cache doesn't exist for ${app}" elif [ "$preserve_cache" -eq 0 ]; then linfo "Excluding cache for ${app}" taropts="${taropts} --exclude=cache" else linfo "Preserving cache for ${app}" fi # Compress the backup linfo "Compressing userdata for ${app}" eval tar ${taropts} . | gzip -c > "${backups}/${app}/data.tar.gz" } list_apps() { for i in $(ls /data/app); do printf "%s\n" "${i%-*}" done } 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 data="/data/data/${app}" # Decompress data backup linfo "Decompressing user data for ${app}." gzip -d -c ${backups}/${app}/data.tar.gz | tar -x -C "${data}" # Fix data permissions chown -R ${owner}:${owner} "${data}" # Fix selinux labels linfo "Restoring SELinux contexs for ${app}" restorecon -R "${data}" 2>/dev/null } 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." && return 1 local app # Application mame local args # Application arguments export IFS=$'\n' for line in $(cat ${list}); do # Parse out the app name app="$(echo ${line} | tr -s ' ' | cut -d ' ' -f 1)" # Parse out the arguments (if any) args="$(echo ${line} | tr -s ' ' | cut -s -d ' ' -f 2-)" # Execute the backup backup_app "${app}" "${args}" 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:-}" if [ -z "${cmd:-}" ]; then printf "Action required (backup, listbackup, restore, listrestore)\n" return 1 fi shift if [ "${cmd}" = 'help' ]; then usage return 0 fi # 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 else lerror "Unknown command '${cmd}'" usage return 1 fi } main ${@}