-
Notifications
You must be signed in to change notification settings - Fork 0
/
Copy pathbackush
executable file
·569 lines (505 loc) · 16.6 KB
/
backush
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
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
#! /usr/bin/env bash
#######################################################################
## What this program does:
## 0. Set functions 'logs', 'errs', 'depend', 'run', and 'onearg'
## 1. Check if colors are possible, if so, reset 'logs' and add color codes
## 2. Check if various tools are available and set signal handling
## 3. Process server list into an array
## 4. Call 'clean' recursively
## 5. Get the servers that will be used (via stdin)
## 6. Perform actual up/down -loading
## 7. Run 'contamine' recursively
#######################################################################
set -e
version() {
cat <<EOF
backush (BacKush) version 0.5.3
Copyright (C) 2016 Kevin Robert Stravers
License GPLv3+: GNU GPL version 3 or later <http://gnu.org/licenses/gpl.html>
This is free software: you are free to change and redistribute it.
There is NO WARRANTY, to the extent permitted by law.
EOF
}
usage() {
version
local program="${BASH_SOURCE[0]##*/}"
cat <<EOF
Usage:
$program
Start interactive mode in push.
$program --push [ server1 server2 ... ]
Push to one or more servers. If no servers are provided, interactive mode.
$program --pull [ server1 server2 ... ]
Pull from one or more servers. If no servers are provided, interactive mode.
$program --clean
Only run 'clean' scripts found from the current directory.
$program --contamine
Only run 'contamine' scripts found from the current directory.
$program --version
Show the version.
$program --help
Show this message.
$program --check
Only check if all necessary dependencies are installed.
$program --acon
Extract the ACON snippets and generate an ACON parser.
EOF
}
if [ "$1" = "--help" ]; then
usage
exit
fi
if [ "$1" = "--version" ]; then
version
exit
fi
if [ "$1" = "--push" ] || [ "$1" = "--pull" ] || [ "$1" = "--" ]; then
if [ "$#" -gt 1 ]; then
bash -c "${BASH_SOURCE[0]} $1 <<< '${*:2}'"
exit
fi
fi
#######################################################################
# 0. Set functions 'logs', 'errs', 'depend', 'run', 'onearg', 'escape', and 'escape_all'
#######################################################################
logs() { printf "$2"; } # Log to stdout
errs() { logs "$@" >&2; } # Log to stderr
depend() { # Check dependencies
local lacking=0
for com in "$@"; do
if command -v "$com" >/dev/null 2>&1; then
errs "$Green" "Found program $com\n"
else
lacking=1
errs "$Red" "This script requires ""$com"", but it's not installed.\n"
fi
done
if [ "$lacking" -eq 1 ]; then
errs "$Red" 'Aborting.\n'
exit 1
fi
}
run() { # Run a script in its native folder
cd "${1%/*}"
local prepper="$(echo $1 | sed 's/\//\\\//g')"
printf "$Blue"
./"${1##*/}" 3>&1 2>&1 |& sed "s/^/$prepper: /g"
RET="${PIPESTATUS[0]}"
printf "$Color_Off"
local code="$RET"
if [ "$code" -ne 0 ]; then
return 1
fi
}
onearg() { # Compare two arguments
if [ "$1" = "$2" ]; then
return 0
else
return 1
fi
}
escape() {
awk '
// {
split($0, array, "\\([0-9]+\\)", separators);
len = length(array);
seplen = length(separators);
for (i = 1; i <= len; ++i) {
separators[i] = gensub("\\(|\\)", "", "g", separators[i]);
}
for (i = 1; i <= len; ++i) {
printf "%s%c", array[i], int(separators[i]);
}
}
'
}
escape_all() {
local index=0
new_array=()
while [ "$index" -lt "${#array[@]}" ]; do
new_array+=("$(echo "${array[$index]}" | escape)")
: $((++index))
done
}
#######################################################################
# 1. Check if colors are possible, if so, reset 'logs' and add color codes
#######################################################################
errs "$Yellow" "Checking dependecies for printing color\n"
depend printf sed tput
if test -t 1 && [ "$(tput colors)" -ge 8 ]; then
logs() { printf "$(printf '%s' "$2" | sed "s/^/$1/g" | sed "s/\$/${Color_Off}/g")"; } # Log to stdout, use color coding
# Reset
Color_Off=$'\e[0m' # Text Reset
# Regular Colors
Black=$'\e[0;30m' # Black
Red=$'\e[0;31m' # Red
Green=$'\e[0;32m' # Green
Yellow=$'\e[0;33m' # Yellow
Blue=$'\e[0;34m' # Blue
Purple=$'\e[0;35m' # Purple
Cyan=$'\e[0;36m' # Cyan
White=$'\e[0;37m' # White
# Bold
BBlack=$'\e[1;30m' # Black
BRed=$'\e[1;31m' # Red
BGreen=$'\e[1;32m' # Green
BYellow=$'\e[1;33m' # Yellow
BBlue=$'\e[1;34m' # Blue
BPurple=$'\e[1;35m' # Purple
BCyan=$'\e[1;36m' # Cyan
BWhite=$'\e[1;37m' # White
# Underline
UBlack=$'\e[4;30m' # Black
URed=$'\e[4;31m' # Red
UGreen=$'\e[4;32m' # Green
UYellow=$'\e[4;33m' # Yellow
UBlue=$'\e[4;34m' # Blue
UPurple=$'\e[4;35m' # Purple
UCyan=$'\e[4;36m' # Cyan
UWhite=$'\e[4;37m' # White
# Background
On_Black=$'\e[40m' # Black
On_Red=$'\e[41m' # Red
On_Green=$'\e[42m' # Green
On_Yellow=$'\e[43m' # Yellow
On_Blue=$'\e[44m' # Blue
On_Purple=$'\e[45m' # Purple
On_Cyan=$'\e[46m' # Cyan
On_White=$'\e[47m' # White
# High Intensity
IBlack=$'\e[0;90m' # Black
IRed=$'\e[0;91m' # Red
IGreen=$'\e[0;92m' # Green
IYellow=$'\e[0;93m' # Yellow
IBlue=$'\e[0;94m' # Blue
IPurple=$'\e[0;95m' # Purple
ICyan=$'\e[0;96m' # Cyan
IWhite=$'\e[0;97m' # White
# Bold High Intensity
BIBlack=$'\e[1;90m' # Black
BIRed=$'\e[1;91m' # Red
BIGreen=$'\e[1;92m' # Green
BIYellow=$'\e[1;93m' # Yellow
BIBlue=$'\e[1;94m' # Blue
BIPurple=$'\e[1;95m' # Purple
BICyan=$'\e[1;96m' # Cyan
BIWhite=$'\e[1;97m' # White
# High Intensity backgrounds
On_IBlack=$'\e[0;100m' # Black
On_IRed=$'\e[0;101m' # Red
On_IGreen=$'\e[0;102m' # Green
On_IYellow=$'\e[0;103m' # Yellow
On_IBlue=$'\e[0;104m' # Blue
On_IPurple=$'\e[0;105m' # Purple
On_ICyan=$'\e[0;106m' # Cyan
On_IWhite=$'\e[0;107m' # White
fi
#######################################################################
# 2. Check if various tools are available and set signal handling
#######################################################################
errs "$Yellow" "Checking dependencies for processing the server list\n"
depend awk cat cd declare find grep read rsync pwd ssh xargs
if [ "$1" = "--check" ]; then
errs "$Green" "All dependencies were found\n"
exit
fi
if [ "$1" = "--acon" ]; then
selfing() {
cat "${BASH_SOURCE[0]}" | awk '
BEGIN { inside = 0; }
/^## BEGIN '"$1"'$/ { inside = 1; next; }
/^## END '"$1"'$/ { exit; }
{ if (inside == 1) print; }
' | sed 's/\t//g'
}
echo "#! /usr/bin/awk -f" > acon.awk
selfing ACON | sed 's/\\\$/$/g' >> acon.awk
errs "$Green" 'Extracted acon.awk\n'
echo "#! /bin/bash" > aconf
selfing ACONF >> aconf
errs "$Green" 'Extracted aconf\n'
exit
fi
#######################################################################
# 3. Process server list into an array
#######################################################################
if ! onearg "$1" "--clean" && ! onearg "$1" "--contamine"; then
if [ -f servers ]; then
errs "$Green" "Located backup server list at $(pwd)/servers\n"
else
errs "$Red" "$(pwd)/servers does not exist! Creating an example\n"
cat <<EOF > servers
# Example backup server file
# The address is provided to rsync and ssh (where the server is)
# The push and pull variables denote what happens when you either push or pull
# All of these are rsync flags, escape spaces using (32)
# path specifies which directories and/or files to up/down -load
# A star can be used here to denote every file
# All paths are from the current working directory, but can also be absolute
# Minimal example
{ offsite-backup
address username@server
push -P --timeout=10
path ./stuf/
from file1 folder1
}
# With SSH-execution after backup
{ onsite-backup
address 192.168.1.1
push --rsh=ssh(32)-p55555 -P --delete --timeout=10
pull --rsh=ssh(32)-p55555 -P --delete --timeout=30
path ./folder1/
from .
execute date >> backup_date
execute_flags -p 55555
}
EOF
exit 1
fi
errs "$Yellow" "Loading the ACON parser\n"
acon=$(cat <<EOF
## BEGIN ACON
BEGIN { paths = 0; lists = 0; }
function printElements(start) { for (i = start; i <= NF; ++i) printf " %s", \$i; }
function isInArray() { return lists > 0 && topList() == paths; }
function pushPath(argument) { path[paths] = argument; ++paths; }
function popPath() { if (paths <= 0) { printf "Can not pop path, already empty" | "cat 1>&2"; exit 1; } else { --paths; return path[paths]; }}
function pushList(argument) { list[lists] = argument; ++lists; }
function popList() { if (lists <= 0) { printf "Can not pop list, already empty" | "cat 1>&2"; exit 1; } else { --lists; return list[lists]; }}
function topList() { if (lists <= 0) { printf "Can not get top element, list is empty" | "cat 1>&2"; exit 1; } else { return list[lists - 1]; }}
function incrementIfInList() { if (lists > 0 && paths == list[lists - 1]) ++path[paths - 1]; }
function isNamed() { if (paths == 0 || path[paths - 1] == ".") return ""; else return "."; }
function printUnitTitle() { if (lists <= 0 || lists > 0 && isInArray() == 0) { if (\$1 != "") printf "%s%s", isNamed(), \$1; } }
function printPath() { for (i = 0; i < paths - 1; ++i) { if (path[i] != ".") printf "%s.", path[i]; } if (paths > 0 && path[paths - 1] != ".") printf "%s", path[paths - 1]; }
\$1 ~ /#/ { next; }
\$1 ~ /{/ { pushPath(isInArray() && \$2 == "" ? "." : \$2); next; }
\$1 ~ /}/ { popPath(); incrementIfInList(); next; }
\$1 ~ /\[/ { pushPath(isInArray() && \$2 == "" ? "." : \$2); pushPath(0); pushList(paths); next; }
\$1 ~ /\]/ { popPath(); popPath(); popList(); incrementIfInList(); next; }
\$1 ~ /\\$/ { paths = 0; lists = 0; next; }
// { if (NF == 0) { if (isInArray()) { printPath(); incrementIfInList(); printf "\n";} next; } else { printPath(); printUnitTitle(); printElements((isInArray() == 0) + 1); printf "\n"; incrementIfInList(); } }
## END ACON
EOF
)
aconf() {
## BEGIN ACONF
awk -v pattern="$1" '$1 ~ pattern { for (i = 2; i <= NF; ++i) printf "%s ", $i; printf "\n"; }' | sed 's/ *$//g;s/^ *//g'
## END ACONF
}
errs "$Yellow" "Setting up server escaper\n"
errs "$Yellow" "Processing the servers\n"
servers="$(cat servers | awk "$acon")"
names="$(echo "$servers" | awk -F'.' '{ print $1; }' | uniq)"
raw_list=()
list=()
while read line; do
list+=("$(echo $line | escape)")
raw_list+=("$line")
errs "$Green" "Loaded server '$(echo $line | escape)'\n"
done < <(echo "$names")
if [ "${#list[@]}" -eq 0 ]; then
errs "$Red" "Server list is empty. Delete the servers file to let this script generate one for you. Aborting\n"
exit 1
fi
errs "$Yellow" 'Checking internal consistency\n'
if [ "${#list[@]}" -ne "${#raw_list[@]}" ]; then
errs "$Red" 'Internal Error: The escaped list and names are not of the same size.\n'
exit 1
fi
fi
#######################################################################
# 4. Call 'clean' recursively
#######################################################################
if ! onearg "$1" "--contamine" && ! onearg "$1" "--pull"; then
errs "$Yellow" "Running clean recursively...\n"
fail=0
while read -r -d $'\0' clean; do
root="$(pwd)"
errs "$Yellow" "running '$clean'\n"
if run "$clean"; then
errs "$Green" "Cleaning script '$clean' succeeded\n"
else
errs "$Red" "Cleaning script '$clean' failed\n"
: $((++fail))
fi
cd "$root"
done < <(find -name clean -perm -u=x -type f -print0)
if [ "$fail" -ge 1 ]; then
wording=('scripts' 'these')
if [ "$fail" -eq 1 ]; then
wording=('script' 'it')
fi
errs "$BIPurple" "$fail ${wording[0]} failed to return succesfully. Please check ${wording[1]} and run again.\n"
exit 1
fi
fi
#######################################################################
# 5. Get the servers that will be used (via stdin)
#######################################################################
if ! onearg "$1" "--clean" && ! onearg "$1" "--contamine"; then
errs "$Yellow" "Finding largest server name\n"
index=0
largest=0
while [ "$index" -lt "${#list[@]}" ]; do
size="${#list[$index]}"
if [ "$largest" -lt "$size" ]; then
largest="$size"
fi
: $((++index))
done
padding() {
local insize="${#1}"
local difference="$(($largest-$insize))"
v="$(printf "%-${difference}s" "")"
echo "${v// / }"
}
errs "$Yellow" "Listing servers\n"
index=0
logs "$IBlue" 'All: (blank input)\n'
while [ "$index" -lt "${#list[@]}" ]; do
spaces="$(padding "${list[$index]}")"
logs "$Blue" "${list[$index]}:$spaces '${raw_list[$index]}'\n"
: $((++index))
done
logs "" "Enter server name (separate multiple servers by spaces): "
read -a servers
fi
#######################################################################
# 6. Perform actual up/down -loading
#######################################################################
if ! onearg "$1" "--clean" && ! onearg "$1" "--contamine"; then
errs "$Yellow" "Preparing upload facilities\n"
all_but_last="$(cat <<EOF
// { for (i = 2; i <= NF; ++i) print \$i; }
EOF
)"
get_entry() {
local line="$(echo "$1" | awk '{ printf "%s.%d\n", $1, NR; }' | \
awk -F '.' -v pattern="^$2\$" '$2 ~ pattern { print $3; }')"
if [ "$line" != "" ]; then
echo "$1" | head -n "$line" | tail -n 1 | awk "$all_but_last" | tr $'\n' ' ' | sed 's/^ *//;s/ *$//'
fi
}
get_elements() {
address="$(echo "$1" | aconf '^[^\\.]+\\.address$')"
push="$(echo "$1" | aconf '^[^\\.]+\\.push$')"
pull="$(echo "$1" | aconf '^[^\\.]+\\.pull$')"
path="$(echo "$1" | aconf '^[^\\.]+\\.path$')"
from="$(echo "$1" | aconf '^[^\\.]+\\.from$')"
execute="$(echo "$1" | aconf '^[^\\.]+\\.execute$')"
execute_flags="$(echo "$1" | aconf '^[^\\.]+\\.execute_flags')"
}
upload() {
local server="$(echo "$1" | sed 's/(/\\\\(/g' | sed 's/)/\\\\)/g')"
local value="$(cat servers | awk "$acon" | awk -F'.' -v pattern="^$server\$" '$1 ~ pattern { print; }')"
if [ "$value" = "" ]; then
errs "$Red" "Server '$server' not found. Aborting\n"
exit 1
fi
get_elements "$value"
errs "$Yellow" "Starting upload from '$from' to $address:$path\n"
errs "$Yellow" "Command: 'rsync $push $from ""$address:$path'\n"
read -r -a array <<< "$push"
escape_all
rsync "${new_array[@]}" $from "$address:$path"
if [ "$execute" != "" ]; then
ssh -o StrictHostKeyChecking=no $execute_flags "$address" <<< "$execute"
fi
}
download() {
local server="$(echo "$1" | sed 's/(/\\\\(/g' | sed 's/)/\\\\)/g')"
local value="$(cat servers | awk "$acon" | awk -F'.' -v pattern="^$server\$" '$1 ~ pattern { print; }')"
if [ "$value" = "" ]; then
errs "$Red" "Server '$server' not found. Aborting\n"
exit 1
fi
get_elements "$value"
errs "$Yellow" "Starting upload from '$from' to $address:$path\n"
errs "$Yellow" "Command: 'rsync $push $from ""$address:$path'\n"
read -r -a array <<< "$pull"
escape_all
rsync "${new_array[@]}" "$address:$path" .
if [ "$execute" != "" ]; then
bash -c "$execute"
fi
}
fail=0
if ! onearg "$1" "--pull"; then
errs "$Yellow" "Uploading...\n"
if [ "${#servers[@]}" -eq 0 ]; then
for server in "${raw_list[@]}"; do
set +e
upload "$server"
if [ "$?" -ne 0 ]; then
: $((fail+=1))
fi
set -e
done
else
for server in "${servers[@]}"; do
set +e
upload "$server"
if [ "$?" -ne 0 ]; then
: $((fail+=1))
fi
set -e
done
fi
else
errs "$Yellow" "Downloading...\n"
if [ "${#servers[@]}" -eq 0 ]; then
for server in "${raw_list[@]}"; do
set +e
download "$server"
if [ "$?" -ne 0 ]; then
: $((fail+=1))
fi
set -e
done
else
for server in "${servers[@]}"; do
set +e
download "$server"
if [ "$?" -ne 0 ]; then
: $((fail+=1))
fi
set -e
done
fi
fi
if [ "$fail" -ge 1 ]; then
wording=('scripts' 'these')
if [ "$fail" -eq 1 ]; then
wording=('script' 'it')
fi
errs "$BIPurple" "$fail ${wording[0]} failed to return succesfully. Please check ${wording[1]} and run again.\n"
exit 1
else
errs "$BIGreen" "All uploads succeeded\n"
fi
fi
#######################################################################
# 7. Run 'contamine' recursively
#######################################################################
if onearg "$1" "--pull" || onearg "$1" "--contamine"; then
fail=0
while read -r -d $'\0' contamine; do
root="$(pwd)"
errs "$Yellow" "running '$contamine'\n"
if run "$contamine"; then
errs "$Green" "Contamine script '$contamine' succeeded\n"
else
errs "$Red" "Contamine script '$contamine' failed\n"
: $((++fail))
fi
cd "$root"
done < <(find -name contamine -perm -u=x -type f -print0)
if [ "$fail" -ge 1 ]; then
wording=('scripts' 'these')
if [ "$fail" -eq 1 ]; then
wording=('script' 'it')
fi
errs "$BIPurple" "$fail ${wording[0]} failed to return succesfully. Please check ${wording[1]} and run again.\n"
exit 1
fi
fi