Skip to content

Instantly share code, notes, and snippets.

@JonTheNiceGuy
Created April 25, 2019 22:35
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save JonTheNiceGuy/007a84265938a2c96cab99d9e192796b to your computer and use it in GitHub Desktop.
Save JonTheNiceGuy/007a84265938a2c96cab99d9e192796b to your computer and use it in GitHub Desktop.
Three-way sync on Keepass files, using Keepass2 and KPScript on a Linux based system.

This script is used if you want to do a three-way sync between (for example) Dropbox, Nextcloud and Syncthing (depending on your personal deployment capabilities).

It should be relatively easy to determine what tweaks are needed to reduce this to a 2-way sync, or up to a 4, 5 or even 6 way sync.

#!/bin/bash
# Set the mask for the following actions to prevent anyone *else* from reading the following file
umask 0077
# Create the temp file
tmpfile="/tmp/$(basename "$0")"."$$"."$(awk 'BEGIN {srand();printf "%d\n", rand() * 10^10}')"
# Tell you script to delete the file it just created when the script dies
trap 'if [ $(cat "$tmpfile/log" | wc -l) -gt 1 ]; then cat "$tmpfile/log" | mail -s "KPSync" $(whoami); fi; rm -Rf -- "$tmpfile" >/dev/null' INT TERM HUP EXIT
mkdir -p "$tmpfile"
log() {
echo "$1" | tee -a "$tmpfile/log"
}
error() {
log "$1"
exit $2
}
log_cp() {
cp -v "$1" "$2" 2>&1 | tee -a "$tmpfile/log"
}
log_rm() {
rm -fv "$1" 2>&1 | tee -a "$tmpfile/log"
}
debug_echo() {
if [ -n "$DEBUG" ]; then
echo "$1"
fi
}
debug_cls() {
if [ -n "$DEBUG_CLEAR" ]; then
clear
fi
}
help() {
echo "KPSync file1.kdbx file2.kdbx file3.kdbx"
echo ""
echo "KPSync first fixes any file conflicts and then synchronizes between the paths."
echo ""
exit 1
}
Resync() {
log "Synchronizing $1 and $2"
chmod u+w "$1" "$2"
if [ -z "$SYNCPW" ]; then
read -s -p "Please enter sync password: [Note: This will be in your ps output] " SYNCREAD ; /usr/bin/cli /usr/lib/keepass2/KPScript.exe -pw:"$SYNCREAD" -c:Sync -File:"$1" "$2"
ERROR=$?
if [ $ERROR -gt 0 ]; then
error "Error while resyncing" $ERROR
fi
else
/usr/bin/cli /usr/lib/keepass2/KPScript.exe -pw-enc:"$SYNCPW" -c:Sync -File:"$1" "$2" | tee -a "$tmpfile/log"
ERROR=$?
if [ $ERROR -gt 0 ]; then
error "Error while resyncing" $ERROR
fi
fi
}
TweakTime() {
touch -r "$1" -d '+1 second' "$1"
touch -r "$1" -d '-1 second' "$1"
}
FixConflicts() {
FILE1="$1"
PATH1="$(dirname "$1")"
NAME1="$(basename "$1" .kdbx)"
if [ `find "$PATH1" -name "${NAME1}*conflict*kdbx" | wc -l` -gt 0 ]; then
debug_echo "Conflicts found in Path"
find "$PATH1" -name "${NAME1}*conflict*kdbx" | while read file
do
log "Resolving conflict between $1 and $file"
Resync "$1" "$file"
log "Removing Conflict File $file"
log_rm "$file"
done
fi
}
if [ -z "$1" ] || [ ! -f "$1" ]; then
error "Missing file 1" 1
help
fi
if [ -z "$2" ] || [ ! -f "$2" ]; then
error "Missing file 2" 1
help
fi
if [ -z "$3" ] || [ ! -f "$3" ]; then
error "Missing file 3" 1
help
fi
_SYNCTIME=60
if [ -n "$SYNCTIME" ]; then
_SYNCTIME="$SYNCTIME"
fi
while true; do
debug_cls
debug_echo "Starting Cycle at $(date)"
echo "" > "$tmpfile/log"
SYNCPW="$(cat .syncpw 2>/dev/null || cat ~/.config/syncpw 2>/dev/null || cat ~/.syncpw 2>/dev/null || true)"
if [ $(wc -l "$1" | cut -f1 -d\ ) -lt 1 ]; then NULLFILE1=true; else NULLFILE1=false; fi
if [ $(wc -l "$2" | cut -f1 -d\ ) -lt 1 ]; then NULLFILE2=true; else NULLFILE2=false; fi
if [ $(wc -l "$3" | cut -f1 -d\ ) -lt 1 ]; then NULLFILE3=true; else NULLFILE3=false; fi
if [ "$NULLFILE1" = "true" ] && [ "$NULLFILE2" = "true" ] && [ "$NULLFILE3" = "true" ]; then
error "All files are truncated. Please urgently recover from backups!" 127
elif [ "$NULLFILE1" = "true" ]; then
if [ "$NULLFILE2" = "false" ]; then
log "$1 was truncated, recovering from $2"
log_cp "$2" "$1"
else # NULLFILE3 = false
log "$1 was truncated, recovering from $3"
log_cp "$3" "$1"
fi
elif [ "$NULLFILE2" = "true" ]; then
if [ "$NULLFILE1" = "false" ]; then
log "$2 was truncated, recovering from $1"
log_cp "$1" "$2"
else # NULLFILE3 = false
log "$2 was truncated, recovering from $3"
log_cp "$3" "$2"
fi
elif [ "$NULLFILE3" = "true" ]; then
if [ "$NULLFILE1" = "false" ]; then
log "$3 was truncated, recovering from $1"
log_cp "$1" "$3"
else # NULLFILE2 = false
log "$3 was truncated, recovering from $2"
log_cp "$2" "$3"
fi
fi
debug_echo "Touching files"
TweakTime "$1"
TweakTime "$2"
TweakTime "$3"
debug_echo "Checking for conflicts"
FixConflicts "$1"
FixConflicts "$2"
FixConflicts "$3"
debug_echo "Syncing Files"
HASH1="$(md5sum "$1" | cut -f1 -d\ )"
HASH2="$(md5sum "$2" | cut -f1 -d\ )"
HASH3="$(md5sum "$3" | cut -f1 -d\ )"
debug_echo "$HASH1 $1"
debug_echo "$HASH2 $2"
debug_echo "$HASH3 $3"
if [ "$HASH1" = "$HASH2" ] && [ "$HASH1" != "$HASH3" ] && [ "$HASH2" != "$HASH3" ]; then
# 1 and 2 match, 3 does not
log "File $3 is out of sync with the peer files. Resync occurring."
log_cp "$1" "$tmpfile/target.kdbx"
log_cp "$3" "$tmpfile/diff.kdbx"
Resync "$tmpfile/target.kdbx" "$tmpfile/diff.kdbx"
log "Resync complete. Replacing files with updated version."
log_cp "$tmpfile/target.kdbx" "$1"
log_cp "$tmpfile/target.kdbx" "$2"
log_cp "$tmpfile/target.kdbx" "$3"
debug_echo "Removing master and diff files."
log_rm "$tmpfile/target.kdbx"
log_rm "$tmpfile/diff.kdbx"
elif [ "$HASH1" != "$HASH2" ] && [ "$HASH1" = "$HASH3" ] && [ "$HASH2" != "$HASH3" ]; then
# 1 and 3 match, 2 does not
log "File $2 is out of sync with the peer files. Resync occurring."
log_cp "$1" "$tmpfile/target.kdbx"
log_cp "$2" "$tmpfile/diff.kdbx"
Resync "$tmpfile/target.kdbx" "$tmpfile/diff.kdbx"
log "Resync complete. Replacing files with updated version."
log_cp "$tmpfile/target.kdbx" "$1"
log_cp "$tmpfile/target.kdbx" "$2"
log_cp "$tmpfile/target.kdbx" "$3"
debug_echo "Removing master and diff files."
log_rm "$tmpfile/target.kdbx"
log_rm "$tmpfile/diff.kdbx"
elif [ "$HASH1" != "$HASH2" ] && [ "$HASH1" != "$HASH3" ] && [ "$HASH2" = "$HASH3" ]; then
# 2 and 3 match, 1 does not
log "File $1 is out of sync with the peer files. Resync occurring."
log_cp "$1" "$tmpfile/target.kdbx"
log_cp "$2" "$tmpfile/diff.kdbx"
Resync "$tmpfile/target.kdbx" "$tmpfile/diff.kdbx"
log "Resync complete. Replacing files with updated version."
log_cp "$tmpfile/target.kdbx" "$1"
log_cp "$tmpfile/target.kdbx" "$2"
log_cp "$tmpfile/target.kdbx" "$3"
debug_echo "Removing master and diff files."
log_rm "$tmpfile/target.kdbx"
log_rm "$tmpfile/diff.kdbx"
elif [ "$HASH1" != "$HASH2" ] && [ "$HASH1" != "$HASH3" ] && [ "$HASH2" != "$HASH3" ]; then
# ALL FILES OUT OF SYNC
log "All files are out of sync. Bringing all versions (1 -> 2, 1&2 -> 3) up to date."
log_cp "$1" "$tmpfile/target.kdbx"
log_cp "$2" "$tmpfile/diff.kdbx"
Resync "$tmpfile/target.kdbx" "$tmpfile/diff.kdbx"
log_cp "$3" "$tmpfile/diff.kdbx"
Resync "$tmpfile/target.kdbx" "$tmpfile/diff.kdbx"
log "Resync complete. Replacing files with updated version."
log_cp "$tmpfile/target.kdbx" "$1"
log_cp "$tmpfile/target.kdbx" "$2"
log_cp "$tmpfile/target.kdbx" "$3"
debug_echo "Removing master and diff files."
log_rm "$tmpfile/target.kdbx"
log_rm "$tmpfile/diff.kdbx"
elif [ "$HASH1" = "$HASH2" ] && [ "$HASH1" = "$HASH3" ] && [ "$HASH2" = "$HASH3" ]; then
debug_echo "All files in sync."
else
error "Unexpected state. Exiting" 100
fi
if [ $(cat "$tmpfile/log" | wc -l) -gt 1 ]; then
cat "$tmpfile/log" | mail -s 'KPSync' $(whoami)
fi
if [ $_SYNCTIME -lt 2 ]; then
exit 0
fi
sleep $_SYNCTIME
done
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment