aboutsummaryrefslogtreecommitdiffstats
path: root/bitpocket_mac
blob: 36be0396af64abfd30cb5ffc27c6d82c75839af8 (plain) (blame)
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
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
#!/usr/local/bin/bash
# Mac version 3. Sic.

LANG=$(locale | grep LANG= | gsed 's:LANG=::')
if [[ -z "$LANG" ]]; then
    LANG="C"
fi

#export LC_ALL=$LANG # for stable "sort" output
export LC_ALL="C"

# Paths
DOT_DIR=.bitpocket
CFG_FILE="$DOT_DIR/config"
TMP_DIR="$DOT_DIR/tmp"
STATE_DIR="$DOT_DIR/state"
LOCK_DIR="$TMP_DIR/lock"  # Use a lock directory for atomic locks. See the Bash FAQ http://mywiki.wooledge.org/BashFAQ/045

# Default settings
SLOW_SYNC_TIME=10
SLOW_SYNC_FILE="$TMP_DIR/slow"
RSYNC_RSH="ssh"
REMOTE_BACKUPS=false
BACKUPS=true
LOCAL_MOUNTPOINT=false
REMOTE_MOUNTPOINT=false

# Default command-line options and such
COMMANDS=()
ARGS=()
OPTIONS=()

# Load config file
[[ -f "$CFG_FILE" ]] && . "$CFG_FILE"

# Colors
GREEN=""
RED=""
CLEAR=""
YELLOW=""
if [[ -t 1 ]]; then
    GREEN="\x1b\x5b1;32m"
    RED="\x1b\x5b1;31m"
    YELLOW="\x1b\x5b1;33m"
    CLEAR="\x1b\x5b0m"
fi

# Test for GNU versions of core utils. Bail if non-GNU.
if gsed --version >/dev/null 2>/dev/null; then
    alias gcp="gcp --parents --reflink=auto"
else
    echo "\
Error: ---------------------------------------------------
It seems like you are running on a system without GNU coreutils.
bitpocket will not work correctly on MacOS without these.
Please install coreutils and gnu-sed from Homebrew. Exiting.
"
    exit 1
fi

# Decide on runner (ssh / bash -c)
function setup_remote() {
    if [[ -n "$REMOTE_HOST" ]]; then
      REMOTE_RUNNER="$RSYNC_RSH $REMOTE_HOST"
      REMOTE="$REMOTE_HOST:$REMOTE_PATH"
    else
      REMOTE_RUNNER="bash -c"
      REMOTE="$REMOTE_PATH"
    fi
}
setup_remote

# Version of the state files. Original version is 1, second version with
# leading mode is 2.
#
# Current state version is 2, and the format of the state file is
# ":" TYPE MODE "/" PATH "\n"
# Where TYPE = ("-" | "d" | "l" )
#       MODE = unix mode (9-character) (like "rw-rw-r--")
#       PATH = relative full-path of file
STATE_VERSION=2

REMOTE_TMP_DIR="$REMOTE_PATH/$DOT_DIR/tmp"
HOSTNAME="$(hostname)"

# Don't sync user excluded files
if [[ -f "$DOT_DIR/exclude" ]]; then
  user_exclude="--exclude-from $DOT_DIR/exclude"
fi

# Specify certain files to include
if [[ -f "$DOT_DIR/include" ]]; then
  user_include="--include-from $DOT_DIR/include"
fi

# Specify rsync filter rules
if [[ -f "$DOT_DIR/filter" ]]; then
  # The underscore (_) is required for correct operation
  user_filter="--filter merge_$DOT_DIR/filter"
fi

USER_RULES="$user_filter $user_include $user_exclude"

TIMESTAMP=$(date "+%Y-%m-%d.%H%M%S")

export RSYNC_RSH

function prefix() {
    while read -r line; do
        echo "$1$line"
    done
}

