Nach den letzten Artikeln über das launchd-Framework und rsync, werden nun beide Ansätze kombiniert, um ein Backup-Skript zu erstellen. Dieses Skript synchronisiert einen Ordner auf das Synology NAS und sichert zusätzliche gelöschte Dateien vom Quellordner in Backup-Ordner auf dem NAS. Als Vorbereitung muss für die Verwendung des rsync-Befehls SSH auf dem Mac und auf der Synology eingerichtet werden, was im vorherigen Artikel ausführlich beschrieben wurde.
Anforderungen
Im Artikel Automatische Sicherung von Mac-Ordnern auf ein Synology NAS habe ich meine Lösung beschrieben, wie ich mit einem Klick 1:1 Kopien meines Obsidian Vaults mit auf meiner Synology NAS erstellen kann.In diesem Artikel gehe ich einen Schritt weiter und stelle eine Lösung vor, wie dies mit rsync und launchd automatisch und periodisch geschehen kann, wobei zusätzlich eine Kopie von eventuell im Quellordner gelöschten Dateien separat gespeichert wird. Der entsprechende Backup-Ordner auf dem NAS sollte folgende Struktur haben:
ObsBackup/
├── 2024-06-08_16-06-49
├── 2024-06-08_17-06-49
├── 2024-06-08_18-06-49
└── current
Im Ordner “current” enthält die aktuelle 1:1 Kopie des Quellordners. In Ordnern, die nach Datum und Uhrzeit benannt sind (in der Form YYYY-MM-DD_HH-MM-SS), werden die Dateien gespeichert, die während seit dem letzten Backup-Laufs im Quellordner gelöscht wurden. Die Anzahl dieser Ordner soll begrenzt werden, so dass die ältesten Ordner automatisch gelöscht werden, wenn eine bestimmte Anzahl überschritten wird.
Ein Backup-Lauf soll nur dann gestartet werden, wenn das NAS erreichtbar ist, d.h. wenn sich der Mac im Heimnetz befindet und das Synology NAS läuft.
Lösung
Das Skript ist ähnlich aufgebaut wie das in meinem rsync-Artikel, jedoch mit einigen Ergänzungen:
- 1. Zunächst wird der Zeitstempel in die Logdatei geschrieben.
- 2. Danach wird geprüft, ob der NAS-Server erreichbar ist.
- 3. Ist der Server erreichbar, wird der Backup-Befehl ausgeführt und das Ergebnis in die Logdatei geschrieben.
- 4. Anschließend wird überprüft, wie viele Ordner mit gelöschten Dateien vorhanden sind.
- 5. Wenn eine bestimmte Anzahl überschritten ist, wird der älteste Ordner gelöscht und dies im Log vermerkt.
- 6. Ist der NAS-Server nicht erreichbar, wird ein entsprechender Eintrag ins Log geschrieben.
- 7. In beiden Fällen wird das Skript danach beendet.
Das gesamte Skript sieht in meinem Beispiel so aus:
#!/bin/zsh # Füge einen Zeitstempel zur Protokolldatei hinzu echo -e "\n\n$(date +"%Y-%m-%d %H:%M:%S")" >> /tmp/ObsBackup.log # Variablen SOURCE="/Users/leif/Documents/01 Meine Dokumente/ObsidianNotes/" DESTINATION="synology:/volume1/homes/leif/ObsBackup/current" BACKUP_DIR_BASE="/volume1/homes/leif/ObsBackup" MAX_BACKUPS=30 TIMESTAMP=$(date +%Y-%m-%d_%H-%M-%S) BACKUP_DIR="${BACKUP_DIR_BASE}/${TIMESTAMP}" # Teste die Erreichbarkeit des Servers if ping -c 1 192.168.1.80 &> /dev/null then # Rsync mit --delete und --backup, Ausschluss der @eaDir und .DS_Store Ordner rsync -avz --delete --backup --backup-dir="${BACKUP_DIR}" --exclude='.DS_Store' --filter 'P @eaDir/' -e ssh "$SOURCE" "$DESTINATION" >> /tmp/ObsBackup.log 2>&1 # Überprüfe die Anzahl der Backups und lösche die ältesten, wenn mehr als MAX_BACKUPS vorhanden sind BACKUP_COUNT=$(ssh synology "ls -1 ${BACKUP_DIR_BASE} | grep -E '^[0-9]{4}-[0-9]{2}-[0-9]{2}_[0-9]{2}-[0-9]{2}-[0-9]{2}$' | wc -l") if [ "$BACKUP_COUNT" -gt "$MAX_BACKUPS" ]; then echo "Anzahl der Backups ($BACKUP_COUNT) überschreitet das Maximum ($MAX_BACKUPS). Älteste Backups werden gelöscht." >> /tmp/ObsBackup.log ssh synology "ls -1 ${BACKUP_DIR_BASE} | grep -E '^[0-9]{4}-[0-9]{2}-[0-9]{2}_[0-9]{2}-[0-9]{2}-[0-9]{2}$' | sort | head -n -${MAX_BACKUPS} | xargs -I {} rm -rf ${BACKUP_DIR_BASE}/{}" >> /tmp/ObsBackup.log 2>&1 fi else echo "SERVER ist nicht erreichbar. Backup wird nicht ausgeführt." >> /tmp/ObsBackup.log fi
Mit dem Befehl echo wird der Zeitstempel in die Logdatei /tmp/ObsBackup.log
geschrieben (>>
), wobei der Eintrag zunächst mit zwei Leerzeilen getrennt wird (\n\n). Die Konstruktion $(date +"%Y-%m-%d %H:%M:%S")
fügt das aktuelle Datum und die Uhrzeit im Format YYYY-MM-DD HH:MM:SS ein.
# Füge einen Zeitstempel zur Protokolldatei hinzu echo -e "\n\n$(date +"%Y-%m-%d %H:%M:%S")" >> /tmp/ObsBackup.log
Danach werden die Variablen definiert, die das Quellverzeichnis auf dem Mac, das Zielverzeichnis auf der Synology sowie das Zielverzeichnis der Backup-Ordner für die gelöschten Dateien festlegen. In der Variable MAX_BACKUPS wird die Anzahl der Backup-Ordner für die gelöschten Dateien definiert. Dieser Wert hängt davon ab, wie häufig Dateien geändert werden und wie oft das Skript später ausgeführt wird.
Der Aufrufparameter für meine Synology in der Variable DESTINATION
ist synology:
, so wie er in der config Datei in meinem .ssh Verzeichnis definiert wurde.
# Variablen SOURCE="/Users/leif/Documents/01 Meine Dokumente/ObsidianNotes/" DESTINATION="synology:/volume1/homes/leif/ObsBackup/current" BACKUP_DIR_BASE="/volume1/homes/leif/ObsBackup" MAX_BACKUPS=30
Wie erwähnt, wird bei jedem Durchlauf, bei einer Änderung im Quellordner, ein Backup-Ordner erstellt. Ich habe hier neben dem Datum und der Uhrzeit auch noch die Sekunden als Namensbestandteil aufgenommen, da dies bei meinen Testläufen sehr praktisch war. Das BACK_DIR
wird aus dem definierten BACKUP_DIR_BASE
, einem/
und dem TIMESTAMP
zusammengesetzt.
TIMESTAMP=$(date +%Y-%m-%d_%H-%M-%S) BACKUP_DIR="${BACKUP_DIR_BASE}/${TIMESTAMP}"
Um sicherzustellen, dass das rsync-Kommando nur aufgerufen wird, wenn das NAS erreichbar ist, wird mit ping
ein Paket an den Server geschickt. Die Option -c 1
stellt sicher, dass nur ein Paket gesendet wird. Wenn das NAS auf den ping
antwortet, wird dies als true
für das if-Statement gewertet und der then
-Teil, also die rsync
-Komanndozeile, ausgeführt.
# Teste die Erreichbarkeit des Servers if ping -c 1 192.168.1.80 &> /dev/null then # Rsync mit --delete und --backup, Ausschluss der @eaDir und .DS_Store Ordner rsync -avz --delete --backup --backup-dir="${BACKUP_DIR}" --exclude='.DS_Store' --filter 'P @eaDir/' -e ssh "$SOURCE" "$DESTINATION" >> /tmp/ObsBackup.log 2>&1
Die Kommandozeile setzte sich dann aus den folgenden Optionen zusammen:
-avz: Eine Kombination aus der Archive-Option (-a), die definiert, dass alle Daten gesichert werden sollen, der Verbose-Option (-v), die für eine ausführlichere Ausgabe sorgt, und der Kompressionsoption (-z), die rsync anweist, die Daten für die Übertragung zu komprimieren. Letzteres ist in einem internen Netz und bei wenigen Änderungen wahrscheinlich nicht notwendig.
--delete
: Sorgt dafür, dass alle Daten, die im Quellverzeichnis seit dem letzten Aufruf gelöscht wurden, auch im Zielordner gelöscht werden. Im Ordner current
befindet sich so eine exakte Kopie des Ordners auf dem Mac.
--backup
: Definiert, dass gelöschte Dateien gesichert werden.
--backup-dir="${BACKUP_DIR}"
: Gibt das Verzeichnis an, in dem die Backups gespeichert werden sollen.
--exclude='.DS_Store'
: Schließt die .DS_Store-Dateien auf dem Mac von der Synchronisation aus.
--filter 'P @eaDir/'
: Verhindert das wiederholte Löschen des @eaDir-Verzeichnis auf dem Synology NAS, das die Informationen für die „Universal Search” enthält
-e ssh
: Definiert das Protokoll, das rsync
für die Übertragung nutzt, hier SSH
.
"$SOURCE" "$DESTINATION"
: Die oben definierten Quell- und Zielpfade.
» /tmp/ObsBackup.log 2>&1: Sorgt dafür, dass die Ausgabe und eventuelle Fehler in die Protokolldatei /tmp/ObsBackup.log
umgeleitet wird.
Nun folgt etwas Unix-Pipe-Magie, die ich nicht im Detail durchdringe. Hier wird die Variable BACKUP_COUNT mit dem Ergebnis eines Kommandos belegt, das per ssh
auf der Synology ausgeführt wird. Der Aufruf listet alle Ordner im Verzeichnis BACKUP_DIR_BASE auf der Synology auf, filtert diejenigen heraus, die nicht dem Namensschema für die Backup-Dateien entsprechen (also den Ordner current), und zählt die verbleibenden (wc -l
).
BACKUP_COUNT=$(ssh synology "ls -1 ${BACKUP_DIR_BASE} | grep -E '^[0-9]{4}-[0-9]{2}-[0-9]{2}_[0-9]{2}-[0-9]{2}-[0-9]{2}$' | wc -l")
Wenn der BACKUP_COUNT größer (-gt
greater then) als der oben definierte MAX_BACKUPS
Wert wird, dann wird eine entsprechende Meldung in die Logdatei geschrieben:
echo "Anzahl der Backups ($BACKUP_COUNT) überschreitet das Maximum ($MAX_BACKUPS). Älteste Backups werden gelöscht." >> /tmp/ObsBackup.log
Danach passiert noch weitere Unix-Pipe-Magie: mit einem Befehl, der wiederum via ssh
auf der Synology ausgeführt wird, werden die Backup-Ordner gelistet, nach Namen sortiert und dann der überzählige und älteste Ordner gelöscht. Eine entsprechende Meldung wird ebenfalls in die Logdatei geschrieben:
if [ "$BACKUP_COUNT" -gt "$MAX_BACKUPS" ]; then echo "Anzahl der Backups ($BACKUP_COUNT) überschreitet das Maximum ($MAX_BACKUPS). Älteste Backups werden gelöscht." >> /tmp/ObsBackup.log ssh synology "ls -1 ${BACKUP_DIR_BASE} | grep -E '^[0-9]{4}-[0-9]{2}-[0-9]{2}_[0-9]{2}-[0-9]{2}-[0-9]{2}$' | sort | head -n -${MAX_BACKUPS} | xargs -I {} rm -rf ${BACKUP_DIR_BASE}/{}" >> /tmp/ObsBackup.log 2>&1 fi
Am Ende wird in dem else
-Teil der des if-Konstruktes, die Nicht.Erreichbarkeit des Servers in der Logdatei dokumentiert:
else echo "SERVER ist nicht erreichbar. Backup wird nicht ausgeführt." >> /tmp/ObsBackup.log fi
Skript speichern und testen
Nun sollte das Shell-Skript getestet werden. Dazu wird es in gespeichert und mit der Ausführungsberechtigung versehen. In diesem Beispiel speicher ich das Skript auf meinem Scrheibtisch mit dem Namen Backup.sh
und füge die Ausführungsberechtigung mit dem folgenden Befehl hinzu:
chmod +x ~/Desktop/Backup.sh
Nun kann das Skript aufrufen und getestet werden:
~/Dektop/Backup.sh
Ergebnisse überprüfen
Wenn alles gut läuft, dann ist der als Quelle definierte Ordner nun auf die Synology kopiert. Ein Blick in die Logdatei sollte in diesem Fall keine Fehler anzeigen. Wenn der Quell-Ordner viele Dateien enthält, wird die Übertragung jeder einzelnen Datei angezeigt, wodurch die Logdatei ziemlich lang werden kann.
Ich empfehle, den Inhalt der Logdatei nach dem ersten erfolgreichen Lauf zu löschen, damit man später nicht so weit in der Datei blättern muss. Später werden normalerweise nur wenige Änderungen gemacht, sodass die Logdatei übersichtlich bleibt.
Die Automator-Hülle
Wie erwähnt, kann ein Shell-Skript, das Dateien manipuliert, nicht ohne Weiteres in mit launchd
gestartet werden. Das gilt übrigens auch für das Ausführen von Shell-Skripten in Kurzbefehlen. Nur Programmen kann in den „Daten und Sicherheit” Einstellungen die Berechtigung zum Zugriff auf Ordner oder der Festplatte gewährt werden. Daher muss das Skript in eine Programm-Hülle gepackt werden. Wie ich in dem vorherigen Artikel schon erwähnt habe, kann dies u.a. mit dem Automator erledigt werden.
Dazu wird nach dem Starten des Automators ein neues Dokument angelegt und dann als Art des Dokumentes, „Programm” ausgewählt.
Nachdem das Dokumentfenster geöffnet wurde, wird die Aktion “Shell-Skript ausführen” in den Programmierbereich gezogen. Das Ganze wird dann abgespeichert. Ich verwende für meine lokalen Programme den Programme-Ordner direkt unter meinem Benutzerverzeichnis (~/Applications
). In diesem Beispiel speichere ich das Programm unter dem Namen “ObsidianBackup” ab.
Programm testen und Berechtigungen erteilen
Nun sollte auch dieses Programm getestet werden. Beim ersten Aufruf fragt das System normalerweise nach, ob das Programm auf den Ordner “Dokumente” zugreifen darf. Wenn dieses Popup bestätigt wird, erhält das Programm die Berechtigung und es wird in den entsprechenden “Daten- und Sicherheit”-Einstellungen in der Liste angezeigt.
Falls es dabei Probleme gibt, kann das Programm-Icon auch manuell auf die Liste in den Einstellungen gezogen werden, um die Berechtigungen zu erteilen.
Launchd-Einstellugen
Das launchd
-Framework ermöglicht das automatische Ausführen von Skripten, Programmen und anderen Befehlen. So können Programme z.B. beim Systemstart automatisch gestartet und Hintergrund ausgeführt werden, bis das System heruntergefahren werden, oder aber wie hier gewünscht zu bestimmten Zeiten gestartet werden. Unter Linux kennt man einen solchen Service unter dem Namen cron
, der zwar auch noch im macOS zur Verfügung steht, aber mittelfristig durch launchd
ersetzt werden soll.
Die Konfiguration eines „launchd”-Job wird über eine XML-Datei im Verzeichnis ~/Library/LaunchAgents
gesteuert. Für dieses Beispiel habe ich die Datei ~/Library/LaunchAgent/com.leif.ObsidianBackup.plist
erstellt und folgendes XML hineinkopiert.
<?xml version="1.0" encoding="UTF-8"?> <!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd"> <plist version="1.0"> <dict> <key>Label</key> <string>com.leif.ObsidianBackup</string> <key>ProgramArguments</key> <array> <string>/usr/bin/open</string> <string>-W</string> <string>/Users/leif/Applications/ObsidianBackup.app</string> </array> <key>RunAtLoad</key> <true/> <key>StartInterval</key> <integer>3600</integer> </dict> </plist>
Der Schlüssel Label
ist der Name des Jobs, der eindeutig sein muss und dem Dateinamen entsprechen sollte. Der Schlüssel ProgramArguments
ist ein Array, das den Aufruf des oben erstellten Automator-Programms definiert, wobei die einzelnen Befehle und Optionen, die sonst mit einem Leerzeichen getrennt sind, als eigene Strings definiert werden. Die Option -W
sorgt dafür, dass gewartet wird, bis das Programm ausgeführt wurde. RunAtLoad sorgt dafür, dass der Job sofort ausgeführt wird, wenn die plist geladen wird, was immer bei einem Systemstart passiert. Das StartInterval, hier auf 3600 Sekunden (also eine Stunde) gesetzt, definiert die Zeit, die zwischen zwei Läufen des Skripts vergehen soll.
Wenn die Datei erstellt und abgespeichert ist, wird der Job manuell gestartet:
launchctl load /Users/leif/Library/LaunchAgents/com.leif.obsidian-backup.plist
Um zu kontrollieren, ob der Job im Hintergrund läuft, kann der folgende Befehl verwendet werden:
launchctl list | grep com.leif.obsidian-backup
Und falls irgendetwas nicht funktionieren sollte, kann der Job mit folgendem Befehl gestoppt werden:
launchctl unload /Users/leif/Library/LaunchAgents/com.leif.obsidian-backup.plist
Falls nun nochmal eine Abfrage angezeigt wird, ob das vom Automator erstellte Programm auf die Daten zugreifen darf, muss dies natürlich bejaht werden. Das passiert auch wenn an dem Programm etwas geändert wird. So wie der Job definiert ist, wird er nach jedem Neustart wieder aktiviert und es wird dann alle Stunde ein Backup-Lauf gestartet.
Anmerkungen
Ich habe etwas länger an dem Skript und an diesem Artikel gearbeitet, da vor allem die Lösung von zwei Probleme Zeit gebraucht und viele Test benötigt haben.
Erstens nutze ich in meinem Home-Verzeichnis ein System, bei dem meine Ordner mit einer Nummer und einem Leerzeichen versehen sind. So heißt mein eigentlicher Backup-Ordner “98 Backups”. Leerzeichen in Pfaden führen oft zu Problemen, wenn man auf der Kommandozeile im Terminal arbeitet. Die üblichen Methoden zur Handhabung von Variablen und zum Zusammenbau der Backup-Ordner funktionierten einfach nicht. Deshalb habe ich am Ende einen neuen Backup-Ordner in meinem Home-Verzeichnis erstellt, der keine Leerzeichen enthält.
Zweitens führte die Idee als Lösung für das Leerzeichen-Problem, einen neuen “Freigegebenen Ordner” zu nutzen, der auch das Problem mit dem @eaDir-Verzeichnis gelöst hätte, nicht zum Erfolg. Das Skript brach immer mit der Meldung ab, dass es Dateien nicht löschen konnte, da es nicht genügend Rechte hätte, obwohl der Benutzer, unter dem es lief, alle nötigen Rechte hatte.
Nach langem Probieren habe ich dann die einfache Lösung implementiert und in meinem Home-Verzeichnis einen Ordner ohne Leerzeichen für das Backup erstellt. Hier wurden dann komischerweise keine Berechtigungsfehler mehr gemeldet. Falls jemand mir einen Hinweis geben kann, was mein Fehler war, nehme ich diesen gerne entgegen ;-).
Mit diesem Skript werden die Änderungen im Ordner .obsidian
auch mitgesichert. Ich habe jedoch festgestellt, dass in diesem Ordner während der Arbeit mit Obsidian viele Dateien aktualisiert und gelöscht werden, was die Anzahl der Backup-Ordner erhöhen kann. Man könnte daher den Ordner mit –exclude=’.obsidian’ vom Backup ausschließen.
Natürlich kann dieses neue Skript auch über einen Kurzbefehl, wie im vorherigen Artikel beschrieben, jederzeit aufgerufen werden. Das ermöglicht zwischendurch einfach ein Backup zu starten, das sich genauso verhält, wie ein von launchd gestartetes Backup.
Anmerkungen, Korrekturen, und Kritik bitte gerne in den Kommentaren hinterlassen.
Schreibe einen Kommentar