#!/usr/bin/env bash set -euo pipefail export PATH="/usr/sbin:/sbin:/usr/bin:/bin:$PATH" #################### CONFIG – EDIT THESE #################### # LUKS2 block device LUKS_DEVICE="/dev/disk/by-uuid/6659e638-d52a-4c32-9781-9dcedd44db35" # Name for the mapper device (will appear as /dev/mapper/${LUKS_NAME}) LUKS_NAME="shield" # Mount point for the decrypted filesystem LUKS_MOUNTPOINT="/mnt/shield" # Local folders to back up with rsync → LUKS volume BACKUP_ITEMS=( "$HOME/" "/media/.secrets/" ) # Machine identifier (destination subdir on the LUKS volume) MACHINE_DIR="T480" # rsync exclude patterns (passed via stdin to --exclude-from=-) # Paths are relative to each rsync source root (i.e. $HOME for the home backup). read -r -d '' RSYNC_EXCLUDES <<'EOF' || true # === Top-level junk === downloads/ Downloads/ tmp/ .tmp/ # === All caches (3.9G in .cache alone) === .cache/ # === Browsers (rebuilt on next launch) === .mozilla/ .mullvad-browser/ # === Shell / history / runtime state === .fasd .viminfo .zcompdump* .lesshst .sqlite_history .python_history .node_repl_history .wget-hsts .xsession-errors* .Xauthority .ICEauthority .dbus/ .pki/ .gvfs/ # === Trash === .local/share/Trash/ .Trash-*/ # === Claude Code old versions (884M, current one re-fetches) === .local/share/claude/versions/ # === Haskell / cabal (1.9G total, fully regenerable) === .cache/cabal/ .local/state/cabal/ # === Rust / cargo registry (152M, regenerable from Cargo.lock) === .cargo/registry/ .cargo/git/ # === Python: venvs, caches, bytecode (anywhere in tree) === __pycache__/ *.pyc *.pyo .mypy_cache/ .pytest_cache/ .ruff_cache/ .tox/ .venv/ venv/ .virtualenvs/ .local/share/pipx/ # === Node / JS === node_modules/ .npm/ .yarn/cache/ .pnpm-store/ # === Build artifacts in repos === build/ dist/ target/ .gradle/ .m2/repository/ # === LaTeX caches === .texlive*/texmf-var/ .texlive*/texmf-cache/ # === Java runtime junk === .java/ # === Notmuch index (668M, rebuild with `notmuch new`) === .local/share/mail/.notmuch/xapian/ # === Whisper.cpp models (754M, redownloadable) === git/whisper.cpp/models/ git/whisper.cpp/build/ # === Reinstallable third-party software in ~/opt === opt/tor-browser/ opt/monero-gui-*/ opt/Electrum-*/ # === Themes (509M, reinstall from upstream) === .themes/gruvbox-gtk-theme/ # === Crypto node chain data (re-syncs) === .bitmonero/ .shared-ringdb/ .ethereum/geth/chaindata/ .bitcoin/blocks/ .bitcoin/chainstate/ # === Electron / chat app caches === .config/Signal/Cache/ .config/Code/Cache/ .config/Code/CachedData/ .config/Code/logs/ # === Flatpak / Snap caches === .var/app/*/cache/ snap/*/common/.cache/ # === OS junk === .DS_Store Thumbs.db *.swp *.swo *~ core core.* EOF #################### INTERNALS – NO NEED TO EDIT ############ need_cmd() { command -v "$1" >/dev/null 2>&1 || { echo "ERROR: missing command: $1" >&2; exit 1; }; } cleanup() { echo echo ">>> Cleaning up..." if mountpoint -q "${LUKS_MOUNTPOINT}"; then echo ">>> Unmounting ${LUKS_MOUNTPOINT}..." sudo umount "${LUKS_MOUNTPOINT}" || true fi if [ -e "/dev/mapper/${LUKS_NAME}" ]; then echo ">>> Closing LUKS mapping ${LUKS_NAME}..." sudo cryptsetup close "${LUKS_NAME}" || true fi } trap cleanup EXIT need_cmd cryptsetup need_cmd rsync need_cmd mountpoint # Cache sudo credentials early sudo -v echo ">>> Ensuring mountpoint exists: ${LUKS_MOUNTPOINT}" sudo mkdir -p "${LUKS_MOUNTPOINT}" if [ -e "/dev/mapper/${LUKS_NAME}" ]; then echo ">>> WARNING: /dev/mapper/${LUKS_NAME} already exists, assuming already open." else echo ">>> Opening LUKS volume (interactive prompt)..." sudo cryptsetup open "${LUKS_DEVICE}" "${LUKS_NAME}" fi if [ ! -e "/dev/mapper/${LUKS_NAME}" ]; then echo "ERROR: /dev/mapper/${LUKS_NAME} not found after cryptsetup open." >&2 exit 1 fi if mountpoint -q "${LUKS_MOUNTPOINT}"; then echo ">>> WARNING: ${LUKS_MOUNTPOINT} already mounted." else echo ">>> Mounting /dev/mapper/${LUKS_NAME} on ${LUKS_MOUNTPOINT}..." sudo mount "/dev/mapper/${LUKS_NAME}" "${LUKS_MOUNTPOINT}" fi echo ">>> LUKS volume mounted at ${LUKS_MOUNTPOINT}" echo ">>> Starting rsync backups of local folders -> ${LUKS_MOUNTPOINT}/${MACHINE_DIR}/..." sudo mkdir -p "${LUKS_MOUNTPOINT}/${MACHINE_DIR}" for SRC in "${BACKUP_ITEMS[@]}"; do if [ ! -d "$SRC" ]; then echo ">>> WARNING: source folder does not exist, skipping: $SRC" continue fi case "$SRC" in "$HOME/"|"$HOME") RELDEST="${MACHINE_DIR}/HOME" ;; "/media/.secrets/"|"/media/.secrets") RELDEST="${MACHINE_DIR}/secrets" ;; *) NAME="$(basename "${SRC%/}")" RELDEST="${MACHINE_DIR}/${NAME}" ;; esac DEST="${LUKS_MOUNTPOINT}/${RELDEST}" sudo mkdir -p "${DEST}" echo ">>> rsync: ${SRC} -> ${DEST}" set +e sudo rsync -a --delete --progress \ --exclude-from=- \ "${SRC%/}/" \ "${DEST}/" <<< "${RSYNC_EXCLUDES}" RSYNC_RC=$? set -e case "${RSYNC_RC}" in 0) ;; 23|24) echo ">>> rsync finished with code ${RSYNC_RC} (some files skipped/vanished); continuing." ;; *) echo "ERROR: rsync failed with code ${RSYNC_RC} on ${SRC}" >&2 exit "${RSYNC_RC}" ;; esac done echo ">>> All rsync backups finished." echo ">>> Unmounting and closing LUKS (via trap)..."