function init {
  if [[ -d "$DOT_DIR" || -f "$CFG_FILE" ]]; then
    echo "fatal: Current directory already initialized for bitpocket"
    exit 128
  fi

  if [[ $# == 2 ]]; then
    REMOTE_HOST=$1
    shift
  fi

  REMOTE_PATH="$1"
  setup_remote
  if $REMOTE_RUNNER "[ ! -d '$1' ]"; then
    echo "fatal: '$REMOTE': Remote path is not accessible"
    exit 128
  fi

  gmkdir "$DOT_DIR"

  gcat <<EOF > "$CFG_FILE"
## Host and path of central storage
REMOTE_HOST=$REMOTE_HOST
REMOTE_PATH="$REMOTE_PATH"

## Backups -----------------------------------
## Enable file revisioning locally in the pull phase (>false< to disable)
BACKUPS=true
## Make revisions of files on the REMOTE_HOST in the push phase.
REMOTE_BACKUPS=false

## rsync Advanced Options --------------------
## SSH command with options for connecting to \$REMOTE
# RSYNC_RSH="ssh -p 22 -i $DOT_DIR/id_rsa"

## Uncomment following line to follow symlinks (transform it into referent file/dir)
# RSYNC_OPTS="-L"

## Use the following if a remote FAT or VFAT filesystem is being synchronized.
## This is automatically detected for local FAT filesystems.
# RSYNC_OPTS="--no-perms --no-owner --no-group --modify-window=2"

## Uncomment following lines to get sync notifications
# SLOW_SYNC_TIME=10
# SLOW_SYNC_START_CMD="notify-send 'BitPocket sync in progress...'"
# SLOW_SYNC_STOP_CMD="notify-send 'BitPocket sync finished'"

## Indicate a remote mount point. If this is set and the mountpoint is not
## mounted, then the bitpocket sync will abort. This addresses situations where
## a sync target appears empty because it is not mounted.  Such a sync might
## result in all local or remote data disappearing. Give the expected
## mountpoint of the local and/or remote target.
# REMOTE_MOUNTPOINT=/
EOF

  echo "Initialized bitpocket directory at $(gpwd)"
  echo "Please have a look at the config file ($DOT_DIR/config)"
}

function log {
  assert_dotdir
  tail -f "$DOT_DIR/log"
}

function prefix() {
    while read -r line
    do
        echo "$1$line"
    done
}

function pull() {
  # Actual fetch
  # Pulling changes from server
  # Order of includes/excludes/filters is EXTREMELY important
  echo
  echo "# Pulling changes from server"

  local BACKUP_TARGET="$DOT_DIR/backups/$TIMESTAMP"
  local DO_BACKUP=""

  if [[ $BACKUPS == true ]]
  then
      echo "# >> Saving current state and backing up files (if needed)"
      local DO_BACKUP="--backup --backup-dir=$BACKUP_TARGET"
  fi

  gcp "$STATE_DIR/tree-current" "$TMP_DIR/tree-after"

  # Determine what will be fetched from server and make backup copies of any
  # local files to be deleted or overwritten.
  #
  # Only delete locally if deleted remotely. To do this, use the remote-del
  # file to set the *R*isk filter flag (allow delete), and protect everything
  # else with the *P*rotect flag.
  #
  # Order of includes/excludes/filters is EXTREMELY important
  #
  # TODO: Consider adding %U and %G to the output format to capture owner and
  # group changes
  prefix "R " < "$TMP_DIR/remote-del" \
  | /usr/local/bin/rsync -auzx --delete --exclude "/$DOT_DIR" \
        --exclude-from="$TMP_DIR/local-del" \
        --exclude-from="$TMP_DIR/local-add-change" \
        --filter=". -" \
        --filter="P **" \
        $DO_BACKUP \
        --out-format=%i:%B:%n \
        $RSYNC_OPTS $USER_RULES "$REMOTE"/ . \
  | detect_changes \
  | prefix "  | " || die "PULL"

  # Some versions of rsync will create the backup dir, even if it doesn't get
  # populated with any backups
  if [[ -d "$BACKUP_TARGET" ]]
  then
      if (shopt -s nullglob dotglob; f=("$BACKUP_TARGET"/*); ((${#f[@]})))
      then
          echo "  | Some files were backed up to $BACKUP_TARGET"
      else
          rmdir "$BACKUP_TARGET"
      fi
  fi
}

function detect_changes() {
    # Create a duplicate of STDOUT for logging of backed-up files, and use fd#4
    # for logging of deleted files, which need to be sorted
    exec 3> >(grep -Ff /dev/stdin "$STATE_DIR/tree-current" | sort > "$TMP_DIR/pull-delete")

    while read -r line
    do
        IFS=":" read -ra info <<< "$line"
        operation=${info[0]}
        filename="${info[*]:2}"
        if [[ "$operation" =~ ^\*deleting ]]
        then
            echo "/${filename}" >&3
        elif [[ "$operation" =~ \+\+\+\+$ ]]
        then
            # Mark as added locally (with proper mode)
            mode=${info[1]}
            filetype="${operation:1:1}"
            filetype="${filetype/f/-}"
            echo ":${filetype}${mode}/$filename" >> "$TMP_DIR/tree-after"
        fi
        echo "$operation $filename"
    done

    exec 3>&-
}

function push() {
  # Actual push

  # Send new and updated, remotely remove files deleted locally
  # Order of includes/excludes/filters is EXTREMELY important
  echo
  echo "# Pushing changes to server"

  local BACKUP_TARGET="$DOT_DIR/backups/$TIMESTAMP"
  local DO_BACKUP=""
  if [[ $REMOTE_BACKUPS == true ]]
  then
      echo "# >> Saving current state and backing up files (if needed)"
      DO_BACKUP="--backup --backup-dir=$BACKUP_TARGET"
  fi

  # Do not push back remotely deleted files
  prefix "R " < "$TMP_DIR/local-del" \
  | /usr/local/bin/rsync -auzxi --delete $RSYNC_OPTS --exclude "/$DOT_DIR" \
        --exclude-from="$TMP_DIR/remote-del" \
        --filter=". -" \
        --filter="P **" \
        $DO_BACKUP \
        $USER_RULES . "$REMOTE"/ \
  | prefix "  | " || die "PUSH"

  # Some versions of rsync will create the backup dir, even if it doesn't get
  # populated with any backups
  if [[ $REMOTE_BACKUPS == true ]]
  then
      $REMOTE_RUNNER "
      cd '$REMOTE_PATH'
      if [[ -d '$BACKUP_TARGET' ]]
      then
          if (shopt -s nullglob dotglob; f=('$BACKUP_TARGET'/*); ((\${#f[@]})))
          then
              echo '  | Some files were backed up to $BACKUP_TARGET'
          else
              rmdir '$BACKUP_TARGET'
          fi
      fi
      "
  fi
}

function scrub_rsync_list {
    # Capture the 1st and 5th columns (mode and file name), remove blank lines,
    # drop the `/.` folder/file, and escape files with `[*?` characters in
    # them. Use the ASCII "file separator" (0x1c) to separate the filename from
    # the file mode in the output.
    gsed -En '/^[dl-]/ {
        s:^([^[:space:]]*)[[:space:]]*[^[:space:]]*[[:space:]]*[^[:space:]]*[[:space:]]*[^[:space:]]*[[:space:]]*(.*$):\:\1/\2:
        /\/\.$/ b
        s:([*?[]):\\\1:g
        p
    }'
}

function analyse {
  # Check what has changed
  gtouch "$STATE_DIR/tree-prev"

  # Save before-sync state
  # Must be done with rsync itself (rather than find) to respect includes/excludes
  # Order of includes/excludes/filters is EXTREMELY important
  echo "# Capturing current local and remote state"
  echo "  | Root dir: $(gpwd)"

  # Collect the current snapshot of the remote tree, if a previous tree
  # snapshot is available locally
  if [[ -s "$STATE_DIR/tree-prev" ]]; then
    echo "  | Root dir: $REMOTE"
    /usr/local/bin/rsync --list-only --recursive --exclude "/$DOT_DIR" $USER_RULES "$REMOTE"/ \
      | scrub_rsync_list \
      | sort -k 1.12 \
      > "$STATE_DIR/remote-tree-current" &
    local remote_tree_pid=$!
  fi

  # Collect the current snapshot of the local tree
  /usr/local/bin/rsync --list-only --recursive --exclude "/$DOT_DIR" $USER_RULES . \
      | scrub_rsync_list \
      | sort -k 1.12 \
      > "$STATE_DIR/tree-current" \
      || die "SNAPSHOT"

  # Prevent bringing back locally deleted files
  if [[ -s "$STATE_DIR/tree-prev" ]]
  then
      # Compile a list of files added locally and removed locally. These
      # should be protected in the pull phase. Escape rsync filter wildcard
      # characters, remove blank lines
      strip_mode < "$STATE_DIR/tree-prev" \
      | comm -23 - <(strip_mode < "$STATE_DIR/tree-current") \
      > "$TMP_DIR/local-del"

      # Honor local mode changes (link to file, as well as permissions).
      # These should be protected in the pull phase. Ignore files already
      # masked as locally added or locally removed.
      if [[ $STATE_VERSION -gt 1 ]]
      then
          # Use comm to detect the differences in the pseudo-sorted files
          # (they're sorted on column 12). Comm will detect some false
          # positives, so run the output back through `grep` to check if there
          # is an exact match in the previous state file. This is much faster
          # than resorting both of the files if they are somewhat large.
          comm -23 "$STATE_DIR/tree-current" "$STATE_DIR/tree-prev" 2>/dev/null \
          | while read -r line
            do
                if ! grep -Fxq "$line" "$STATE_DIR/tree-prev"
                then
                    echo "$line"
                fi
            done \
          | strip_mode \
          > "$TMP_DIR/local-add-change"
      else
          # In transition to the new state file, ignore the changes in mode
          comm -23 <(strip_mode < "$STATE_DIR/tree-current") "$STATE_DIR/tree-prev" \
          > "$TMP_DIR/local-add-change"
      fi

      # Also protect the folders where files were locally added and removed so
      # that the modify times of them are not reverted to the remote ones
      gcat "$TMP_DIR/local-del" "$TMP_DIR/local-add-change" \
      | while read -r line; do
          # Only parent folders of files--not folders
          if [[ "${line: -1}" != "/" ]]
          then
              echo "${line%/*}/"
          fi
        done \
      | sort -u \
      >> "$TMP_DIR/local-add-change"

      # Prevent deleting local files which were not deleted remotely ie.
      # prevent deleting newly added local files. Compile a list of remotely
      # deleted files which should be protected in the push phase.
      wait $remote_tree_pid
      strip_mode < "$STATE_DIR/tree-prev" \
      | comm -23 - <(strip_mode < "$STATE_DIR/remote-tree-current") \
      > "$TMP_DIR/remote-del"
  else
      # In the case of a new sync, where no previous tree snapshot is available,
      # assume all the files on the local side should be protected
      gcp "$STATE_DIR/tree-current" "$TMP_DIR/local-add-change"
      gtouch "$TMP_DIR/local-del"
      gtouch "$TMP_DIR/remote-del"
  fi
}

function strip_mode {
    if [[ $STATE_VERSION -gt 1 ]]; then
        cut -c12-
    else
        # State file version might be intermixed. Evaluate each line
        gsed -E "s/^:.{10}//"
    fi
}

# Do the actual synchronization
function sync {
  assert_dotdir
  assert_mountpoints
  acquire_lock
  acquire_remote_lock
  check_state_version
  detect_fatfs

  echo
  echo -e "${GREEN}bitpocket started${CLEAR} at $(date)."
  echo

  # Fire off slow sync start notifier in background
  on_slow_sync_start

  # Build addtion/deletion lists
  analyse

  if [[ "${OPTIONS[*]}" =~ pretend ]]; then
      RSYNC_OPTS="${RSYNC_OPTS} --dry-run"
      echo -e "${YELLOW}Pretending to sync only. No changes will be made${CLEAR}"
  fi

  if [[ "$1" != "onlypush" ]] ; then
      pull
  fi

  if [[ "$1" != "onlypull" ]] ; then
      push
  fi

  if [[ ! "${OPTIONS[*]}" =~ pretend && "$1" != "onlypush" ]]; then
      # Save after-sync state

      # Generate a incremental snapshot of the local tree including files deleted
      # and added via the pull()
      #
      # Remove pull-deleted files from the tree-after snapshot
      sort -k1.12 "$TMP_DIR/tree-after" \
      | comm -23 - "$TMP_DIR/pull-delete" 2> /dev/null \
      | gsed -e "s:/\$::" \
      > "$STATE_DIR/tree-prev"
  fi
  grm "$TMP_DIR/tree-after" 2> /dev/null

  # Fire off slow sync stop notifier in background
  on_slow_sync_stop

  cleanup
  echo
  echo -e "${GREEN}bitpocket finished${CLEAR} at $(date)."
  echo

}

# Pack backups into a git repository
function pack {
    assert_dotdir

    # Git is required for backup packing
    if ! builtin type -p git > /dev/null; then
        echo "fatal: For backup packing, git must be installed"
        exit 128
    fi

    # If pack directory is missing, create it and prepare git repo
    if [ ! -d "$DOT_DIR/pack" ]
    then
        gmkdir    $DOT_DIR/pack
        git init $DOT_DIR/pack
        gtouch    $DOT_DIR/pack/.git-init-marker
        (cd $DOT_DIR/pack && git add .)
        (cd $DOT_DIR/pack && git commit -a -q -m "INIT")
    fi

    # If any backups exist, pack them into the repo
    if [ -d "$DOT_DIR/backups" ] && [ "$(gls -A $DOT_DIR/backups)" ]
    then
        for DIR in $DOT_DIR/backups/*
        do
            TSTAMP=$(echo $DIR | gsed "s|.*/||")
            if [ "$(gls -A $DIR)" ]
            then
                echo -n "Processing: $TSTAMP ... "
                echo -n "Moving ... "
                (gcp -rfl $DIR/* $DOT_DIR/pack && grm -rf $DIR)        || die MV
                echo -n "Adding ... "
                (cd $DOT_DIR/pack && git add .)                      || die ADD
                echo -n "Committing ... "
                # Commit only if repository has uncommitted changes
                (cd $DOT_DIR/pack \
                    && git diff-index --quiet HEAD \
                    || git commit -a -q -m "$TSTAMP" )             || die COMMIT
                echo "Done."
            else
                echo "Removing empty dir $DIR  ..."
                rmdir $DIR
            fi
        done
        echo "Running 'git gc' on pack dir"
        du -hs $DOT_DIR/pack
        (cd $DOT_DIR/pack && git gc) || die GC
        du -hs $DOT_DIR/pack
        echo "All snapshots packed successfully."
    else
        echo "No unpacked backups found ..."
    fi

}

function on_slow_sync_start {
  if [ -n "$SLOW_SYNC_START_CMD" ]; then
    grm -rf "$SLOW_SYNC_FILE"
    (sleep $SLOW_SYNC_TIME && gtouch "$SLOW_SYNC_FILE" && eval "$SLOW_SYNC_START_CMD" ; wait) &
    disown
    shell_pid=$!
  fi
}

function on_slow_sync_stop {
  if [ -n "$shell_pid" ]; then
    gkill $shell_pid &>/dev/null

    if [[ -n "$SLOW_SYNC_STOP_CMD" && -f "$SLOW_SYNC_FILE" ]]; then
      (eval "$SLOW_SYNC_STOP_CMD") &
    fi
  fi
}

function cron {
  DISPLAY=:0.0 sync 2>&1 | timestamp >>"$DOT_DIR/log"
}

function timestamp {
  while read -r data
  do
      echo "[$(date +"%D %T")] $data"
  done
}

function acquire_lock {
  if ! gmkdir "$LOCK_DIR" 2>/dev/null
  then
    if gkill -0 $(gcat "$LOCK_DIR/pid") &>/dev/null
    then
      echo "There's already an instance of BitPocket syncing this directory. Exiting."
      exit 1
    else
      if [[ "${OPTIONS[*]}" =~ force ]]
      then
        echo -e "${YELLOW}Removing stale, local lock file${CLEAR}"
        grm "$LOCK_DIR/pid" && rmdir "$LOCK_DIR" && acquire_lock && return 0
      fi

      echo -e "${RED}bitpocket error:${CLEAR} Bitpocket found a stale lock directory:"
      echo "  | Root dir: $(gpwd)"
      echo "  | Lock dir: $LOCK_DIR"
      echo "  | Command:  LOCK_PATH=$(gpwd)/$LOCK_DIR && grm \$LOCK_PATH/pid && rmdir \$LOCK_PATH"
      echo "Please remove the lock directory and try again."
      exit 2
    fi
  fi

  echo $$ > "$LOCK_DIR/pid"
}

function release_lock {
  grm "$LOCK_DIR/pid" &>/dev/null && rmdir "$LOCK_DIR" &>/dev/null
}

function acquire_remote_lock {
    # TODO: Place the local hostname and this PID in a file, which will make
    # automatic lock file cleanup possible. It will also offer better output if
    # another host is truly syncing with the remote host.
    local INFO="$HOSTNAME:$$:$TIMESTAMP"
    local REMOTE_INFO=$($REMOTE_RUNNER "
        mkdir -p '$REMOTE_TMP_DIR' && cd '$REMOTE_PATH'
        [[ -d '$LOCK_DIR' ]] || mkdir '$LOCK_DIR'
        [[ -e '$LOCK_DIR'/remote ]] || echo '$INFO' > '$LOCK_DIR'/remote
        cat '$LOCK_DIR'/remote")

    [[ "$INFO" == "$REMOTE_INFO" ]] && return 0

    IFS=":" read -ra INFO <<< "$REMOTE_INFO"

    # From here down, assume the lock could not be acquired
    local code=3
    if [[ -z $REMOTE_INFO ]]
    then
        echo "Couldn't acquire remote lock or lock file couldn't be created. Exiting."
    elif [[ "$HOSTNAME" != "${INFO[0]}" ]]
    then
        echo -e "${YELLOW}Another client is syncing with '$REMOTE'${CLEAR}"
        echo ">> Host:    ${INFO[0]}"
        echo ">> PID:     ${INFO[1]}"
        echo ">> Started: ${INFO[2]}"
    elif [[ "$$" != "${INFO[1]}" ]]
    then
        # This host is syncing with the remote host. Check if the PID is still running
        if gkill -0 "${INFO[1]}" &>/dev/null
        then
            # XXX: This should be handled in the `acquire_lock` function
            echo "Another instance of Bitpocket is currently syncing this" \
                 "host with '$REMOTE'"
            code=1
        else
            # In this case, this host is holding the lock with the remote server
            # but the sync is no longer running. It is perhaps possible to remove
            # the lock?
            if [[ "${OPTIONS[*]}" =~ force ]]
            then
                echo -e "${YELLOW}Removing stale, remote lock file${CLEAR}"
                $REMOTE_RUNNER "cd '$REMOTE_PATH' && rm '$LOCK_DIR/remote' && rmdir '$LOCK_DIR'"
                # Try again
                acquire_remote_lock && return 0
            fi

            echo "The remote lock is held by this host and is stale." \
                 "It should be removed, and the sync should be retried."
            code=6
        fi
    fi

    release_lock
    exit $code
}

function release_remote_lock {
  $REMOTE_RUNNER "cd \"$REMOTE_PATH\" && grep -q '$HOSTNAME:$$' '$LOCK_DIR/remote' && rm '$LOCK_DIR/remote' && rmdir '$LOCK_DIR' &>/dev/null"
}

function assert_dotdir {
  if [ ! -d "$DOT_DIR" ]; then
    echo "fatal: Not a bitpocket directory. Try 'bitpocket help' for usage."
    exit 128
  fi
  gmkdir -p "$TMP_DIR"
  gmkdir -p "$STATE_DIR"
}

function detect_fatfs {
    # Find the local mountpoint
    if [[ $LOCAL_MOUNTPOINT == false ]]
    then
        if builtin type -p findmnt &> /dev/null
        then
            LOCAL_MOUNTPOINT=$(until findmnt . >/dev/null; do cd .. ; done && findmnt -no TARGET .)
        else
            LOCAL_MOUNTPOINT=$(until $(mount | grep -Ew "$(gpwd -P)" >/dev/null); do cd .. ; done && gpwd -P)
        fi
    fi

    # Detect local mount is FAT and add appropriate
    local fsinfo=($(mount | grep -Ew "${LOCAL_MOUNTPOINT}"))
    local fstype=${fsinfo[4]}

    if [[ $fstype == *fat ]]
    then
        RSYNC_OPTS="${RSYNC_OPTS} --no-perms --no-owner --no-group --modify-window=2"
    fi

    # TODO: Consider remote filesystem type?
}

function assert_mountpoints {
    if [[ ${REMOTE_MOUNTPOINT} != false ]]
    then
        # Sanity check -- ensure mountpoint is a parent of local target
        if [[ "${REMOTE_PATH:0:${#REMOTE_MOUNTPOINT}}" != "${REMOTE_MOUNTPOINT}" ]]
        then
            echo -e "${YELLOW}warning: Remote mount point is not a parent of '${REMOTE_PATH}'${CLEAR}"
        fi

        $REMOTE_RUNNER "mount | grep -E '\s${REMOTE_MOUNTPOINT}\s'" &> /dev/null
        if [[ $? != 0 ]]
        then
            echo -e "${RED}fatal: Remote sync target is not mounted${CLEAR}"
            exit 4
        fi
    fi
}

function cleanup {
  release_lock
  release_remote_lock
}

##
# Inspect the state file tree-prev to see if the format of the file is the
# current version (2) or the original version (1). This is sed in the
# `strip_mode` function to optimize the sync process when the `tree-prev` file
# uses the current state version.
function check_state_version() {
    # In the original state files, the start of the line was the filename with
    # a leading slash
    if [[ -s "$STATE_DIR/tree-prev" ]]; then
        local first=$(head -1 "$STATE_DIR/tree-prev" 2>/dev/null)
        if [[ ${first:0:1} == "/" ]]; then
            STATE_VERSION=1
        fi
    fi
}

function bring_the_children_let_me_kill_them {
  if [ -n "$shell_pid" ]; then
    pkill -P $shell_pid &>/dev/null
    gkill $shell_pid &>/dev/null
  fi
}

function die {
  cleanup
  bring_the_children_let_me_kill_them

  echo -e "${RED}fatal${CLEAR}: command failed $1"
  exit 128
}

function intr_cleanup {
  die "${YELLOW}Interrupted${CLEAR}"
}

trap intr_cleanup SIGINT

# List all files in the sync set
function list {
  echo -e "${GREEN}bitpocket${CLEAR} will sync the following files:"
  /usr/local/bin/rsync -av --list-only --exclude "/$DOT_DIR"  $USER_RULES . \
      | scrub_rsync_list \
      | strip_mode \
      | sort
}

function usage {
  gcat <<EOF  
usage:  bitpocket { init [<REMOTE_HOST>] <REMOTE_PATH>
                  | sync | push | pull | help | pack | log | cron | list }

Available commands:
   sync    Run the sync process. If no command is specified, sync is run by
           default.
   push    Only push new files to the server.
   pull    Only pull new files from the server.
   init    Initialize a new bitpocket folder. Requires path and optional
           remote host params. Remote path must already exist.
   pack    Pack any existing (automatic) backups into a git repository.
   cron    Run sync optimized for cron, logging output to file instead of
           stdout.
   log     Display the log generated by the cron command
   list    List all files in the sync set (honoring include/exclude/filter
           config).
   help    Show this message.

Options:
   -f, --force      Clean up stale lock files automatically
   -p, --pretend    Don't really perform the sync or update the current
                    state. Instead, show what would be synchronized.

Note: All commands (apart from help), must be run in the root of a
      new or existing bitpocket directory structure.
EOF
}

function parseargs() {
    while [[ -n $1 ]]; do
        case $1 in
            # Switches and configuration
            -p|--pretend)   OPTIONS+=('pretend');;
            -f|--force)     OPTIONS+=('force');;
            -h|--help|-*)   COMMANDS+=('help');;
            # Arguments (commands)
            init)           if [[ $# -lt 2 ]]; then
                                echo "usage: bitpocket init [<REMOTE_HOST>] <REMOTE_PATH>"
                                exit 128
                            fi
                            COMMANDS+=("$1")
                            ARGS+=("$2")
                            if [[ $# -gt 2 ]]; then
                                ARGS+=("$3")
                                shift;
                            fi
                            shift;;
            sync|push|pull|pack|cron|log|list|help)
                            COMMANDS+=("$1");;
            # Anything else
            *)              echo "!!! Invalid command: $1";;
        esac
        shift
    done
}

parseargs "$@"

# By default, run the sync process
[[ ${#COMMANDS} == 0 ]] && COMMANDS+=('sync')

# For now, only one command really makes sense
case ${COMMANDS[0]} in
    init)       init "${ARGS[@]}";;
    pack)       pack;;
    log)        log;;
    cron)       cron;;
    list)       list;;
    help)       usage;;
    push)       sync onlypush;;
    pull)       sync onlypull;;
    sync)       sync;;
esac

© 2014-2022 Faster IT GmbH | imprint | privacy policy