#!/usr/bin/env bash ## Back up my files to each external drive using rsync. ## ## Usage: backup ## ## Dependencies: GNU coreutils, GNU grep, GNU sed, rsync ## ## This script creates an incremental backup in <drive>/backups/ using a list of ## source paths. Unchanged files are hard linked to the last backup. This script ## also syncs my home directory to <drive>/mirror/ just in case I someday fail ## to add an important path to the list of sources for my backups. set -euo pipefail shopt -s inherit_errexit die() { echo "$(basename "$0"): $1" >&2; exit 1; } [[ " $* " =~ ' --help ' ]] && sed -n 's/^## \?//p' "$0" && exit (( UID != 0 )) || die 'do not run as root' # Source paths for backups (not mirrors). sources=( ~/./.archive ~/./.bashrc ~/./.config/.archive ~/./.config/Xresources ~/./.config/dunst ~/./.config/emacs/.archive ~/./.config/emacs/init.el ~/./.config/emacs/lisp ~/./.config/emacs/themes ~/./.config/feh ~/./.config/fontconfig ~/./.config/git ~/./.config/gtk-3.0 ~/./.config/gtk-4.0 ~/./.config/mpv ~/./.config/npmrc ~/./.config/openbox ~/./.config/shellcheckrc ~/./.config/user-dirs.dirs ~/./.config/zathura ~/./.gnupg ~/./.ignore ~/./.local/share/.archive ~/./.local/share/password-store ~/./.local/share/selectcmd.dat ~/./.local/share/systemd/user ~/./.local/share/zathura ~/./.mozilla/firefox/j8lriz5r.4/.archive ~/./.mozilla/firefox/j8lriz5r.4/bookmarkbackups ~/./.mozilla/firefox/j8lriz5r.4/bookmarks.html ~/./.mozilla/firefox/j8lriz5r.4/chrome ~/./.mozilla/firefox/j8lriz5r.4/persdict.dat ~/./.mozilla/firefox/j8lriz5r.4/sessionstore-backups ~/./.mozilla/firefox/j8lriz5r.4/user.js ~/./.ssh ~/./.xinitrc ~/./main ) # Files excluded from backups and mirrors. exclusions=( --exclude='*.asc' # OpenPGP keys exported for temporary use. --exclude='*.log' --exclude='*.log.*' # foo.log.2, foo.log.old, etc. --exclude='*.sqlite-journal' # Temporary files. May vanish. --exclude='*.sqlite-wal' # Write-ahead log files. --exclude='.build/*' --exclude='.cache/*' --exclude='.config/emacs/eln-cache/*' --exclude='.config/emacs/elpa/*' --exclude='.config/google-chrome/*' --exclude='.gnupg/.#*' # Lock files. https://forum.gnupg.org/t/4174 --exclude='.local/share/Steam/*' # Temporary exclusion. --exclude='.mozilla/firefox/*/datareporting/*' # Files may vanish. --exclude='.mozilla/firefox/*/storage/*' --exclude='.next/*' # Build directory for Next.js apps. --exclude='.wrangler/*' # Temporary files for projects that use Wrangler. --exclude='Maildir/*' # Temporary exclusion. --exclude='__pycache__/*' --exclude='build/*' --exclude='dist/*' # Build directory for Astro projects, etc. --exclude='joshscalisi.com/src/dotfiles/*' # Copies from ~/.config, etc. --exclude='joshscalisi.com/src/scripts/*' # Copies from ~/main/s/scripts. --exclude='joshscalisi.com/vendor/*' --exclude='node_modules/*' --exclude='pending/*' ) destinations=( /media/wd-25a2-2tb /media/wd-25e2-2tb-1 /media/wd-25e2-2tb-2 ) for dest in "${destinations[@]}"; do [[ -d $dest/backups ]] || die "directory not found: $dest/backups" latest_backup=$dest/backups/latest new_backup=$dest/backups/$(date +%Y/%m/%d%H%M%S) rsync_backup_opts=(-vaRHAXh --mkpath "${exclusions[@]}") # Enable hard linking of unchanged files in the new backup if the destination # has a previous backup. if [[ -d $latest_backup ]]; then rsync_backup_opts+=(--link-dest="$latest_backup") else echo "Previous backup not found: $latest_backup" read -rp 'Create a new full backup without hard links? ' input [[ $input =~ ^[Yy] ]] && break fi echo "Creating backup in $new_backup" rsync "${rsync_backup_opts[@]}" "${sources[@]}" "$new_backup" | grep -Ev '^$|/$|/.git/' # Don't print blank lines, directories, or Git files. ln -fns "$new_backup" "$latest_backup" echo echo "Syncing home directory to $dest/mirror" rsync -vaRHAXh --delete "${exclusions[@]}" ~/./ "$dest"/mirror | grep -E '^sent|^total' # Print summary only. echo done