Created
March 10, 2019 20:35
-
-
Save sneppy/4eda6111d3825f346d5b4b1d3f2472b4 to your computer and use it in GitHub Desktop.
An extension of the bitpocket by sickill -> https://github.com/sickill/bitpocket
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
#!/bin/bash | |
LANG=$(locale | grep LANG= | sed 's:LANG=::') | |
if [[ -z "$LANG" ]]; then | |
LANG="C" | |
fi | |
export LC_ALL=$LANG # for stable "sort" output | |
# 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 sed --version >/dev/null 2>/dev/null; then | |
alias cp="cp --parents --reflink=auto" | |
else | |
echo "\ | |
Warning: --------------------------------------------------- | |
It seems like you are running on a system without GNU coreutils. bitpocket | |
may not work correctly on this platform. Please beware and report any issues | |
you encounter. | |
" | |
alias sed="sed -E" | |
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)" | |
# Temp files used, deleted at the end of the script | |
tmp_bitignore_files="" | |
# Don't sync user ignored files | |
shopt -s globstar | |
for bitignore in **/.bitignore; do | |
# Check that file actually exists | |
if [[ -f $bitignore ]]; then | |
# Prepend relative dir | |
tmp_bitignore=$(mktemp --tmpdir=$TMP_DIR) | |
rel_dir=$(dirname $bitignore) | |
if [ $rel_dir == "." ] | |
# Prepending current folder '.' causes problem | |
# thus in this case, don't prepend anything | |
# just copy | |
then cp $bitignore $tmp_bitignore | |
else sed -e "s:^:$rel_dir/:" $bitignore > $tmp_bitignore | |
fi | |
# Add to list of temp bitignore files | |
tmp_bitignore_files="$tmp_bitignore_files $tmp_bitignore" | |
# Add to exclude file list | |
user_exclude="$user_exclude --exclude-from $tmp_bitignore" | |
fi | |
done | |
shopt -u globstar | |
USER_RULES="$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 | |
mkdir "$DOT_DIR" | |
cat <<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 $(pwd)" | |
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 | |
cp "$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" \ | |
| 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" \ | |
| 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. | |
sed -En '/^[dl-]/ { | |
s:^([^[:space:]]*)[[:space:]]*[^[:space:]]*[[:space:]]*[^[:space:]]*[[:space:]]*[^[:space:]]*[[:space:]]*(.*$):\:\1/\2: | |
/\/\.$/ b | |
s:([*?[]):\\\1:g | |
p | |
}' | |
} | |
function analyse { | |
# Check what has changed | |
touch "$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: $(pwd)" | |
# 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" | |
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 | |
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 | |
cat "$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 | |
cp "$STATE_DIR/tree-current" "$TMP_DIR/local-add-change" | |
touch "$TMP_DIR/local-del" | |
touch "$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 | |
sed -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 | |
pull | |
push | |
if [[ ! "${OPTIONS[*]}" =~ pretend ]]; 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 \ | |
| sed -e "s:/\$::" \ | |
> "$STATE_DIR/tree-prev" | |
fi | |
rm "$TMP_DIR/tree-after" | |
# 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 | |
mkdir $DOT_DIR/pack | |
git init $DOT_DIR/pack | |
touch $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" ] && [ "$(ls -A $DOT_DIR/backups)" ] | |
then | |
for DIR in $DOT_DIR/backups/* | |
do | |
TSTAMP=$(echo $DIR | sed "s|.*/||") | |
if [ "$(ls -A $DIR)" ] | |
then | |
echo -n "Processing: $TSTAMP ... " | |
echo -n "Moving ... " | |
(cp -rfl $DIR/* $DOT_DIR/pack && rm -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 | |
rm -rf "$SLOW_SYNC_FILE" | |
(sleep $SLOW_SYNC_TIME && touch "$SLOW_SYNC_FILE" && eval "$SLOW_SYNC_START_CMD" ; wait) & | |
disown | |
shell_pid=$! | |
fi | |
} | |
function on_slow_sync_stop { | |
if [ -n "$shell_pid" ]; then | |
kill $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 ! mkdir "$LOCK_DIR" 2>/dev/null | |
then | |
if kill -0 $(cat "$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}" | |
rm "$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: $(pwd)" | |
echo " | Lock dir: $LOCK_DIR" | |
echo " | Command: LOCK_PATH=$(pwd)/$LOCK_DIR && rm \$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 { | |
rm "$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 kill -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 | |
mkdir -p "$TMP_DIR" | |
mkdir -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 "$(pwd -P)" >/dev/null); do cd .. ; done && pwd -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 used 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 | |
kill $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:" | |
rsync -av --list-only --exclude "/$DOT_DIR" $USER_RULES . \ | |
| scrub_rsync_list \ | |
| strip_mode \ | |
| sort | |
} | |
function usage { | |
cat <<EOF | |
usage: bitpocket { init [<REMOTE_HOST>] <REMOTE_PATH> | |
| sync | help | pack | log | cron | list } | |
Available commands: | |
sync Run the sync process. If no command is specified, sync is run by | |
default. | |
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|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;; | |
sync) sync;; | |
esac | |
# Remove temp bitignore files | |
if [[ -n $tmp_bitignore_files ]]; then | |
rm $tmp_bitignore_files | |
fi |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment