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 ${@}
|