blob: 40ab2244fdb109c352a71c833783cbde5744817f (
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
24
25 usage() {
26 # NOTE: The maximum width of this text should be 65 chars. This
27 # will ensure it displays properly on most devices without
28 # strange wrapping.
29 # If using vim to reflow, execute ':set tw=65'
30 out="
31 Andbackup is a basic Android application (or package) backup and
32 restore script. It can backup a single application, or a a list
33 of applications provided in a text file (one application name per
34 line). It can do the same for application restore.
35
36 The default backup path is '/sdcard/andbackup/'.
37
38 Usage:
39 andbackup.sh <command> [opts]
40
41 Commands:
42 help Print this help text
43 backup <app_name> Back up a single package
44 restore <app_name> Restore a single package
45 list List installed packages
46 listbackup <list_file> Backup packages using a list text file
47 listrestore <list_file> Restore packages from a list text file
48
49 "
50 printf "${out}"
51 }
52
53
54 log() {
55 logtype=${1:-}
56 logmsg=${2:-}
57 logdate=$(date '+%T %F')
58
59 printf "%s %s %s\n" "${logdate}" "${logtype}" "${logmsg}"
60 }
61
62 # Some useful macros
63 linfo() {
64 log "info " "${1:-}"
65 }
66
67 lerror() {
68 log "error" "${1:-}"
69 }
70
71 lwarn() {
72 log "warn " "${1:-}"
73 }
74
75 lfatal() {
76 log "fatal" "${1:-}"
77 }
78
79
80 backup_app() {
81 local app=${1:-}
82
83 # Make sure app is specified
84 if [[ -z ${app} ]]; then
85 lerror "Please specify an app to backup."
86 return 1
87 fi
88
89 # Make sure app is installed
90 if [[ $(dumpsys package ${app} | wc -l) -eq 0 ]]; then
91 lerror "Package ${app} appears to not be installed."
92 return 1
93 fi
94
95 linfo "Backing up ${app}"
96
97 # Stop the application if it is running
98 am force-stop ${app}
99
100 # Get pertinent metadata
101 local code=$(dumpsys package ${app} | grep codePath | cut -d'=' -f2 | head -n1)
102 local data=$(dumpsys package ${app} | grep dataDir | cut -d'=' -f2 | head -n1)
103
104 # Create backup destination if not exist
105 [[ ! -d ${backups} ]] && mkdir -p ${backups}
106 [[ ! -d ${backups}/${app}/data ]] && mkdir -p ${backups}/${app}/data
107
108 # Backup the apk file if it exists
109 if [[ -f ${code}/base.apk ]]; then
110 cp ${code}/base.apk ${backups}/${app}/base.apk
111 else
112 linfo "${app} apk file could not be found. Skipping apk installer backup."
113 fi
114
115 # Copy the user data
116 cp -rp ${data}/* ${backups}/${app}/data/
117 # Delete cache directory if it exists
118 # This will sometimes significant amounts of space
119 if [[ -d "${backups}/${app}/data/cache" ]]; then
120 rm -rf "${backups}/${app}/data/cache"
121 fi
122
123 # Compress the backup
124 linfo "Compressing userdata for ${app}"
125 cd ${backups}/${app}/
126 tar -c data | gzip -c > data.tar.gz
127 rm -rf data
128 }
129
130
131 function list_apps {
132 pm list package | cut -d ':' -f 2 | sort
133 }
134
135
136 restore_app() {
137 local app=${1:-}
138
139 # Make sure app is specified
140 if [[ -z ${app} ]]; then
141 lerror "Please specify an app to restore."
142 return 1
143 fi
144
145 # Check that backup exists to be restored
146 if [[ ! -d ${backups}/${app} ]]; then
147 lerror "No backup for ${app} exists."
148 return 1
149 fi
150
151 linfo "Restoring ${app}"
152
153 # Install app if it is not yet installed
154 if [[ ! -f "${backups}/${app}/base.apk" ]]; then
155 linfo "Installer for ${app} not found. Only restoring data."
156 elif [[ $(dumpsys package ${app} | grep -c userId) -eq 0 ]]; then
157 linfo "Installer detected but package ${app} is not installed. Installing"
158 pm install ${backups}/${app}/base.apk
159 fi
160
161 # Stop the application if it is running
162 am force-stop ${app}
163
164 # Get pertinent metadata
165 local owner=$(dumpsys package ${app} | grep userId | cut -d'=' -f2 | head -n1)
166 local code=$(dumpsys package ${app} | grep codePath | cut -d'=' -f2 | head -n1)
167 local data=$(dumpsys package ${app} | grep dataDir | cut -d'=' -f2 | head -n1)
168
169 # Decompress backup
170 linfo "Decompressing user data for ${app}."
171 cd ${backups}/${app}/
172 gzip -d -c data.tar.gz | tar -x
173
174 # Copy the user data in
175 cp -rp ${backups}/${app}/data/* ${data}/
176 # Fix data permissions
177 chown -R ${owner}:${owner} ${data}
178 # Fix selinux labels
179 linfo "Restoring SELinux contexs for ${app}"
180 restorecon -R ${data} 2>/dev/null
181
182 # Cleanup the extracted data so restores don't take too much space
183 rm -rf ${backups}/${app}/data
184 }
185
186
187 backup_apps() {
188 local apps=${@}
189 [[ -z ${apps[@]:-} ]] && lerror "At least one app is required." && return 1
190
191 for app in ${apps[@]}; do
192 backup_app ${app}
193 done
194 }
195
196
197 restore_apps() {
198 local apps=${@}
199 [[ -z ${apps[@]:-} ]] && log error "At least one app is required." && return 1
200
201 for app in ${apps[@]}; do
202 restore_app ${app}
203 done
204 }
205
206
207 list_backup_apps() {
208 local list=${1:-}
209 [[ -z ${list} ]] && printf "A backup list is required." && return 1
210
211 for app in $(cat ${list}); do
212 backup_app ${app}
213 done
214 }
215
216
217 list_restore_apps() {
218 local list=${1}
219 [[ -z ${list} ]] && printf "A backup list is required." && return 1
220
221 for app in $(cat ${list}); do
222 restore_app ${app}
223 done
224 }
225
226
227 main() {
228 local cmd=${1:-}
229 shift
230
231 # Ensure root is running this script
232 if [ $(id -u) -gt 0 ]; then
233 lerror "Script must be run as root (uid 0)."
234 return 1
235 fi
236
237
238 if [ "${cmd}" = 'backup' ]; then
239 backup_apps ${@}
240 elif [ "${cmd}" = 'listbackup' ]; then
241 list_backup_apps ${@}
242 elif [ "${cmd}" = 'restore' ]; then
243 restore_apps ${@}
244 elif [ "${cmd}" = 'listrestore' ]; then
245 list_restore_apps ${@}
246 elif [ "${cmd}" = 'ls' ] || [ "${cmd}" = 'list' ]; then
247 list_apps
248 elif [ "${cmd}" = 'help' ]; then
249 usage
250 else
251 lerror "Unknown command '${cmd}'"
252 usage
253 return 1
254 fi
255 }
256
257 main ${@}
|