diff --git a/00_README.md b/00_README.md new file mode 100644 index 0000000..dcc3311 --- /dev/null +++ b/00_README.md @@ -0,0 +1,203 @@ +# README – Snapshot-Backup mit rsync + +Dieses Skript erstellt Snapshots des Home-Verzeichnisses mit `rsync` und `--link-dest`.[cite:43][cite:34] Jeder Snapshot sieht wie ein vollständiges Backup aus, aber unveränderte Dateien werden per Hardlink wiederverwendet, sodass Speicherplatz gespart wird.[cite:34][cite:92] + +## Zweck + +Das Skript sichert den Inhalt von `HOME` auf ein externes Zielverzeichnis und legt dabei zeitgestempelte Snapshot-Ordner an.[cite:43] Über den Symlink `latest` wird immer auf den zuletzt erfolgreichen Sicherungsstand verwiesen.[cite:34][cite:92] + +Typische Vorteile dieses Ansatzes: + +- Einfach browsebare Backups, weil jeder Snapshot als normales Verzeichnis vorliegt.[cite:43][cite:91] +- Platzersparnis durch Hardlinks mit `--link-dest`.[cite:34][cite:92] +- Aufbewahrung mehrerer Sicherungsstände durch Rotation älterer Snapshots.[cite:43] + +## Voraussetzungen + +Für `--link-dest` sollte das Ziel auf einem Linux-Dateisystem liegen, das Hardlinks sauber unterstützt, etwa ext4, XFS oder btrfs.[cite:34][cite:91] Dateisysteme wie exFAT sind für diese Snapshot-Technik ungeeignet.[cite:34] + +Benötigte Werkzeuge: + +- `bash` +- `rsync` +- `readlink` +- `tee` +- Standard-Tools wie `mkdir`, `ln`, `ls`, `tail`, `rm` + +## Verzeichnisaufbau + +Das Skript erzeugt pro Lauf einen neuen Snapshot mit Zeitstempel.[cite:43][cite:92] + +Beispiel: + +```text +/run/media/hans/usbsicherung/jacboy_sicherung/ +└── / + ├── 2026-04-28_10-30-00/ + ├── 2026-04-29_10-30-00/ + ├── latest -> /run/media/hans/usbsicherung/jacboy_sicherung//2026-04-29_10-30-00 + └── logs/ +``` + +## Funktionsweise + +Das Skript prüft zunächst, ob das Zielverzeichnis existiert und beschreibbar ist, und legt anschließend Snapshot- und Log-Verzeichnisse an.[cite:43][cite:97] Danach wird `rsync` mit Archivoptionen sowie Ausschlüssen für nicht benötigte Verzeichnisse ausgeführt.[cite:43][cite:91] + +Wenn bereits ein gültiger vorheriger Snapshot existiert, wird dieser über `--link-dest` als Referenz verwendet.[cite:34][cite:92] Unveränderte Dateien werden dann nicht erneut kopiert, sondern als Hardlinks im neuen Snapshot eingebunden.[cite:34] + +Nach erfolgreichem Lauf aktualisiert das Skript den Symlink `latest` und entfernt ältere Snapshots oberhalb des konfigurierten Aufbewahrungswerts `KEEP`.[cite:43] + +## Verwendete rsync-Optionen + +| Option | Bedeutung | +|---|---| +| `-a` | Archivmodus: rekursiv kopieren und Metadaten erhalten.[cite:91][cite:43] | +| `-H` | Hardlinks in der Quelle erhalten.[cite:91] | +| `-A` | ACLs mitnehmen, sofern vorhanden.[cite:91] | +| `-X` | Erweiterte Attribute mitnehmen.[cite:91] | +| `-x` | Auf demselben Dateisystem bleiben; zusätzliche Mounts unterhalb von `HOME` werden nicht traversiert.[cite:91][cite:43] | +| `--delete` | Entfernt Dateien im neuen Snapshot, die in der Quelle nicht mehr vorhanden sind; ältere Snapshots bleiben dabei unberührt.[cite:91][cite:34] | +| `--delete-excluded` | Entfernt auch explizit ausgeschlossene Inhalte aus dem Ziel-Snapshot.[cite:91] | +| `--link-dest` | Verweist auf den letzten Snapshot, damit unveränderte Dateien per Hardlink wiederverwendet werden.[cite:34][cite:92] | + +## Anpassbare Variablen + +Die wichtigsten Variablen stehen am Anfang des Skripts: + +```bash +SRC="${HOME}/" +DEST_BASE="/run/media/hans/usbsicherung/jacboy_sicherung" +KEEP=30 +``` + +- `SRC`: Quellverzeichnis, standardmäßig das eigene Home-Verzeichnis. +- `DEST_BASE`: Basisverzeichnis auf dem Backup-Medium. +- `KEEP`: Anzahl der Snapshots, die aufbewahrt werden sollen. + +Auch die Ausschlussliste kann direkt im Array `EXCLUDES` angepasst werden, zum Beispiel für Cache-, Download- oder temporäre Verzeichnisse.[cite:43] + +## Verwendung + +Syntax prüfen: + +```bash +bash -n sicherung.sh +``` + +Skript ausführen: + +```bash +bash sicherung.sh +``` + +Für einen sicheren Test kann vorübergehend `--dry-run` in die `RSYNC_OPTS` aufgenommen werden; dann zeigt `rsync` nur an, was passieren würde, ohne tatsächlich Daten zu schreiben.[cite:91][cite:43] + +## Beispielskript + +```bash +#!/usr/bin/env bash +set -Eeuo pipefail + +SRC="${HOME}/" +DEST_BASE="/run/media/hans/usbsicherung/jacboy_sicherung" +HOST="$(hostname -s)" +STAMP="$(date +%F_%H-%M-%S)" +SNAPSHOT_DIR="${DEST_BASE}/${HOST}/${STAMP}" +LAST_LINK="${DEST_BASE}/${HOST}/latest" +LOG_DIR="${DEST_BASE}/${HOST}/logs" +LOG_FILE="${LOG_DIR}/backup-${STAMP}.log" +KEEP=30 + +log() { + local msg="[$(date +%F\ %T)] $*" + echo "${msg}" + if [[ -n "${LOG_FILE:-}" ]]; then + mkdir -p "$(dirname "${LOG_FILE}")" + echo "${msg}" >> "${LOG_FILE}" + fi +} + +fail() { + log "FEHLER: $*" + exit 1 +} + +if [[ ! -d "${DEST_BASE}" ]]; then + fail "Zielbasis ${DEST_BASE} existiert nicht oder ist nicht gemountet. Bitte USB-Laufwerk prüfen." +fi + +if [[ ! -w "${DEST_BASE}" ]]; then + fail "Zielbasis ${DEST_BASE} ist nicht beschreibbar. Rechte/Mount prüfen." +fi + +mkdir -p "${SNAPSHOT_DIR}" "${LOG_DIR}" + +log "Starte Backup: ${SRC} -> ${SNAPSHOT_DIR}" + +RSYNC_OPTS=( + -aHAXx + --numeric-ids + --delete + --delete-excluded + --info=stats2,progress2 + --human-readable + --partial +) + +EXCLUDES=( + --exclude=".cache/" + --exclude="Downloads/" + --exclude=".local/share/Trash/" + --exclude=".gvfs/" + --exclude=".dotnet/" + --exclude=".codegpt/" + --exclude=".copilot/" + --exclude=".var/" + --exclude=".vscode/" + --exclude="temp/" +) + +if [[ -L "${LAST_LINK}" ]] && [[ -d "$(readlink -f "${LAST_LINK}")" ]]; then + RSYNC_OPTS+=(--link-dest="$(readlink -f "${LAST_LINK}")") + log "Verwende link-dest: $(readlink -f "${LAST_LINK}")" +else + log "Kein gültiger letzter Snapshot gefunden – es wird ein vollständiger Lauf erstellt." +fi + +if rsync "${RSYNC_OPTS[@]}" \ + "${EXCLUDES[@]}" \ + "${SRC}" "${SNAPSHOT_DIR}/" | tee -a "${LOG_FILE}" +then + log "rsync erfolgreich abgeschlossen." +else + fail "rsync ist mit einem Fehler beendet worden." +fi + +ln -sfn "${SNAPSHOT_DIR}" "${LAST_LINK}" +log "latest-Link zeigt jetzt auf ${SNAPSHOT_DIR}" + +cd "${DEST_BASE}/${HOST}" +SNAPS=(20*/) + +if (( ${#SNAPS[@]} > KEEP )); then + log "Rotationslauf: Es existieren ${#SNAPS[@]} Snapshots, KEEP=${KEEP}." + ls -1dt 20*/ | tail -n +$((KEEP + 1)) | while read -r old; do + log "Lösche alten Snapshot: ${old}" + rm -rf -- "${old}" + done +else + log "Rotationslauf: Es müssen keine alten Snapshots gelöscht werden (Anzahl: ${#SNAPS[@]}, KEEP=${KEEP})." +fi + +log "Backup abgeschlossen." +``` + +## Sicherheitshinweise + +Ein erster Testlauf sollte als Dry-Run erfolgen, um Ausschlüsse, Zielpfad und Verhalten zu prüfen, bevor echte Daten geschrieben oder gelöscht werden.[cite:91][cite:43] Das Zielverzeichnis sollte nicht innerhalb des zu sichernden Home-Verzeichnisses liegen, um Rekursionen und ungewollte Mitsicherungen zu vermeiden.[cite:43] + +Bei Verwendung von `--delete` gilt: Gelöschte Dateien verschwinden aus dem neu erzeugten Snapshot, bleiben aber in älteren Snapshots weiterhin vorhanden.[cite:34][cite:92] Genau dadurch entsteht die gewünschte Snapshot-Historie.[cite:92] + +## Mögliche Erweiterungen + +Sinnvolle nächste Ausbaustufen wären ein optionaler `--dry-run`-Schalter, eine Prüfung des tatsächlichen Mountpoints per `mountpoint`, sowie eine Automatisierung über `systemd`-Timer statt manueller Ausführung.[cite:97][cite:43]