-
Notifications
You must be signed in to change notification settings - Fork 0
/
Copy pathrbackup.sh
executable file
·286 lines (243 loc) · 11.6 KB
/
rbackup.sh
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
#!/bin/bash
# rbackup - remote backup utility - version 2.3 - July 2013
# Copyright © 2011 2012 2013 Giuseppe Cuccu - all rights reserved
# This script is provided AS USED and under constant updating - NO WARRANTY
# Initialization and intro
intro() {
# where to look for configuration file
conf_file="${HOME}/.rbackup.conf"
starting_time=$( date +%s )
echo -e "\n\t rbackup"
echo
}
# Check if the config file is present, create it otherwise
load_conf() {
if [[ ! -f "${conf_file}" ]]; then
echo "Config file ${conf_file} not found - creating"
write_conf_file
echo "Config file ${conf_file} created - please customize it and try again"
exit 1
fi
source ${conf_file}
}
# Set destination variables: srv, srvp, path
set_dest () {
# process array of lists of words in input
for deststr in "${dest_lst[@]}"; do
dest=($deststr)
# type of dest depends on number of params: 1 -> local, 2 -> remote, 3 -> remote+port
dest_type=${#dest[@]}
case $dest_type in
1 )
unset srv; unset srvp; path=${dest[0]}
echo -e " Accessing local directory\n Path: ${path}"
;;
2 )
srv=${dest[0]}; srvp=22; path=${dest[1]}
echo -e " Accessing remote server\n Server: ${srv} - Path: ${path}"
;;
3 )
srv=${dest[0]}; srvp=${dest[1]}; path=${dest[2]}
echo -e " Accessing remote server\n Server: ${srv} - Port: ${srvp} - Path: ${path}"
;;
* )
echo " ERROR! One of the destinations has a wrong number of arguments! Check config file!"
echo ${dest[@]}
exit 1
;;
esac
if accessible ; then # if the current configuration is accessible for backup
echo " OK!"
local dest_found=0 # accessible destination found
break # exit the loop
else
echo " FAIL"
fi
done
echo
# unless found accessible destination, give error and exit
if [[ -z ${dest_found} ]]; then
echo "ERROR! None of the destinations provided was accessible!"
exit 1
fi
}
# Verify if current location is accessible
accessible() {
case $dest_type in
1 ) # local
[[ -d $path ]]
;;
2|3 ) # remote
nc -zw 10 $srv $srvp 2> /dev/null && ssh $srv -p $srvp test -d $path
;;
* ) # error
echo "ERROR! unrecognized type!"
exit 1
;;
esac
}
# Set system variables based on configuration
pre_call() {
# rcmd will send the commands to the remote server, if present
if [[ $dest_type -eq 2 || $dest_type -eq 3 ]]; then
rcmd="ssh -p ${srvp} ${srv}"; else unset rcmd; fi
tmp="${path}/partial" # Temporary backup folder on destination
cur="${path}/current" # Symlink to current (latest) snapshot
while ! ${rcmd} [[ -d "${cur}" ]]; do force_create_cur; done # Enforce the use of --link-dest feature
dest="${path}/$( date +%y%m%d__%a_%d_%b__%H:%M )" # Time-based named backup destination directory
if ${rcmd} [[ -d "${dest}" ]]; then dest+="$( date +:%S )"; fi # Add seconds if multiple backups per minute
}
# Forces creation of link to current backup if not present
force_create_cur() {
newest="${path}/$( ${rcmd} ls -t1 -I ${cur} -I ${tmp} ${path} | head -1 )"
echo -e " BEWARE! Link to latest backup not found\n -> cannot continue\n Most recent backup:\n ${newest}\n Required link name:\n ${cur}\n Link name:\n ${cur}"
read -p " Do you wish me to create it for you? [y/n]: " -n 1 -r
if [[ $REPLY =~ ^[Yy]$ ]]
then
echo -e "\n\n Creating link to latest backup...\n"
${rcmd} ln -s "${newest}" "${cur}"
else
echo -e "\n\nERROR: Link to latest backup not found."
exit 1
fi
}
# rsync main call:
# backup main folder, propagate deletions, use exclusion list
rsync_main() {
rsync_origin="${origin}/"
rsync_dest="${tmp}"
rsync_current="--link-dest=${cur}"
rsync_deletion="--delete --delete-excluded"
rsync_exclusion=$( # add from exclusions
for excl in ${exclusions}; do
echo -n " --exclude $excl"; done)
rsync_exclusion+=$( # add from up-only folders
for up in "${up_only[@]}"; do
echo -n " --exclude /${up%% *}/"; done ) # first element
echo -e " Main backup:\n ${rsync_origin}"
rsync_call
echo
}
# rsync up-only calls:
# upload from up_only list, don't propagate deletions, ignore exclusion list
rsync_uponly() {
unset rsync_current
unset rsync_deletion
unset rsync_exclusion
for up in "${up_only[@]}"; do
rsync_origin="${origin}/${up%% *}/" # first element
rsync_dest="${path}/${up##* }" # last element
if [[ -d "${rsync_origin}" ]]; then
echo -e " Synchronizing:\n ${rsync_origin}"
rsync_call
echo
else
echo -e " Not found:\n ${rsync_origin}\n -- skipping..."
echo
fi
done
}
# rsync call
rsync_call(){
# build rsync destination
rsync_dest="${srv+${srv}:}${rsync_dest}"
rsync_rsh="${srvp+--rsh=ssh -p ${srvp}}"
rsync_flags='-'
rsync_flags+='a' # archive mode
rsync_flags+='z' # compress data
rsync_flags+='h' # human readable sizes
rsync_flags+='P' # show progress
# Main call
echo " Launching rsync"
# (set -x # uncomment subshell to print rsync command expansion
rsync \
${rsync_flags} \
"${rsync_rsh}" \
${rsync_deletion} \
${rsync_exclusion} \
${rsync_current} \
${rsync_origin} \
${rsync_dest}
# ) # uncomment subshell to print rsync command expansion
# If failed, show error and exit
if [[ $? -ne 0 ]]; then
echo "ERROR!! rsync reported an error status"
exit 1
fi
}
# count the number of versions currently maintained
update_nvers() { nvers=$( ${rcmd} ls -l ${path} -I ${cur} -I ${tmp} | grep ^d | wc -l ); }
# Round up after the main call
post_main() {
${rcmd} mv "${tmp}" "${dest}" # Make the temporary backup definitive
${rcmd} rm -f "${cur}" # Remove previous current link
${rcmd} ln -s "${dest}" "${cur}" # Make the new backup current
${rcmd} touch "${dest}" # Fix creation time of new backup
# Count the number of versions
update_nvers
# If there are more versions than desired, delete oldest backups
# (if they're less, this number will grow with each new backup)
while [[ ${nvers} -gt ${desired_vers} ]]; do
oldest="${path}/$( ${rcmd} ls -tr1 ${path} | head -1 )"
echo -e "\n Removing oldest backup:\n ${oldest} \n please wait..."
${rcmd} rm -rf "${oldest}"
update_nvers
done
}
# Calculate the total time and print some stats
print_time() {
echo -e "\n\t SUCCESS!\n"
tot_s=$(( $(date +%s) - ${starting_time?"ERROR!! Set starting_time at beginning of script!"} ))
tot_m=$(echo -e "scale=1;${tot_s}/60.0" | bc)
tot_h=$(echo -e "scale=1;${tot_s}/3600.0" | bc)
echo -e " Total time:"
if [[ ${tot_s} -ge 3600 ]]; then echo -e " ${tot_h} hours"
elif [[ ${tot_s} -ge 60 ]]; then echo -e " ${tot_m} minutes"
else echo -e " ${tot_s} seconds"
fi
echo -e " Backup stored at:\n ${srv+${srv}:}${dest}"
echo -e " You are currently maintaining ${nvers} versions.\n"
}
# Write down a default configuration file
write_conf_file() {
local conf='#!/bin/bash\n# rsync configuration\n#\n# THIS IS JUST AN EXAMPLE -- customize this file before using it!\n# NOTE: be very careful to escaping spaces in paths!!!\n\n
# `origin` is The One, what you want to mirror in your backups\norigin="${HOME}"\n
# How much redundancy is enough? You choose how often you backup, and you choose\n# how many versions to keep.\n#
# `desired_vers` is the number of versions you wish to maintain\n# all backups older than the first `desired_vers` will be deleted!
desired_vers=5\n\n
# Sometimes you backup to an external HDD for speed. Then you just connect it to\n# a home server and leave it there. Sometimes you backup to it through the LAN,\n# sometimes from outside. You need that flexibility.\n#
# `dest_lst` is a prioritized array of where to find/add your backups\n# destinations are strings that look like this: `[server [port]] local_path`\n# Add one destination per line, the first working destination will be used
dest_lst=( "/media/giuse/backup_hd/giuse" # backup drive mounted locally (USB)\n "bkpsrv.local /backup_hd/giuse" # backup drive mounted to local server (LAN)\n "mydns.dyndnsprovider.com 8080 /backup_hd/giuse" ) # backup from outside my LAN\n\n
# Having a large archive of music but wanting to carry around just few albums?\n# Or same deal with pictures, ebooks, videos, software?\n# These destinations will update any modification to files you have a copy of,\n# so you can change the tags on MP3 files locally then upload.\n# Any new file will be uploaded too, like new pictures or videos. But files not\n# found locally will not be deleted from the destination.\n# Note how these files often tend to be private on a laptop, but shared in a\n# home environment. Hence the destination often leaving the private backup folder.\n#
# `up_only` is an array of couples of paths RELATIVE to the above `origin` and destination\n# each points to an archive you want to keep updated but do not want to entirely carry around\n# each of these origins will be used to updated the corresponding destination without deletion\n# origin destination
up_only=(\n "archive/ebooks ../share/ebooks"\n "archive/music ../share/music_giuse"\n "archive/pictures ../share/pics_giuse"\n "archive/software ../share/software"\n "archive/videos ../share/videos"\n # QUICK HACK - android backup\n "../../media/042F-1590 ../backup_android"\n )\n
# And then of course there is plenty of folders you do not want to back up.\n# Caches, stuff you are backing up by other means, thumbnails...\n# Feel free anything that is slowing you down on a daily basis.\n#
# `exclusions` is a list of paths to exclude from backup\n# - can use regular expressions\n# - / at the beginning means start from the above declared origin\n# - / at the end means "all directory content"\n# - example: add "/.*/" to exclude all hidden directories in origin\n# See man rsync page for a thorough reference\n\n
exclusions="\n *.part\n /exclude/\n /Dropbox/\n /.dropbox*/\n .dropbox/\n /.local/share/Trash/\n /.rvm/\n /.cache/\n /.mozilla/\n /.config/\n /.local/\n /.gconf/\n /.gnome2/\n /.thumbnails/\n /.xsession*\n /.gvfs/\n /.wine/\n"\n
'
echo -e "${conf}" > "${conf_file}"
}
# Execution control
main() {
intro # Initialization and intro
load_conf # Check if the config file is present, and if not write it
set_dest # Set destination variables: srv, srvp, path
pre_call # Set system variables based on configuration
rsync_uponly # rsync main call: backup main folder, propagate deletions, use exclusion list
rsync_main # rsync up-only calls: upload from folder list, no deletions, no exclusion list
post_main # Round up after the main call
print_time # Calculate the total time and print some stats
}
# Actual call
main
# TODO NEXT:
# - add server user name to configuration
# - add username to ssh calls: dests need to be either path or srv port user path
# - cli parser
# - verify writing permissions on destination directory d rwx r-x --- # ${rcmd} mkdir -p -m 750 "${path}"
# - load_conf implementation
# - notification popup of starting + result of backup
# - cron integration
# - function to restore a folder (works also for full backup)
# - write backup conf depending on GUI choices
# - create file for rsync exclusions, use it, back it up, then delete it