Skip to content

Instantly share code, notes, and snippets.

@AlexanderMakarov
Last active April 21, 2024 12:20
Show Gist options
  • Save AlexanderMakarov/fbb997bab3b69617ed3380ca4f04cfe1 to your computer and use it in GitHub Desktop.
Save AlexanderMakarov/fbb997bab3b69617ed3380ca4f04cfe1 to your computer and use it in GitHub Desktop.
Script for regular encrypted backups an restoring of specific folders and filterable files on Gdrive.
#!/bin/bash -xeE
set -o pipefail
# Idea: archive files or folders (tar), next encrypt archives (7z), push into GDrive (rclone).
# Prerequisits:
# - (usually already here) install 7za/7z - `sudo apt-get install p7zip-full`/`brew install p7zip`
# If executable has another name then need to see in script how it is used and correct.
# - install rclone and setup - https://ostechnix.com/install-rclone-in-linux/ or `brew install rclone`
# https://ostechnix.com/mount-google-drive-using-rclone-in-linux/ and don't set password for the configuration
# - set right values into variables (upper-cased, right below description).
# - setup to run by cron - https://www.cyberciti.biz/faq/how-do-i-add-jobs-to-cron-under-linux-or-unix-oses/
# like '30 9 * * * user /home/user/sync_encrypt.sh > /home/user/last_backup.log 2>&1'
# User entry (second) need only for some distros.
# To restore copy files except script itself from GDrive into $SYNC_FOLDER, create $PASSWORD_FILE
# and run with 'decrypt' argument. It will restore all files in /tmp folder to next manually move.
# Notes:
# - $LOCATIONS_WITH_EXTENTIONS is not used on restoring.
# - Removed $LOCATIONS_WITH_EXTENTIONS entry remains in a backup and in $SYNC_FOLDER,
# to get rid of it need to remove manually both from $SYNC_FOLDER and GDrive folder.
# - Any issue is reported into STDOUT/ERR and creates $NOTIFY_ABOUT_ERROR_FILE.
# - Any issue (like location doesn't not exsit) breaks the whole backup process.
# - On Mac OS `rclone` very often looses authentication to GDrive.
# - Empty folder in a backup may break decrypting.
# - Don't need to setup `rclone` for restoring - just copy files from relevant folder in Grdive
# into $SYNC_FOLDER and comment last `rclone copy` at bottom of this script.
# Put values below as lines with comma-separated values without spaces.
# First value is required and it is a path to file of folder to backup.
# Remained values are optional and means filters for files by suffix/extension to backup. Format:
# - Location - path started strongly from the root and with / at the end if it is a folder. '^' in name is unsupported.
# - Suffixes - suffixes with a dot if an extension and lower-cased like '.pdf'.
LOCATIONS_WITH_EXTENTIONS=(
"/home/user/.bashrc"
"/home/user/.ssh/"
"/home/user/scripts/,.sh"
"/mnt/data/Docs/,.txt,.xml,.pdf,.odt,.doc,.docx,.xls,.xlsx,.jpg,.jpeg,.png,.xcf,.zip,.sh"
)
# File with password to encrypt files via 7z. Should has restricted access and password without spaces inside.
PASSWORD_FILE="/home/user/.backup/backup_password.txt"
# Folder to copy results into.
SYNC_FOLDER="/home/user/sync_encrypt_rclone/"
# Google Drive folder to sync results into. Should be in `rclone` destination format.
RCLONE_DEST="gdrive:Backups/name-of-machine"
# Path to folder where to restore decrypted files.
DECRYPT_DEST="/tmp/sync_encrypt"
# Path to file which will be created in case of error in the script.
NOTIFY_ABOUT_ERROR_FILE="/home/user/Desktop/sync_encrypt_error.log"
# (Don't need to change) name of temporal file to save internal errors into.
FILE_FOR_ERRORS="/tmp/sync_encrypt_stderr"
# Make .tar.7z password-encrypted archive from given file/folder with filtering.
# - 1: Path to input folder/file.
# - 2: Comma-separated list of endings to filter by. Empty line means no filtering.
# - 3: Path to file with password to encrypt 7z archive with.
# - 4: Result file path (without .tar.7z, it will be added inside). Folder should exist.
function encrypt_folder {
local file_path="$4"
echo "$1 => $file_path"
local find_args=()
if [[ "$2" ]]; then
IFS=',' read -ra patterns <<< "$2"
for pattern in "${patterns[@]}"; do
find_args+=("-iname" "*$pattern" "-o")
done
# Remove the last "-o" added.
unset 'find_args[${#find_args[@]}-1]'
fi
rm -f "$file_path" # 7z can't overwrite, it adds inside, so remove first.
if [ "${#find_args[@]}" -eq 0 ]; then
files=$(find "$1")
else
files=$(find "$1" "${find_args[@]}")
fi
OS=$(uname -s)
set +x # Don't print password.
local password=$(cat "$3")
# 7z below need to pack one file with path received from STDIN and encrypt it with password from file specified in $3.
if [ "$OS" == "Linux" ]; then
echo "$files" | tr '\n' '\0' \
| tar -cv --hard-dereference --null -T - \
| 7za u -sae -si "$file_path" -p$password
elif [ "$OS" == "Darwin" ]; then
echo "$files" | tr '\n' '\0' \
| tar -cv -f - --null --files-from - \
| /opt/homebrew/bin/7za u -sae -si "$file_path" -p$password
else
echo "Unsupported OS '$OS'"
exit 1
fi
}
# Decrypts and unzip-s given file content into given folder.
# - 1: Path to file with decrypted data.
# - 2: Path to file with password to decrypt 7z archive with.
# - 3: Path to folder decrypt into.
function decrypt_file {
set +x # Don't print password.
# 7z need to unpack and decrypt into STDOUT file $1 with password from file specified in $2.
local command="7z x -sae -so -p\$(cat '$2') '$1' | tar -C '$3' -xf -"
echo ">Running: $command"
eval "$command"
}
# Handles errors and writes details into NOTIFY_ABOUT_ERROR_FILE file which would be noted by user.
# - 1: Line where error happened.
function handle_error {
local last_exit_status="$?"
local last_lineno="$1"
local stderr=$(<$FILE_FOR_ERRORS) # Read from FILE_FOR_ERRORS.
local script=`realpath $0`
local message="$(date): ${script}:${last_lineno} '${BASH_COMMAND}' failed with ${last_exit_status} code: ${stderr}"
echo "${message}"
echo "${message}" > ${NOTIFY_ABOUT_ERROR_FILE}
exec 3>&- # Close file descriptor 3
exit "${last_exit_status}"
}
# Trap and handle error.
trap 'handle_error $LINENO' ERR
# Entry point.
if [[ "$1" == "-h" || "$1" == "--help" ]];then
set +x
echo "
Backups configured list of locations into GDrive with encrypting them for security.
Also puts enencrypted script itself into backup to restore on a new machine.
Correct variables on the top of this script and run without arguments for backup.
For restoring create '${PASSWORD_FILE}' file with the password used for creating backup.
Run with 'decrypt' argument to copy and decrypt files into '${DECRYPT_DEST}'.
On any error script creates '${NOTIFY_ABOUT_ERROR_FILE}'' with error description.
Note that '$SYNC_FOLDER' should be clean before restoring and not touched on backups."
exit 0
fi
# Create SYNC_FOLDER and FILE_FOR_ERRORS, redirect all STDERR into it.
if [[ ! -d "$SYNC_FOLDER" ]];then
mkdir "$SYNC_FOLDER"
fi
rm -f "$FILE_FOR_ERRORS"
exec 2> >(tee "${FILE_FOR_ERRORS}")
# Check which operation need to do.
if [[ "$1" != "decrypt" ]];then
script=`realpath $0`
# Copy script itself, for restoring and having list of locations.
# It may help examine backup content and keep script in restored environment.
cp "$script" "${SYNC_FOLDER}/"`basename $0`
# Ecrypt locations.
for line in "${LOCATIONS_WITH_EXTENTIONS[@]}";do
location=${line%%,*} # https://www.baeldung.com/linux/bash-string-manipulation
if [[ "$line" =~ "," ]];then
filters=${line#*,}
else
filters=""
fi
file_name=${location////^} # Encode path with replacing '/'->'^'.
encrypt_folder "$location" "$filters" "$PASSWORD_FILE" "$SYNC_FOLDER/$file_name"
done
# Clone into backup storage.
OS=$(uname -s)
if [ "$OS" == "Linux" ]; then
rclone copy "$SYNC_FOLDER" "$RCLONE_DEST"
elif [ "$OS" == "Darwin" ]; then
/opt/homebrew/bin/rclone copy "$SYNC_FOLDER" "$RCLONE_DEST"
fi
echo "Backup into $RCLONE_DEST has completed."
else
rclone copy "$RCLONE_DEST" "$SYNC_FOLDER"
if [[ ! -d "$DECRYPT_DEST" ]];then
mkdir "$DECRYPT_DEST"
fi
for sync_entry_path in "$SYNC_FOLDER"/*;do
# Skip the script itself.
if [[ "$sync_entry_path" == *^* ]]; then
decrypt_file "$sync_entry_path" "$PASSWORD_FILE" "$DECRYPT_DEST"
fi
done
echo "Restoring from $SYNC_FOLDER into $DECRYPT_DEST has completed."
fi
# Close FIFO_FOR_ERRORS at the end.
exec 3>&-
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment