blob: 4296973d6270e4717523290f7f1c37cbc47dc91d (
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 pm list package | cut -d ':' -f 2 | sort
178 }
179
180
181 restore_app() {
182 local app=${1:-}
183
184 # Make sure app is specified
185 if [ -z "${app}" ]; then
186 lerror "Please specify an app to restore."
187 return 1
188 fi
189
190 # If twrp binary is found on the filesystem, do not attempt restore as
191 # restores cannot be done without a running system (the various android
192 # binaries don't work right).
193 if [ -f /sbin/twrp ]; then
194 printf "Cannot perform a %s while in recovery mode.\n" "${app}"
195 return 128
196 fi
197
198 # When restoring something with flags need to restore app alone
199 if [[ 0 = $(echo ${app} | grep -q [${flag_delimiter}]; echo $?) ]]; then
200 linfo "This app $app had passed in options"
201 #Need to get app back to std naming (without params)
202 app=`echo ${app} | grep -o ^.*- | sed s/-//g | cat -`
203 linfo "Restoring to std name \"${app}\" while we restore"
204 fi
205
206 # Check that backup exists to be restored
207 if [[ ! -d ${backups}/${app} ]]; then
208 lerror "No backup for ${app} exists."
209 return 1
210 fi
211
212 linfo "Restoring ${app}"
213
214 # Install app if it is not yet installed
215 if [[ ! -f "${backups}/${app}/base.apk" ]]; then
216 linfo "Installer for ${app} not found. Only restoring data."
217 elif [[ $(dumpsys package ${app} | grep -c userId) -eq 0 ]]; then
218 linfo "Installer detected but package ${app} is not installed. Installing"
219 pm install ${backups}/${app}/base.apk
220 fi
221
222 # Stop the application if it is running
223 am force-stop ${app}
224
225 # Get pertinent metadata
226 local owner=$(dumpsys package ${app} | grep userId | cut -d'=' -f2 | head -n1)
227 local data="/data/data/${app}"
228
229 # Decompress data backup
230 linfo "Decompressing user data for ${app}."
231 gzip -d -c ${backups}/${app}/data.tar.gz | tar -x -C "${data}"
232
233 # Fix data permissions
234 chown -R ${owner}:${owner} "${data}"
235 # Fix selinux labels
236 linfo "Restoring SELinux contexs for ${app}"
237 restorecon -R "${data}" 2>/dev/null
238 }
239
240
241 backup_apps() {
242 local apps=${@}
243 [ -z "${apps}" ] && lerror "At least one app is required." && return 1
244
245 IFS=' '
246 for app in ${apps}; do
247 backup_app "${app}"
248 done
249 }
250
251
252 restore_apps() {
253 local apps=${@}
254 [ -z "${apps}" ] && log error "At least one app is required.\n" && return 1
255
256 IFS=' '
257 for app in ${apps}; do
258 restore_app "${app}"
259 done
260 }
261
262
263 list_backup_apps() {
264 local list=${1:-}
265 [ -z "${list}" ] && printf "A backup list is required." && return 1
266
267 local app # Application mame
268 local args # Application arguments
269
270 export IFS=$'\n'
271 for line in $(cat ${list}); do
272 # Parse out the app name
273 app="$(echo ${line} | tr -s ' ' | cut -d ' ' -f 1)"
274 # Parse out the arguments (if any)
275 args="$(echo ${line} | tr -s ' ' | cut -s -d ' ' -f 2-)"
276 # Execute the backup
277 backup_app "${app}" "${args}"
278 done
279 }
280
281
282 list_restore_apps() {
283 local list=${1}
284 [[ -z ${list} ]] && printf "A backup list is required.\n" && return 1
285
286 for app in $(cat ${list}); do
287 restore_app ${app}
288 done
289 }
290
291
292 verify_system() {
293 if [ ! -d "${ANDROID_DATA}" ]; then
294 printf "Data directory (%s) not found.\n" ${ANDROID_DATA} >&2
295 printf "Is it mounted?\n" >&2
296 return 1
297 fi
298 if [ ! -d "${ANDROID_CODE}" ]; then
299 printf "Application code directory (%s) not found.\n" ${ANDROID_CODE} >&2
300 printf "Is it mounted?\n" >&2
301 return 1
302 fi
303 if [ ! -d "${ANDROID_SYSTEM}/etc" ]; then
304 printf "System partition (%s) is not mounted.\n" ${ANDROID_SYSTEM} >&2
305 return 1
306 fi
307 return 0
308 }
309
310
311 main() {
312 local cmd="${1:-}"
313
314 if [ -z "${cmd:-}" ]; then
315 printf "Action required (backup, listbackup, restore, listrestore)\n"
316 return 1
317 fi
318
319 shift
320
321 if [ "${cmd}" = 'help' ]; then
322 usage
323 return 0
324 fi
325
326 # Ensure root is running this script
327 if [ $(id -u) -gt 0 ]; then
328 lerror "Script must be run as root (uid 0)."
329 return 1
330 fi
331
332 verify_system || return 1
333
334 if [ "${cmd}" = 'backup' ]; then
335 backup_apps ${@}
336 elif [ "${cmd}" = 'listbackup' ]; then
337 list_backup_apps ${@}
338 elif [ "${cmd}" = 'restore' ]; then
339 restore_apps ${@}
340 elif [ "${cmd}" = 'listrestore' ]; then
341 list_restore_apps ${@}
342 elif [ "${cmd}" = 'ls' ] || [ "${cmd}" = 'list' ]; then
343 list_apps
344 else
345 lerror "Unknown command '${cmd}'"
346 usage
347 return 1
348 fi
349 }
350
351 main ${@}
|