Last active
April 21, 2024 12:20
-
-
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.
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 -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