blob: 3bcc0649df3eaff43fec414a05cdc8b2cbf4a213 (
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 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 originally 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 # Decide if excluding cache or preserving
163 if [[ ! -d "${data}/cache" ]]; then
164 linfo "Cache doesn't exist for ${app}"
165 elif [ "$preserve_cache" -eq 0 ]; then
166 linfo "Excluding cache for ${app}"
167 taropts="${taropts} --exclude=cache"
168 else
169 linfo "Preserving cache for ${app}"
170 fi
171
172 # Compress the backup
173 linfo "Compressing userdata for ${app}"
174 eval tar ${taropts} . | gzip -c > "${backups}/${app}/data.tar.gz"
175 }
176
177
178 list_apps() {
179 pm list package | cut -d ':' -f 2 | sort
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 shift
316
317 if [ "${cmd}" = 'help' ]; then
318 usage
319 return 0
320 fi
321
322 # Ensure root is running this script
323 if [ $(id -u) -gt 0 ]; then
324 lerror "Script must be run as root (uid 0)."
325 return 1
326 fi
327
328 verify_system || return 1
329
330 if [ "${cmd}" = 'backup' ]; then
331 backup_apps ${@}
332 elif [ "${cmd}" = 'listbackup' ]; then
333 list_backup_apps ${@}
334 elif [ "${cmd}" = 'restore' ]; then
335 restore_apps ${@}
336 elif [ "${cmd}" = 'listrestore' ]; then
337 list_restore_apps ${@}
338 elif [ "${cmd}" = 'ls' ] || [ "${cmd}" = 'list' ]; then
339 list_apps
340 else
341 lerror "Unknown command '${cmd}'"
342 usage
343 return 1
344 fi
345 }
346
347 main ${@}
|