#!/usr/bin/env bash # Back up $HOME into one verified .zip (for the NAS) before wiping a machine. # # curl -fsSL https://backup.srkn.me | bash # zip into ~/ (excluded from itself) # curl -fsSL https://backup.srkn.me | bash -s -- /mnt/usb # zip into another directory # # Excludes caches, trash, build artefacts, ~/Downloads etc. The exclude list is # embedded below (no network needed): rubo77/rsync-homedir-excludes (public # domain) plus nixbase additions. Nothing in $HOME is modified. # # GENERATED by nixbase scripts/publish-bootstrap from scripts/bootstrap/backup.sh.in # and scripts/bootstrap/backup-excludes/*.txt — edit those, not the served file. set -euo pipefail EXCLUDES_INFO="rubo77@7730cda + nixbase local.txt" red() { printf '\033[31m%s\033[0m\n' "$*"; } green() { printf '\033[32m%s\033[0m\n' "$*"; } bold() { printf '\033[1m%s\033[0m\n' "$*"; } ask() { # works when the script itself arrives on stdin (curl | bash); BACKUP_YES=1 skips [[ ${BACKUP_YES:-} == 1 ]] && return 0 local reply read -rp "$1 [y/N] " reply /dev/null || need+=("$t"); done if [[ ${#need[@]} -gt 0 ]]; then if command -v nix >/dev/null; then echo "Fetching missing tools via nix: ${need[*]}" pkgs=() for t in "${need[@]}"; do case $t in sha256sum) pkgs+=(nixpkgs#coreutils) ;; *) pkgs+=("nixpkgs#$t") ;; esac done paths=$(nix --extra-experimental-features "nix-command flakes" build --no-link --print-out-paths "${pkgs[@]}") for p in $paths; do PATH="$p/bin:$PATH"; done export PATH else red "Missing tools: ${need[*]} — install them (e.g. apt install rsync zip unzip) and rerun." exit 1 fi fi # --- target ------------------------------------------------------------------- src="$HOME" outdir="${1:-$HOME}" mkdir -p "$outdir" outdir=$(cd "$outdir" && pwd) stamp=$(date +%Y-%m-%d_%H%M) name="home-$(id -un)@$(hostname)-$stamp" zipfile="$outdir/$name.zip" work=$(mktemp -d) trap 'rm -rf "$work"' EXIT bold "== Backup of $src -> $zipfile" cat >"$work/excludes.txt" <<'NIXBASE_EXCLUDES' # upstream: rubo77/rsync-homedir-excludes@7730cda78ac74bb869764851d18380018c7fc3be (public domain) — do not edit, run: mise run excludes:update # A list of files to exclude when backing up *nix home directories using rsync. # Author: Ruben Barkow-Kuder # # #Usage: # USER= # rsync -aP --delete-during --exclude-from=rsync-homedir-excludes.txt /home/$USER/ /media/$USER/linuxbackup/home/$USER/ ############################################### # directories, probably not worth a backup # # (uncomment the files you don't need) # ############################################### #.android #.AndroidStudio*/ #Android/Sdk #.gradle #.gvm #.grails/ #.grails_history #.kde/share/apps/nepomuk #.local/share/notbit #.local/libvirt #.local/share/libvirt #.vagrant #.vagrant.d #.wine #.twister #/twister #Applications/eclipse* #/Downloads #*Popcorntime* #/.aMule #/Scribus #/VirtualBox* #.local/share/gnome-boxes/images #/Nextcloud #_build # ## Ruby gems #.rvm # ## Add Game Folders here: # #/games #.local/share/Steam #.var/app/com.valvesoftware.Steam/.local/share/Steam #.steam #/Steam #.minecraft #.technic #.PlayOnLinux #.local/share/lutris/runners/wine/ # ## Spotify #.config/spotify/Users #.var/app/com.spotify.Client/config/spotify/Users # ## those folders contain only temporary Videos I want to watch #/Videos #/Movies # ## some history files #.python_history #.bash_history #.zsh_history #.mysql_history # ## Python eggs #.local/lib/python* # ## Python virtual environments #PycharmProjects/**/venv ## pip packages inside any virtualenv - reinstallable, but ## restoring without them requires a requirements file #lib/python*/site-packages # ## Maven local repository #/.m2 # ## Kotlin/Native dependencies cache #.konan # ## Downloaded JDKs (Android Studio / JetBrains) #.jdks # ## NuGet package cache #.nuget/packages #.local/share/NuGet # ## Playwright browser binaries (re-downloadable) #.playwright-browsers # ## Unlocked Plasma Vaults #/Vaults ## Flatpak per-app data (~/.var/app) - the sandboxes can hold real ## user data (savegames, documents, app config); pure caches inside ## are already excluded, exclude this only if you don't need them #.var/app ## Thunderbird: regenerable profile data - IMAP copies, indexes ## and caches. Do NOT exclude Mail/Local Folders or POP accounts - ## that mail exists nowhere else! #ImapMail #*.msf #global-messages-db.sqlite* #cache2 ## Docker #.docker/machine ## Podman container storage (images and volumes, rebuildable) #.local/share/containers ################################################ # These directories may definitely be excluded # ################################################ # Contains mounted file systems: .gvfs .local/share/gvfs-metadata # contains the actual encrypted home directory /.Private # Session-specific: /.dbus .cache .var/app/*/cache .var/app/*/.cache /.Trash .Trash-1* .local/share/Trash /Trash # Cached info about audio CDs: .cddb # Cached packages lists: .aptitude # Node Package Manager cache .npm # Node modules - installed via npm/yarn, can be huge (100k+ files) node_modules # Python bytecode caches __pycache__ # Node Version Manager .nvm #Flash-specific: # Cache for flash, maybe others? .adobe # except for Flash persistence, there is no reason to keep this .macromedia #Files: # Contains errors from the current graphical session .xsession-errors .xsession-errors.old .wayland-errors # Recently used files .local/share/RecentDocuments .recently-used .recently-used.xbel recently-used.xbel .thumbnails .thumb Thumbs.db .DS_Store .localised .CFUserTextEncoding .zcompdump* .wget-hsts # Common Unix Printing System /.cups # run-time configuration information for Subversion /.subversion # Python virtual environments /.virtualenvs # Session-specific .Xauthority .ICEauthority .gksu.lock .pulse .pulse-cookie .esd_auth # Lock / Pid files .VeraCrypt-lock-* #KDE specific: # Recent documents on KDE .kde/share/apps/RecentDocuments .kde4/share/apps/RecentDocuments # Contains a history of the Klipper clipboard (KDE) .kde/share/apps/klipper .kde4/share/apps/klipper .local/share/klipper # You will loose saved scrolling positions of PDFs .kde/share/apps/okular/docdata .kde/share/apps/gwenview/recentfolders .kde4/share/apps/okular/docdata .kde4/share/apps/gwenview/recentfolders .local/share/okular/docdata .local/share/org.kde.gwenview/recentfolders # Cached other users' profile pics .kde/share/apps/kmess/displaypics .kde4/share/apps/kmess/displaypics # Cached emoticons of others .kde/share/apps/kmess/customemoticons .kde4/share/apps/kmess/customemoticons #Gnome specific: # tracker data files .local/share/tracker #Tor Browser supposed to not be backupped ;) .local/share/torbrowser #Firefox-specific (see also Profile folder): .mozilla/firefox/*/Cache # in case Fx crashes dumps will be stored in this .mozilla/firefox/*/minidumps # session-specific .mozilla/firefox/*/.parentlock # phishing database, recreated .mozilla/firefox/*/urlclassifier3.sqlite # blacklisted extensions .mozilla/firefox/*/blocklist.xml # extension database, recreated on startup .mozilla/firefox/*/extensions.sqlite .mozilla/firefox/*/extensions.sqlite-journal .mozilla/firefox/*/extensions.rdf .mozilla/firefox/*/extensions.ini .mozilla/firefox/*/extensions.cache # cached UI data, recreated .mozilla/firefox/*/XUL.mfasl .mozilla/firefox/*/XPC.mfasl .mozilla/firefox/*/xpti.dat .mozilla/firefox/*/compreg.dat # cached plugin data, recreated .mozilla/firefox/*/pluginreg.dat #SeaMonkey-specific (see also Profile folder): .mozilla/seamonkey/*/Cache # in case Fx crashes dumps will be stored in this .mozilla/seamonkey/*/minidumps # session-specific .mozilla/seamonkey/*/.parentlock # blacklisted extensions .mozilla/seamonkey/*/blocklist.xml # extension database, recreated on startup .mozilla/seamonkey/*/extensions.sqlite .mozilla/seamonkey/*/extensions.rdf .mozilla/seamonkey/*/extensions.ini # cached UI data, recreated .mozilla/seamonkey/*/xpti.dat .mozilla/seamonkey/*/compreg.dat # cached plugin data, recreated .mozilla/seamonkey/*/pluginreg.dat # Thunderbird Cache in your profile folder .thunderbird/*/Cache #Opera-specific (related question on Superuser.com: Is documentation available on files and directories in the Opera profile folder?): .opera/temporary_downloads .opera/cache .opera/thumbnails .opera/opcache .opera/icons .opera/application_cache .opera/widgets/*/cache .opera/lock # Keychain dir (manage SSH and GPG keys, front-end to ssh-agent and ssh-add) /.keychain #Komodo Edit: .komodoedit/*/codeintel/db .komodoedit/*/host-*/*/codeintel .komodoedit/*/XRE/Cache .komodoedit/*/XRE/.activatestate/komodo edit/Crash Reports .komodoedit/*/XRE/.activatestate/komodo edit/*/Cache .komodoedit/*/XRE/.activatestate/komodo edit/*/minidump .komodoedit/*/XRE/.parentlock .komodoedit/*/XRE/extensions.rdf .komodoedit/*/XRE/extensions.ini .komodoedit/*/XRE/extensions.cache .komodoedit/*/XRE/XPC.mfasl .komodoedit/*/XRE/XUL.mfasl .komodoedit/*/XRE/xpti.dat .komodoedit/*/XRE/pluginreg.dat .komodoedit/*/XRE/compreg.dat .komodoedit/*/XRE/*.sqlite-journal .komodoedit/*/pystdout.log .komodoedit/*/pystderr.log .komodoedit/*/history.sqlite.bak .komodoedit/*/running.lock .komodoedit/*/mutex.lock .komodoedit/*/*.xmlc .komodoedit/*/startup-env.tmp .komodoedit/*/commandments.fifo .komodoedit/*/history.sqlite #GnuPG: .gnupg/rnd .gnupg/random_seed .gnupg/.#* .gnupg/*.lock .gnupg/gpg-agent-info-* #Google Earth: .googleearth/Temp .googleearth/Cache #Google Chrome: .config/google-chrome/ShaderCache .config/google-chrome/*/Local Storage .config/google-chrome/*/Session Storage .config/google-chrome/*/Application Cache .config/google-chrome/*/History Index * .config/google-chrome/*/Service Worker/CacheStorage .config/google-chrome/*/WebStorage/*/CacheStorage #Chromium: .config/chromium/*/Local Storage .config/chromium/*/Session Storage .config/chromium/*/Service Worker/CacheStorage .config/chromium/*/Application Cache .config/chromium/*/History Index * /snap/chromium/common/.cache /snap/chromium/*/.config/chromium/*/Service Worker/CacheStorage /snap/chromium/*/.local/share/ #Riot /snap/riot-web/ #Brave .config/BraveSoftware/Brave-Browser/*/Feature Engagement Tracker/ .config/BraveSoftware/Brave-Browser/*/Local Storage/ .config/BraveSoftware/Brave-Browser/*/Service Worker/CacheStorage/ .config/BraveSoftware/Brave-Browser/*/Session Storage/ .config/BraveSoftware/Brave-Browser/Safe Browsing/ .config/BraveSoftware/Brave-Browser/ShaderCache/ #Local repositories: .gradle/caches /.m2/repository #indexer .local/share/baloo .local/share/zeitgeist .local/share/akonadi #Other apps: # Pidgin .purple/icons # Cached applets .guayadeque/cache.db .java/deployment/cache .icedteaplugin .icedtea .gnome2/epiphany/favicon_cache # other temporary files nohup.out # LibreOffice cache files .config/libreoffice/4/cache # freshplayerplugin LSO .config/freshwrapper-data/Shockwave Flash/WritableRoot/#SharedObjects # Snap caches /snap/*/common/.cache .config/*/Cache # Atom .config/Atom/*Cache .config/Atom/IndexedDB .atom/compile-cache/ # Vim .vim/bundle/* .viminfo # VS Code .vscode-oss/extensions/* .vscode/extensions/* .config/Code - OSS/logs/ .config/Code/logs/ .config/Code/CachedData/* # VS Codium .config/VSCodium/CachedExtensionVSIXs # Cursor (VS Code fork) .cursor/extensions/* .config/Cursor/logs/ # Codeium / Windsurf assistant data - databases and embedded # browser state, rebuilt on use (keep .codeium/*/memories) .codeium/*/database .codeium/*/ws-browser .codeium/*/ws-browser-profile .codeium/*/cascade # Devin / Windsurf (Cognition) - extension binaries, old CLI # versions and logs, all re-downloadable .devin/extensions/* .windsurf/extensions/* .local/share/devin/cli/_versions .local/share/devin/cli/logs .config/Devin/logs/ .config/Windsurf/logs/ # VirtualBox .config/VirtualBox/VBoxSVC.log* .config/VirtualBox/VBoxGuestAdditions_* # Rust artefacts .cargo .rustup # Erlang artefacts .kerl # golang Cache go/pkg/mod/cache # ccache, remove cache but keep config .ccache/? .ccache/tmp # Nvidia OpenGL Shader Cache .nv/GLCache # Citrix ICA Client .ICAClient/logs .ICAClient/.tmp .ICAClient/cache/zlcache # Geany IDE - socket file .config/geany/geany_socket_* # LBRY .config/lbry .local/share/lbry/lbrynet # Electron Apps .config/**/blob_storage .config/**/Application Cache .config/**/Cache .config/**/CachedData .config/**/Code Cache .config/**/GPUCache .config/**/Service Worker/CacheStorage .var/app/**/blob_storage .var/app/**/Application Cache .var/app/**/Cache .var/app/**/CachedData .var/app/**/Code Cache .var/app/**/GPUCache .var/app/**/Service Worker/CacheStorage # SC-Controller - socket file .config/scc/daemon.socket # nixbase additions to the upstream list (rsync --exclude-from syntax, relative to $HOME). # A leading "/" anchors at the top of $HOME. Edit freely, then: scripts/publish-bootstrap # usually junk, re-downloadable /Downloads # reinstallable runtimes / package stores .local/share/mise/installs .local/share/mise/downloads .local/share/pnpm/store .deno .ollama/models # container and VM state (images can be pulled again) .local/share/containers .local/share/docker # remote editor servers .vscode-server .cursor-server # Nix / home-manager generated state .nix-profile .nix-defexpr .local/state/nix .local/state/home-manager .direnv result # app caches the upstream list misses .kube/cache .config/discord/Cache .config/discord/Code Cache .config/obsidian/Cache .config/Slack/Cache NIXBASE_EXCLUDES # never back up previous backups / the output itself printf '/home-*@*.zip\n/home-*@*.sha256\n/home-*@*.files.txt\n' >>"$work/excludes.txt" case "$outdir/" in "$src"/*) rel="${outdir#"$src"}"; [[ -n $rel ]] && printf '%s/\n' "$rel" >>"$work/excludes.txt" ;; esac echo "Exclude rules: $(grep -cv '^\s*\(#\|$\)' "$work/excludes.txt") ($EXCLUDES_INFO)" # --- what would be backed up ---------------------------------------------------- mkdir "$work/dest" cd "$src" rsync -a --dry-run --exclude-from="$work/excludes.txt" --out-format='%n' ./ "$work/dest/" 2>"$work/rsync.err" | grep -v '^\./$' >"$work/files.txt" || true if grep -q . "$work/rsync.err"; then red "rsync reported problems (unreadable files are skipped):" head -20 "$work/rsync.err" fi # sizes of the selected files only (directories would pull in excluded content) grep -v '/$' "$work/files.txt" | tr '\n' '\0' | du -b --apparent-size --files0-from=- 2>/dev/null >"$work/sizes.txt" || true nfiles=$(wc -l <"$work/sizes.txt") bytes=$(awk -F'\t' '{s+=$1} END {printf "%d", s}' "$work/sizes.txt") free=$(df -B1 --output=avail "$outdir" | tail -1) echo bold "Files: $nfiles Size (uncompressed): $(numfmt --to=iec --suffix=B "$bytes") Free in $outdir: $(numfmt --to=iec --suffix=B "$free")" echo "Biggest top-level entries:" awk -F'\t' '{split($2, p, "/"); s[p[1]] += $1} END {for (k in s) printf "%d\t%s\n", s[k], k}' "$work/sizes.txt" | sort -rn | head -15 | numfmt --to=iec --suffix=B --field=1 | sed 's/^/ /' if [[ ${bytes:-0} -gt $free ]]; then red "Probably not enough free space in $outdir (zip may compress, but media won't shrink)." fi echo ask "Create $zipfile?" || { echo "aborted"; exit 1; } # --- zip ------------------------------------------------------------------------ # -y keep symlinks, -n store already-compressed formats as-is, -@ names from stdin zip -q -y -@ \ -n .zip:.gz:.xz:.zst:.bz2:.7z:.rar:.jpg:.jpeg:.png:.webp:.heic:.mp4:.mkv:.mov:.webm:.mp3:.flac:.ogg:.m4a:.pdf:.iso:.qcow2 \ "$zipfile" <"$work/files.txt" bold "== Verifying archive" unzip -tq "$zipfile" (cd "$outdir" && sha256sum "$name.zip" >"$name.sha256") cp "$work/files.txt" "$outdir/$name.files.txt" green "OK: $zipfile ($(du -h "$zipfile" | cut -f1)), checksum in $name.sha256, file list in $name.files.txt" # --- things a home backup does not cover ---------------------------------------- echo bold "== Not in this backup — check before formatting" dirty=0 command -v git >/dev/null || dirty=skip [[ $dirty == skip ]] && echo " git: not installed, repo check skipped" [[ $dirty == skip ]] || while IFS= read -r -d '' g; do repo=$(dirname "$g") st=$(git -C "$repo" status --porcelain 2>/dev/null | wc -l) ahead=$(git -C "$repo" log --branches --not --remotes --oneline 2>/dev/null | wc -l) if [[ $st -gt 0 || $ahead -gt 0 ]]; then echo " git: ${repo#"$src"/} (uncommitted: $st, unpushed commits: $ahead) — included in zip, but not on any remote" dirty=1 fi done < <(find "$src" -maxdepth 4 -name .git -type d -not -path '*/.cache/*' -print0 2>/dev/null) [[ $dirty == 0 ]] && echo " git: all repos below ~ (depth 4) are clean and pushed" for d in /var/lib/docker/volumes /var/lib/libvirt/images /var/lib/containers; do if [[ -d $d ]] && [[ -n $(ls -A "$d" 2>/dev/null) ]]; then echo " $d is not empty (outside \$HOME, not backed up)" fi done if command -v docker >/dev/null && docker volume ls -q >/dev/null 2>&1; then n=$(docker volume ls -q | wc -l) [[ $n -gt 0 ]] && echo " docker: $n volumes (outside \$HOME, not backed up)" fi echo " Excluded on purpose: caches, trash, browser caches, node_modules, toolchains (see rubo77 list)." echo echo "Copy to the NAS, then verify there: sha256sum -c $name.sha256"