#!/usr/bin/env bash # ───────────────────────────────────────────────────────────────────────────── # arcline-snapshot — btrfs snapshot manager (ships at /usr/local/sbin) # # arcline-snapshot snapshot take a snapshot of @ and @home # arcline-snapshot list list stored snapshots # arcline-snapshot prune [--keep N] keep N newest, delete the rest # # Snapshots are read-only copies under /.snapshots, so they survive a failed # upgrade. Run as root (or via sudo). Configured through the # arcline-snapshot.timer unit (see overlays/base/usr/lib/systemd/system/). # ───────────────────────────────────────────────────────────────────────────── set -euo pipefail ACTION="${1:-list}" SNAP_ROOT="/.snapshots" KEEP="${ARCLINE_SNAPSHOT_KEEP:-5}" TOP="/run/arcline-btrfs" require_root() { if [[ $EUID -ne 0 ]] && command -v sudo >/dev/null; then exec sudo -E "$0" "$@" elif [[ $EUID -ne 0 ]]; then echo "error: needs root" >&2; exit 1 fi } ROOT_FS_TYPE="$(findmnt -no FSTYPE / 2>/dev/null || true)" if [[ "$ROOT_FS_TYPE" != "btrfs" ]]; then echo "info: root filesystem is '$ROOT_FS_TYPE', not btrfs — snapshots disabled" exit 0 fi ROOT_DEV="$(findmnt -no SOURCE / | head -1)" mount_top() { mkdir -p "$TOP" mount -o subvolid=5,ro "$ROOT_DEV" "$TOP" trap 'umount "$TOP" 2>/dev/null || true; rmdir "$TOP" 2>/dev/null || true' EXIT } stamp() { date +%Y%m%dT%H%M%S; } case "$ACTION" in snapshot) require_root "$0" "$@" mount_top ts="$(stamp)" for sub in "@" "@home"; do [[ -d "$TOP/$sub" ]] || { echo "warn: subvolume $sub not found, skipping"; continue; } dst="$TOP/@snapshots/$ts-$sub" echo "snapshotting $sub → $dst" btrfs subvolume snapshot -r "$TOP/$sub" "$dst" >/dev/null done echo "snapshot taken: $ts" ;; list) require_root "$0" "$@" mount_top if ! compgen -G "$TOP/@snapshots/*-@*" >/dev/null; then echo "no snapshots yet" exit 0 fi for snap in "$TOP"/@snapshots/*-@*; do [[ -d "$snap" ]] || continue echo "$(basename "$snap")" done | sort -r ;; prune) require_root "$0" "$@" mount_top if [[ "${2:-}" == "--keep" ]]; then KEEP="${3:-$KEEP}"; fi mapfile -t snaps < <(for s in "$TOP"/@snapshots/*-@*; do [[ -d "$s" ]] && echo "$s"; done | sort -r) if [[ ${#snaps[@]} -le $KEEP ]]; then echo "nothing to prune (${#snaps[@]} ≤ keep $KEEP)" exit 0 fi for old in "${snaps[@]:$KEEP}"; do echo "deleting $(basename "$old")" btrfs subvolume delete "$old" >/dev/null done ;; *) echo "usage: arcline-snapshot {snapshot|list|prune [--keep N]}" >&2 exit 1 ;; esac