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