2022-03-13 15:01:09 +01:00
#!/bin/bash
# Copyright © 2022 Matthias Quintern.
2022-03-11 21:07:56 +01:00
# This software comes with no warranty.
2022-03-12 01:01:38 +01:00
# This software is licensed under the GPL3
2022-03-11 21:07:56 +01:00
# ABOUT
# Synchronizes files from a sender to a receiver using rsync
# The files can be encrypted before or decrypted after the transfer using the mkrypt script
# SETTINGS
2022-03-13 15:01:09 +01:00
# The directory that is searched for config files, use ~/.config if $XDG_CONFIG_HOME is not defined
[ [ -z $XDG_CONFIG_HOME ] ] && XDG_CONFIG_HOME = ~/.config
CONFIG_DIR = $XDG_CONFIG_HOME /msynk/
2022-03-11 21:07:56 +01:00
# When using encrypt/decrypt, the source is en/decrypted to TMP_DIR and then rsynced to receiver from there
# Path must contain 'msynk'
TMP_DIR = /tmp/msynk/
# When using --delete, this file is used to determine the files that will be deleted
TMP_FILE = /tmp/msynk_file
# Path to the mkrypt script
mkrypt = /usr/bin/mkrypt
# Additional flags for mkrypt
2022-03-13 15:01:09 +01:00
mkrypt_flags = ( )
2022-03-11 21:07:56 +01:00
2022-03-12 01:01:38 +01:00
rsync_flags = ( -ruh)
2022-03-13 15:01:09 +01:00
# rsync_flags+=(--rsh="ssh -p 42")
2022-03-11 21:07:56 +01:00
# r - relative
# v - verbose
# Ut - preserve modification&access times
# u - update, skip files that are newer on the receiver
# P - show progress and keep partially transferred files
# h - human readable
# see "man rsync" for more
2022-03-13 15:01:09 +01:00
# expand * to hidden files as well
shopt -s dotglob
2022-03-11 21:07:56 +01:00
# UTILITY
2022-03-13 15:01:09 +01:00
FMT_MESSAGE = "\e[1;34m%s\e[0m %s\n"
FMT_ERROR = "\e[1;31mmsync ERROR: \e[0m%s\n"
# exit 1: error, exit 2: rsync exited non 0, exit 3: mkrypt exited non 0
2022-03-12 01:01:38 +01:00
FMT_SYNC = "\e[1;32mSyncing: \e[0m%s\n"
2022-03-11 21:07:56 +01:00
FMT_CONFIG = "\e[34m%s\e:\t\e[1;33m%s\e[0m\n"
2022-03-12 01:01:38 +01:00
FMT_CMD = "\e[1;33mRunning: \e[0m%s\n"
2022-03-11 21:07:56 +01:00
2022-03-12 01:01:38 +01:00
# FUNCTIONS
# silence commands
2022-03-11 21:07:56 +01:00
quiet( ) { " $@ " > /dev/null 2>& 1; }
# check if a passed parameter is contained in file
2022-03-13 15:01:09 +01:00
# path must be set before calling
2022-03-11 21:07:56 +01:00
check_path_in_args( )
{
if [ $all = 1 ] ; then
return 0
fi
2022-03-13 15:01:09 +01:00
for string in ${ paths_2 [@] } ; do
2022-03-11 21:07:56 +01:00
if [ [ $path = = *$string * ] ] ; then
2022-04-08 01:56:55 +02:00
if [ [ $exclude = 1 ] ] ; then
return 1
else
return 0
fi
2022-03-11 21:07:56 +01:00
fi
done
2022-04-08 01:56:55 +02:00
if [ [ $exclude = 1 ] ] ; then
return 0
else
return 1
fi
2022-03-11 21:07:56 +01:00
}
2022-03-12 01:01:38 +01:00
# SYNC SENDER TO RECEIVER
2022-03-11 21:07:56 +01:00
sync_sender_to_receiver( )
{
# perform a dry run to see which files would be deleted
if [ [ -n $delete && -z $skip_dryrun ] ] ; then
2022-03-13 15:01:09 +01:00
[ [ $v -ge 1 ] ] && printf " $FMT_MESSAGE " "Performing dryrun" " to generate list of files that will be deleted from $receiver ... "
2022-03-11 21:07:56 +01:00
echo "" > $TMP_FILE
for path in " ${ filtered_paths [@] } " ; do
dest_subdir = $receiver $( basename $path )
rsync $rsync_flags -v --dry-run --delete $path $dest_subdir | grep deleting | sed " s(deleting ( ${ BACKUP_SUBDIR } /( " >> $TMP_FILE
done
# print files that will be deleted and ask if continue
# grep deleting $TMP_FILE | sed "s(deleting (${receiver}/(" | less
less $TMP_FILE
printf " $FMT_MESSAGE " " The listed files will be deleted from $receiver . "
printf "Proceed?\e[34m (y/n)\e[0m: "
read answer
case $answer in
y| Y)
# printf "$FMT_MESSAGE" "Backing up to $receiver..."
; ;
*)
printf " $FMT_MESSAGE " "Cancelled."
exit 0 ; ;
esac
fi
# actual syncing
for path in " ${ filtered_paths [@] } " ; do
[ [ $v -ge 1 ] ] && printf " $FMT_SYNC " " $path "
2022-03-13 15:01:09 +01:00
if [ [ -d " $path " ] ] ; then
dest = " $receiver " $( basename " $path " )
2022-03-11 21:07:56 +01:00
elif [ [ -f $path ] ] ; then
2022-03-13 15:01:09 +01:00
dest = " $receiver "
elif [ [ " $sender " = = *:* ] ] ; then # if sender is remote, assume the path is a dir if it has a slash and a file otherwise
[ [ " $path " = = *"/" ] ] && dest = " $receiver " $( basename " $path " ) || dest = " $receiver "
2022-03-11 21:07:56 +01:00
else
printf " $FMT_ERROR " " Invalid path: $path " ; exit 1
fi
if [ [ -n $encrypt || -n $decrypt ] ] ; then
2022-04-04 03:27:05 +02:00
mkdir -p " $TMP_DIR "
tmp_source = " $TMP_DIR "
2022-03-11 21:07:56 +01:00
if [ [ -n $encrypt ] ] ; then
2022-03-13 15:01:09 +01:00
# check if TMP_DIR has enough space
tmp_free = $( df --output= avail $TMP_DIR | grep -E "[[:digit:]]+" )
2022-05-02 19:01:20 +02:00
path_size = $( du -s " $path " | awk '{print $1}' | sed -e "s/[^0-9]//g" )
2022-03-13 15:01:09 +01:00
if [ [ $tmp_free -le $path_size ] ] ; then
printf " $FMT_ERROR " " Not enough space on $TMP_DIR to copy $path ! $TMP_DIR $tmp_free free - Size $path : $path_size "
printf " $FMT_MESSAGE " "" "If using a tmpfs, you can increase its size by running: 'sudo mount -o remount,size=XXG,noatime /tmp' to increase the size to XXG (You will need a large enough swap space)"
exit 1
fi
[ [ -f " $path " ] ] && tmp_source = " $TMP_DIR " $( basename " $path " ) .gpg
[ [ $v -ge 3 ] ] && printf " $FMT_CMD " " bash $mkrypt --encrypt $path --output $TMP_DIR ${ mkrypt_flags [*] } "
bash $mkrypt --encrypt " $path " --output " $TMP_DIR " ${ mkrypt_flags [@] } || exit 3
2022-03-12 01:01:38 +01:00
[ [ $v -ge 3 ] ] && printf " $FMT_CMD " " rsync ${ rsync_flags [*] } $tmp_source $dest "
2022-03-13 15:01:09 +01:00
rsync " ${ rsync_flags [@] } " " $tmp_source " " $dest " || { printf " $FMT_ERROR " " rsync exited with exit code $? " ; exit 2; }
2022-03-11 21:07:56 +01:00
else
[ [ -f $path ] ] && tmp_source = $TMP_DIR $( basename $path )
2022-03-12 01:01:38 +01:00
[ [ $v -ge 3 ] ] && printf " $FMT_CMD " " rsync ${ rsync_flags [*] } $path $TMP_DIR "
2022-03-13 15:01:09 +01:00
rsync " ${ rsync_flags [@] } " " $path " " $TMP_DIR " || { printf " $FMT_ERROR " " rsync exited with exit code $? " ; exit 2; }
2022-04-04 03:27:05 +02:00
[ [ $v -ge 3 ] ] && printf " $FMT_CMD " " bash $mkrypt --decrypt $tmp_source --output $( readlink -f $dest ) ${ mkrypt_flags [*] } "
2022-03-13 15:01:09 +01:00
bash $mkrypt --decrypt " $tmp_source " --output " $( readlink -f " $dest " ) " ${ mkrypt_flags [@] } || exit 3
2022-03-11 21:07:56 +01:00
fi
else
2022-03-12 01:01:38 +01:00
[ [ $v -ge 3 ] ] && printf " $FMT_CMD " " rsync ${ rsync_flags [*] } $delete $path $dest "
2022-03-13 15:01:09 +01:00
rsync " ${ rsync_flags [@] } " $delete " $path " " $dest " || { printf " $FMT_ERROR " " rsync exited with exit code $? " ; exit 2; }
2022-03-11 21:07:56 +01:00
fi
2022-03-13 15:01:09 +01:00
[ [ " $TMP_DIR " = = *msynk* ] ] && {
rm -rf " $TMP_DIR " /* || { printf " $FMT_ERROR " " Can not delete contents of \$TMP_DIR: $TMP_DIR " ; exit 1; }
2022-03-11 21:07:56 +01:00
} || { printf " $FMT_ERROR " " Will not delete \$TMP_DIR because it might be dangerous (path does not contain 'msynk'): $TMP_DIR " ; exit 1; }
done
2022-03-12 01:01:38 +01:00
# put todays date in the config
if [ [ -f $CONFIG_DIR /$config ] ] ; then
if grep -xq "date=.*" $CONFIG_DIR $config ; then
2022-03-13 15:01:09 +01:00
[ [ $v -ge 2 ] ] && printf " $FMT_MESSAGE " "Updating" " date in $CONFIG_DIR $config . "
sed " s/date=.*/date=\" $( date --iso= sec) \"/ " $CONFIG_DIR $config -i
2022-03-12 01:01:38 +01:00
else
2022-03-13 15:01:09 +01:00
[ [ $v -ge 2 ] ] && printf " $FMT_MESSAGE " "Writing" " current date to $CONFIG_DIR $config . "
echo " date=\" $( date --iso= sec) \" " >> $CONFIG_DIR $config
2022-03-12 01:01:38 +01:00
fi
fi
2022-03-13 15:01:09 +01:00
[ [ $v -ge 2 ] ] && printf " $FMT_MESSAGE " "Running:" "'sync' to write cached writes to persistent storage"
2022-03-11 21:07:56 +01:00
sync
[ [ $v -ge 1 ] ] && printf " $FMT_MESSAGE " "Done!"
}
# HELP
show_help( )
{
2022-03-13 15:01:09 +01:00
printf " \e[33mArgument Short Action:\e[0m
--help -h Show this.
--settings -s Show current settings.
2022-03-11 21:07:56 +01:00
2022-03-13 15:01:09 +01:00
--config [ name] -c [ name] Use sender, receiver, paths from config file with [ name] .
--show-config [ name] Print variables defined by a config with [ name] .
2022-03-11 21:07:56 +01:00
2022-03-13 15:01:09 +01:00
--sender [ path] -s [ path] Sender directory, with trailing slash! Defaults to the current working directory.
--receiver [ path] -r [ path] Receiver directory, with trailing slash!.
--reverse Swap receiver and sender.
2022-03-11 21:07:56 +01:00
2022-03-13 15:01:09 +01:00
--encrypt Encrypt files before syncing.
--decrypt Decrypt files before syncing.
--check-date Only process files that have been modified since the program was last run. Needs -c and ( --encrypt/--decrypt) .
--mkrypt-flag [ arg] Additional argument for mkrypt.
2022-03-11 21:07:56 +01:00
2022-03-13 15:01:09 +01:00
--delete -d Delete files that exist on receiver, but not on sender. Does not work with --encrypt/--decrypt.
--skip-dryrun Delete without asking first.
--rsync-flag [ arg] Additional argument for rsync.
2022-03-11 21:07:56 +01:00
2022-03-13 15:01:09 +01:00
--verbose -v Increase verbosity.
--silent Decrease verbosity.
--debug Maximum verbosity.
2022-03-11 21:07:56 +01:00
2022-04-08 01:56:55 +02:00
--exclude Interpret positional arguments as blacklist
2022-03-11 21:07:56 +01:00
Positional arguments are:
2022-03-13 15:01:09 +01:00
- if using a config: a string that must be in the configs paths: eg. 'foo' will include ~/foo but not ~/bar
2022-03-11 21:07:56 +01:00
- if not using a config: paths to sync ( must be relative to --sender)
See the manpage for more information.
"
}
2022-03-12 01:01:38 +01:00
# SETTINGS
2022-03-11 21:07:56 +01:00
show_settings( )
{
printf "\e[1mThe current settings are:\e[0m\n"
2022-03-12 01:01:38 +01:00
printf " $FMT_CONFIG " "\$TMP_DIR " " $TMP_DIR "
2022-03-11 21:07:56 +01:00
printf " $FMT_CONFIG " "\$TMP_FILE " " $TMP_FILE "
2022-03-12 01:01:38 +01:00
printf " $FMT_CONFIG " "\$CONFIG_DIR " " $CONFIG_DIR "
[ [ -f $mkrypt ] ] || mkrypt_installed = "(file not found)"
printf " $FMT_CONFIG " "\$mkrypt " " $mkrypt $mkrypt_installed "
2022-03-11 21:07:56 +01:00
printf " $FMT_CONFIG " "\$rsync_flags " " ${ rsync_flags [*] } "
}
2022-03-13 15:01:09 +01:00
show_config( )
{
[ [ ! -f $CONFIG_DIR $config ] ] && { printf " $FMT_ERROR " " Invalid path: $CONFIG_DIR $config " ; exit 1; }
source $CONFIG_DIR $config
printf " \e[1m $CONFIG_DIR $config :\e[0m\n "
printf " $FMT_CONFIG " "\$sender " " $sender "
printf " $FMT_CONFIG " "\$receiver " " $receiver "
printf " $FMT_CONFIG " "\$paths " " ${ paths [*] } "
printf " $FMT_CONFIG " "\$use_encryption " " $use_encryption "
printf " $FMT_CONFIG " " Last run: $date "
}
2022-03-11 21:07:56 +01:00
# PARSE ARGS
if [ -z $1 ] ; then
show_help
exit 0
fi
2022-03-12 01:01:38 +01:00
all = 1
2022-03-11 21:07:56 +01:00
paths_2 = ( )
while ( ( " $# " ) ) ; do
case " $1 " in
-h| --help)
show_help
exit 0 ; ;
--settings)
show_settings
exit 0 ; ;
-c| --config)
if [ [ -n $2 && ${ 2 : 0 : 1 } != "-" ] ] ; then
config = $2
shift 2
else printf " $FMT_ERROR " " Missing argument for $1 " ; exit 1; fi ; ;
2022-03-13 15:01:09 +01:00
--show-config)
if [ [ -n $2 && ${ 2 : 0 : 1 } != "-" ] ] ; then
config = $2
show_config
exit 0
else printf " $FMT_ERROR " " Missing argument for $1 " ; exit 1; fi ; ;
2022-03-11 21:07:56 +01:00
-s| --sender)
if [ [ -n $2 && ${ 2 : 0 : 1 } != "-" ] ] ; then
sender_2 = $2
shift 2
else printf " $FMT_ERROR " " Missing argument for $1 " ; exit 1; fi ; ;
-r| --receiver)
if [ [ -n $2 && ${ 2 : 0 : 1 } != "-" ] ] ; then
receiver_2 = $2
shift 2
else printf " $FMT_ERROR " " Missing argument for $1 " ; exit 1; fi ; ;
--reverse)
reverse = 1
shift ; ;
--encrypt)
encrypt = 1
2022-03-12 01:01:38 +01:00
use_encryption = 1
2022-03-11 21:07:56 +01:00
shift ; ;
--decrypt)
decrypt = 1
2022-03-12 01:01:38 +01:00
use_encryption = 1
2022-03-11 21:07:56 +01:00
shift; ;
-d| --delete)
delete = --delete
shift ; ;
--skip-dryrun)
skip_dryrun = 1
shift ; ;
2022-03-13 15:01:09 +01:00
--rsync-flag)
2022-03-11 21:07:56 +01:00
if [ [ -n ${ 2 } ] ] ; then # && "${2:0:1}" != "-" ]]; then # possibly problematic if $2 is an msynk flag
rsync_flags += ( " ${ 2 } " )
shift 2
else printf " $FMT_ERROR " " Missing argument for $1 " ; exit 1; fi ; ;
2022-03-13 15:01:09 +01:00
--check-date)
check_date = 1
2022-03-12 01:01:38 +01:00
shift ; ;
2022-03-13 15:01:09 +01:00
--mkrypt-flag)
2022-03-11 21:07:56 +01:00
if [ [ -n ${ 2 } ] ] ; then # && "${2:0:1}" != "-" ]]; then # possibly problematic if $2 is an msynk flag
mkrypt_flags += ( " ${ 2 } " )
shift 2
else printf " $FMT_ERROR " " Missing argument for $1 " ; exit 1; fi ; ;
-v| --verbose)
v = 2
rsync_flags += ( -v)
mkrypt_flags += ( -v)
shift ; ;
--silent)
v = 0
mkrypt_flags += ( --silent)
shift ; ;
--debug)
v = 3
rsync_flags += ( -v)
mkrypt_flags += ( -v)
shift; ;
2022-04-08 01:56:55 +02:00
--exclude)
exclude = 1
shift ; ;
2022-03-11 21:07:56 +01:00
-*| --*= ) # unsupported flags
printf " $FMT_ERROR " " Unsupported flag $1 " >& 2
exit 1 ; ;
*) # everything that does not have a - is interpreted as filepath
2022-03-13 15:01:09 +01:00
all = 0
2022-03-11 21:07:56 +01:00
paths_2 += ( $1 )
shift ; ;
esac
done
2022-03-12 01:01:38 +01:00
# PREPARATION & CHECKS
2022-03-13 15:01:09 +01:00
# clear $TMP_DIR: might be non-empty if previous msynk failed
[ [ $TMP_DIR = = *msynk* ] ] && {
rm -rf $TMP_DIR /* || { printf " $FMT_ERROR " " Can not contents of \$TMP_DIR: $TMP_DIR " ; exit 1; }
} || { printf " $FMT_ERROR " " Will not delete \$TMP_DIR because it might be dangerous (path does not contain 'msynk'): $TMP_DIR " ; exit 1; }
2022-03-11 21:07:56 +01:00
# check if rsync is installed
if ! command -v rsync & > /dev/null; then
printf " $FMT_ERROR " "rsync is not installed."
2022-03-12 01:01:38 +01:00
exit 1
2022-03-11 21:07:56 +01:00
fi
2022-03-12 01:01:38 +01:00
# if using encryption, check if mkrypt is installed
2022-03-13 15:01:09 +01:00
[ [ $use_encryption = 1 && ! -f $mkrypt ] ] && { printf " $FMT_ERROR " " Can not use encryption: mkrypt is not installed at given path: $mkrypt . " ; exit 1; }
2022-03-12 01:01:38 +01:00
2022-03-11 21:07:56 +01:00
# if using a config
if [ [ -n $config ] ] ; then
2022-03-13 15:01:09 +01:00
source $CONFIG_DIR $config || { printf " $FMT_ERROR " " Error running the config file: $CONFIG_DIR $config " ; exit 1; }
2022-03-12 01:01:38 +01:00
if [ [ $use_encryption = 1 ] ] ; then
[ [ -z $reverse ] ] && encrypt = 1 || decrypt = 1
fi
2022-04-04 03:27:05 +02:00
[ [ $v -ge 3 ] ] && printf " $FMT_MESSAGE " "Loaded config:" " $CONFIG_DIR $config : date: $date sender: $sender receiver: $receiver use_encryption: $use_encryption encrypt: $encrypt decrypt: $decrypt paths: ${ paths [*] } "
2022-03-11 21:07:56 +01:00
fi
# overwrite stuff from the config if anything else was given / set variables of no config was given
[ [ -n $sender_2 ] ] && sender = $sender_2
[ [ -n $receiver_2 ] ] && receiver = $receiver_2
[ [ -z $sender ] ] && {
2022-03-13 15:01:09 +01:00
[ [ $v -ge 1 ] ] && printf " $FMT_MESSAGE " "Missing sender:" " Using working directory: $( pwd ) "
2022-03-11 21:07:56 +01:00
sender = " $( pwd ) / "
}
# reverse: swap sender and reveiver
if [ [ -n $reverse ] ] ; then
tmp_sender = $sender
sender = $receiver
receiver = $tmp_sender
fi
# make sure receiver has trailing slash
[ [ $receiver != *"/" ] ] && receiver = $receiver "/"
2022-03-12 01:01:38 +01:00
# create receiver dir if its local
2022-03-13 15:01:09 +01:00
[ [ ! $receiver = = *:* ] ] && { mkdir -p $receiver || { printf " $FMT_ERROR " " Can not create receiver directory: $receiver " ; exit 1; } ; }
2022-03-11 21:07:56 +01:00
2022-03-13 15:01:09 +01:00
[ [ $v -ge 3 ] ] && printf " $FMT_MESSAGE " "Variables:" " sender: $sender receiver: $receiver paths: ${ paths [*] } paths_2: ${ paths_2 [*] } "
2022-03-11 21:07:56 +01:00
# filter paths:
# - if using config: all paths from config that have a pos.arg in them
# - else: pos.args, must be relative to specified --sender
filtered_paths = ( )
2022-03-13 15:01:09 +01:00
ifs = $IFS
IFS = $'\n'
2022-03-11 21:07:56 +01:00
if [ [ -n $config ] ] ; then
2022-03-12 01:01:38 +01:00
[ [ $sender != *"/" ] ] && sender = $sender "/"
2022-03-13 15:01:09 +01:00
for path in " ${ paths [@] } " ; do
2022-03-11 21:07:56 +01:00
if check_path_in_args; then
2022-03-13 15:01:09 +01:00
# if reverse and encryption (use_encryption might be overwritten by config when --decrypt is used) and file, it will have a .gpg extension
[ [ -n $reverse && ( $use_encryption = 1 || $decrypt = 1) && " $path " != *"/" ] ] && path = " $path " .gpg
filtered_paths += ( " $sender $path " )
2022-03-11 21:07:56 +01:00
fi
done
else
for path in ${ paths_2 [@] } ; do # add slash if directory
2022-03-13 15:01:09 +01:00
[ [ -d " $path " ] ] && [ [ " $path " != *"/" ] ] && path = " $path / "
filtered_paths += ( " $sender $path " )
2022-03-11 21:07:56 +01:00
done
fi
2022-03-13 15:01:09 +01:00
[ [ $v -ge 3 ] ] && printf " $FMT_MESSAGE " "Filtered paths:" " ${ filtered_paths [*] } "
IFS = $ifs
2022-03-11 21:07:56 +01:00
# sanity checks
[ [ -z $filtered_paths ] ] && { printf " $FMT_ERROR " "Missing valid paths." ; exit 1; }
[ [ -z $receiver ] ] && { printf " $FMT_ERROR " "Missing receiver. Specifiy with --receiver" ; exit 1; }
2022-03-13 15:01:09 +01:00
[ [ -n $delete && ( -n $decrypt || -n $encrypt ) ] ] && { printf " $FMT_ERROR " "Can not use --delete and encryption at the same time." ; exit 1; }
2022-03-11 21:07:56 +01:00
2022-03-13 15:01:09 +01:00
# if using --check-date
if [ [ -n $check_date && -z $date ] ] ; then # if --check-date but date is not set, use 0 (unix time)
[ [ $v -ge 1 ] ] && printf " $FMT_MESSAGE " "--check-date:" "passed but date not set in config, using 0 (unix time)"
date = $( date --iso= sec -d @0)
2022-03-12 01:01:38 +01:00
fi
2022-03-13 15:01:09 +01:00
[ [ -n $check_date ] ] && { mkrypt_flags += ( --check-date " ${ date } " ) ; rsync_flags += ( --times) ; }
2022-03-12 01:01:38 +01:00
# RUN!
2022-03-11 21:07:56 +01:00
sync_sender_to_receiver
exit 0