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