-
Notifications
You must be signed in to change notification settings - Fork 0
/
autorsync.bash
executable file
·401 lines (369 loc) · 13 KB
/
autorsync.bash
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
#!/usr/bin/env bash
## Author Ivan Kuvaldin aka "kyb"
## Distributed under MIT License, https://opensource.org/licenses/MIT
## Project page https://gitlab.com/kyb/autorsync
set -euo pipefail
shopt -u failglob
shopt -s lastpipe huponexit #inherit_errexit
DEBUG={DEBUG:-1}
VERSION=000
NPM_VERSION=0.0.0
## KNOWN ISSUES
## * fswatch on MacOS does not detect deleted folder (CHECK)
## * may need to set max number of open file descriptors ulimit -n 1170
## * fswatch is not a good solution for linux due to Error: Event queue overflow. and long delays. Try inotify-hookable or inotifywait
## * NEED periodic full update
## * Works only with remote DST
## * It's a question: should we remove .rsync.temp?
## * Cannot trap exit via ssh to remove .rsync.temp
## * Somehow fswatch_cmd "$DST_PATH" on remote host keeps running after stop of autorsync.
## * There is a problem in Docker containers on Mac to deliver inotify events to fswatch and inotify-tools. So for Docker on Mac container use --no-rx or --tx-only
## TODO
## * --help
## * --install and --install-symlink as they are in git-rev-label
## * --initial-tx-delete-missing
## * --cleanup-temp
## * Allow more than one SRC
## * For two side sync it is required to distinguish updates made by autorsync and updates made by others. To suppress cyclic updates.
## * OPTIMIZATION if DST is ssh-url, start rsyncd on destination host and user rsync:// for often rsync requests. Will be faster because does not need to open ssh connection every time.
## * OPTIMIZATION Consider locate .rsync-temp in RAM, at least for low size files.
function darwin_aliases {
if test `uname` == Darwin ;then
alias sed=gsed
alias find=gfind
alias date=gdate
alias cp=gcp
alias mv=gmv
alias ls=gls
alias mktemp=gmktemp
alias readlink=greadlink
fi
}
darwin_aliases
## On MacOS need:
## brew install coreutils
readonly mydir="$( dirname $(readlink -f ${BASH_SOURCE[0]} ) )"
##ToDo may be use `realpath`
source $mydir/utils.bash
trap_append 'echoinfo autorsync: TERMINATED' TERM
trap_append 'echoinfo autorsync: INTERRUPTED' INT
trap_append 'echoinfo autorsync: HANG UP' HUP
trap_append 'ps=$(jobs -p); ${ps:+ kill $ps} || true;' EXIT
#trap "kill -hup 0" hup
unset SRC
unset DST
USE_INITIAL_TX=y
USE_TX=y
USE_RX=
RSYNC_PATH= ## From man rsync: --rsync-path=PROGRAM specify the rsync to run on remote machine
RSH=
SSH=ssh
readonly EXCLUDES_LIST=$(mktemp)
trap_append "echodbg 'Removing EXCLUDES_LIST'; rm -f $EXCLUDES_LIST" EXIT
## Parse options
while (( $# > 0 ))
do
case "$1" in
--help)
echowarn "Option --help is under construction!"
exit
;;
--period=*)
period="${1#--period=}"
;;
--exclude=*)
echo "${1#--exclude=}" >>$EXCLUDES_LIST
USE_DEFAULT_EXCLUDES_LIST=${USE_DEFAULT_EXCLUDES_LIST:=n} ## Set to 'n' if was not already set
;;
--exclude-from=*)
cat "${1#--exclude-from=}" >>$EXCLUDES_LIST
USE_DEFAULT_EXCLUDES_LIST=${USE_DEFAULT_EXCLUDES_LIST:=n} ## Set to 'n' if was not already set
;;
--exclude-defaults) ## Normally DEFAULT_EXCLUDES_LIST is not used when --excludes and/or --excludes-from options are set. This brings excludes together.
USE_DEFAULT_EXCLUDES_LIST=y
;;
--no-exclude-defaults) ## Overrides --exclude-defaults to disable its behavior
USE_DEFAULT_EXCLUDES_LIST=n
;;
--show-exclude-defaults|--show-default-exclude|--show-default-excludes)
USE_DEFAULT_EXCLUDES_LIST=y
SHOW_EXCLUDE_DEFAULTS=y
break #STOP_PARSE_OPTIONS
;;
--no-initial-tx|--noinitialtx|--disable-initial-tx|--noitx)
unset USE_INITIAL_TX
;;
--initial-tx|--enable-initial-tx|--use-initial-tx)
USE_INITIAL_TX=y
;;
--initial-tx-only)
USE_INITIAL_TX=y
unset USE_TX USE_RX
;;
--initial-tx-delete-missing)
echowarn "Option --initial-tx-delete-missing is under construction!"
;;
--tx|--use-tx)
USE_TX=y
;;
--rx|--use-rx)
USE_RX=y
;;
--rxtx|--txrx)
USE_TX=y
USE_RX=y
;;
--no-tx|--notx)
unset USE_TX
;;
--no-rx|--norx)
unset USE_RX
;;
--only-tx|--tx-only)
USE_TX=y
unset USE_RX
;;
--only-rx|--rx-only)
USE_RX=y
unset USE_TX
;;
--cleanup-temp)
## Remove .rsync.temp before exit
echowarn "Option --cleanup-temp is under construction!"
;;
--no-remove-rsync-temp)
## Do not .rsync.temp before exit
echowarn "Option --no-remove-rsync-temp is under construction!"
;;
--rsh=*)
RSH="${1#*=}"
SSH="${1#*=}"
;;
--rsync-path=*)
RSYNC_PATH="${1#*=}"
;;
--version)
echo "$VERSION"
exit
;;
--npm-version|--version-npm)
echo "$NPM_VERSION"
exit
;;
--install)
echowarn "Option --install is under construction! Refer to git-rev-label for impl."
;;
--install-symlink)
echowarn "Option --install-symlink is under construction! Refer to git-rev-label for impl."
;;
--install-symlink)
echowarn "Option --install-symlink is under construction! Refer to git-rev-label for impl."
;;
-x) set -x ;;
+x) set +x ;;
-*|--*) fatalerr "Unknown option: $1" ;;
*) if var_is_unset_or_empty SRC ;then
SRC+=("$1")
elif var_is_unset_or_empty DST ;then
DST="$1"
else
#SRC+=("$DST")
#DST="$1"
echowarn "SRC and DST already set. Ignoring $1"
fi
esac
shift
done
USE_DEFAULT_EXCLUDES_LIST=${USE_DEFAULT_EXCLUDES_LIST:=y} ## if unset or empty, set to default value 'y'
if test $USE_DEFAULT_EXCLUDES_LIST = y ;then
cat >>$EXCLUDES_LIST <<END
.fseventsd
.rsync.temp/
.git/FETCH_HEAD
.git/index.lock
.git/modules/*/index.lock
.DS_Store
.Spotlight-V100
.TemporaryItemss
.Trashes
build-*
*___jb_old___
*___jb_tmp___
.idea/
END
fi
if var_is_set_not_empty SHOW_EXCLUDE_DEFAULTS && test $SHOW_EXCLUDE_DEFAULTS = y ;then
cat $EXCLUDES_LIST
exit
fi
## Assert
var_is_set_not_empty SRC || fatalerr "Source is not set"
var_is_set_not_empty DST || fatalerr "Destination is not set"
## If SRC ends with slash '/' sync folder contents without the folder itself
## ToDo: consider combinations DST/SRC/file/directory
function excludes_for_fswatch
{
sed $EXCLUDES_LIST -f <(cat <<'END'
/^#/d
s#\.#\\\.#g
s#\*#.*#g
s#\?#.?#g
s#^#--exclude #g
END
)
}
## Initial sync
function initial_tx
{
echoinfo "Begin initial sync to container. Nothing will be deleted, only copy and update."
#local DST_PATH=$(ssh "$DST_HOST" bash -c "test; test -d \"$DST_PATH\" && echo \"$DST_PATH/../$(basename "$SRC")\" || echo \"$DST_PATH\"")
## If $SRC ends with / sync files in it, else sync SRC itself.
if test "${SRC: -1}" = / && test -d "$SRC" ;then
local FIND_FILES=y
#echoinfo "${FIND_FILES+ --files-from=<(find "$SRC" | sed -E 's#'"$SRC"'/?##g')}"
#find "$SRC" | sed -E 's#'"$SRC"'/?##g'
fi
#mkdir -p .rsync.temp #mktemp -d
$SSH $DST_HOST mkdir -p "$DST_PATH"/.rsync.temp #bash -xc "test -d '$DST_PATH' && mkdir -p '$DST_PATH'/.rsync.temp || mkdir -p '$DST_PATH'.rsync.temp"
if rsync --archive --info=progress2 \
--temp-dir=.rsync.temp \
--exclude-from=$EXCLUDES_LIST \
${RSYNC_PATH:+"--rsync-path=$RSYNC_PATH"} \
${RSH:+"--rsh=$RSH"} \
"$SRC" "$DST" \
#${FIND_FILES:+ --files-from=<(find "$SRC" | sed -E 's#'"$SRC"'/?##g')}
#--relative
then echoinfo "Initial sync to container done."
else echowarn "Initial sync finished with errors/warnings."
fi
}
function fswatch_cmd
{
## See https://github.com/emcrisostomo/fswatch/issues/212#issuecomment-473369919 for full list of events
EndOfTransmittion=$'\x04'
fswatch "$1" --batch-marker=$'\x04' \
${period:+ --latency $period} \
--recursive \
--event Created --event Updated --event Removed --event Renamed --event AttributeModified --event OwnerModified \
$(excludes_for_fswatch)
##--exclude '.*/index.lock' --exclude '\.idea/.*' --exclude '.*___jb_old___' --exclude '.*___jb_tmp___' \
##--exclude '\.DS_Store' --exclude '\.git/FETCH_HEAD' --exclude '\.rsync\.temp/.*'
#--event" "{Created,Updated,Removed,Renamed,AttributeModified} \
#--event IsFile --event IsDir --event IsSymLink \
#--event-flags \
# $(excludes_for_fswatch)
}
function normalize_path
{
local rp="$1"; shift
sed -e"s#$rp/##g" -e"s#^$rp\$##g" "$@" ## linux's inotify can respond with PWD folder, which we will not use
}
function rsync_cmd
{
## ToDo OPTIMIZATION if DST is ssh-url, start rsyncd on destination host and user rsync:// for often rsync requests. Will be faster because does not need to open ssh connection every time.
ff=$(mktemp) &&
trap_append "echodbg 'Clean up rsync_cmd. Removing ff $ff'; rm $ff" EXIT
while read -d $'\x04' ;do
echodbg "$(test "$src" == "$SRC" && echo "-->" || echo "<--") " #$REPLY"
echo "$REPLY" >$ff
normalize_path "$NORMALIZE_PATH" -i $ff
if rsync --archive --relative --delete --delete-missing-args \
--info=progress2 --files-from=$ff \
--temp-dir=.rsync.temp --exclude='.rsync.temp/*' \
"$src" "$dst" \
${RSYNC_PATH:+"--rsync-path=$RSYNC_PATH"} \
${RSH:+"--rsh=$RSH"} \
"$@" \
#${FIND_FILES:+ --files-from=<(find "$1" | sed -E 's#'"$1"'/?##g')}
then :
else echowarn "Failed to sync: $(cat $ff)"
fi
done
}
function rsync_tx
{
$SSH $DST_HOST mkdir -p "$DST_PATH"/.rsync.temp
trap_append "echodbg 'Cleanup rsync_tx. Removing ssh://\"$DST_PATH/.rsync.temp\"'; $SSH $DST_HOST rm -rf \"$DST_PATH/.rsync.temp\"" EXIT
NORMALIZE_PATH="$(realpath "$SRC")"
echodbg NORMALIZE_PATH $NORMALIZE_PATH
fswatch_cmd "$SRC" |
NORMALIZE_PATH="$(realpath "$SRC")" \
src="$SRC" dst="$DST" rsync_cmd
}
function rsync_rx
{
## ToDo Create rsync.temp on local side
# if test -d $SRC
# then local tempdir="$SRC/.rsync.temp"
# else local tempdir="${SRC%/*}/.rsync.temp"
# fi
# mkdir -p "$tempdir"
# trap_append "echodbg 'Cleanup rsync_tx. Removing \"$tempdir\"'; rm -rf \"$tempdir\"" EXIT
remote_command() {
set -euo pipefail
trap OnError ERR
darwin_aliases
#mkdir -p "$DST_PATH"/.rsync.temp
ulimit -n 10000
fswatch_cmd "$DST_PATH"
}
$SSH $DST_HOST ${REMOTE_PATH:+$REMOTE_PATH/}bash -<<END | NORMALIZE_PATH="$($SSH "$DST_HOST" ${REMOTE_PATH:+$REMOTE_PATH/}realpath "$DST_PATH")" src="$DST" dst="$SRC" rsync_cmd
set -euo pipefail
if test `uname` == Darwin ;then export PATH="/usr/local/bin:$PATH" ;fi
$(cat $mydir/utils.bash)
DST_PATH="$DST_PATH" period=$period DEBUG=$DEBUG
EXCLUDES_LIST=\$(mktemp)
echo "$(cat $EXCLUDES_LIST)" >\$EXCLUDES_LIST
$(declare -f)
remote_command
END
unset -f remote_command
}
DST_HOST="${DST%:*}" #$(cut -d: -f1 <<<$DST)
DST_PATH="${DST#*:}" #$(cut -d: -f2 <<<$DST)
REMOTE_UNAME="$($SSH $DST_HOST uname)"
var_is_unset_or_empty REMOTE_PATH && test $REMOTE_UNAME = Darwin && REMOTE_PATH=/usr/local/bin
var_is_unset_or_empty RSYNC_PATH && test $REMOTE_UNAME = Darwin && RSYNC_PATH="${REMOTE_PATH:+$REMOTE_PATH/}rsync"
if var_is_set_not_empty USE_TX && var_is_set_not_empty USE_RX
then fatalerr "Two side sync is not stable!" ;fi
#test $(uname -s) == Darwin && period=0.5 || period=1
if var_is_set USE_INITIAL_TX ;then
echoinfo "Begin initial_tx"
initial_tx
echoinfo "Done initial_tx"
fi
if ! test "${SRC: -1}" = / || ! test -d "$SRC"
then DST+="/`basename "$SRC"`/"
DST_PATH="${DST#*:}"
fi
if var_is_set_not_empty USE_TX ;then
echoinfo "Starting rsync_tx"
rsync_tx & pid_tx=$!
sleep 0.2 && kill -0 $pid_tx 2>&- || fatalerr "rsync_tx $pid_tx failed to start" ## Check process is running successfully
echoinfo "pid_rsync_tx $pid_tx"
#trap_append "kill $pid_tx" EXIT
fi
if var_is_set_not_empty USE_RX ;then
echoinfo "Starting rsync_rx"
rsync_rx & pid_rx=$!
sleep 0.2 && kill -0 $pid_rx 2>&- || fatalerr "rsync_rx $pid_rx failed to start" ## Check process is running successfully
echoinfo "pid_rsync_rx $pid_rx"
#trap_append "kill $pid_rx" EXIT
fi
## Fix problem: subprocesses do not exit with main
#echodbg "============ jobs -l =============="
#debug jobs -l
#echodbg "============ pstree \$\$ =============="
#debug pstree $$
wait || true
echodbg "---------------------------------------------"
echoinfo "autorsync done."
### Wait for jobs exit, if interrupted try normally close them, if they fail to close during 2 seconds, force kill.
#wait || kill $(jobs -p)
#sleep 0.3
#JOBS="$(jobs -p)"
#if test "$JOBS" ;then
# sleep 1.7
# JOBS="$(jobs -p)"
# test "$JOBS" && kill -9 "$JOBS" ## Force kill if did not clone
#fi