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