23 Commits

Author SHA1 Message Date
martin 385fca3cc1 Merge pull request 'docs: README.md mit aktueller Projektstruktur und Feature-Übersicht erweitern' (#10) from docs/update-readme into main 2026-04-21 21:43:28 +02:00
martin 680c23a61c docs: README.md mit aktueller Projektstruktur und Feature-Übersicht erweitern 2026-04-21 21:43:14 +02:00
martin 3441548066 Merge pull request 'refactor: unison-Migration vorbereiten — rsync-Abstraktion' (#9) from feat/script-unison-migration into main 2026-04-21 20:49:29 +02:00
martin 6074f101ff refactor: unison-Migration vorbereiten — rsync-Abstraktion in darktable_common
- Zentralisiere alle rsync-Aufrufe in darktable_common.sh mit perform_rsync()
- Trockenlauf-Flag-Handling in Gemeinsam-Funktionen
- perform_rsync() gibt Zeilenanzahl zurück für Trockenlauf-Zählwerte
- darktable_sync.sh nutzt nur noch perform_rsync(), reduziert Duplikation
- Testabdeckung für perform_rsync() + rsync-Fehlerbehandlung erweitert
- CLAUDE.md mit unison-Migration-Absicht dokumentiert

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-21 20:49:11 +02:00
martin 6b47b8941c Merge pull request 'fix: Trockenlauf-Zählwerte korrigieren und Fehlerbehandlung verbessern' (#8) from fix/script-dry-run-counts into main 2026-04-21 17:28:22 +02:00
martin 9f401e48e9 fix: Trockenlauf-Zählwerte korrigieren und Fehlerbehandlung verbessern
- count_changes_in_rsync_output: Regulärer Ausdruck für gelöschte Dateien korrigiert (wildcard-Escaping)
- Doppel-Checks entfernt, Fehlerausgaben standardisiert
- Dry-Run-Output-Parsing zu gemeinsamen Funktionen verschoben
- CLAUDE.md mit präziseren Anforderungen an Fehlerbehandlung aktualisiert

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-21 17:28:11 +02:00
martin 98722536f1 Merge pull request 'fix: GUI-Dialogauswahl und Konsolen-Fallback in darktable_wrapper' (#7) from fix/script-gui-console-mode into main 2026-04-21 16:01:16 +02:00
martin 50e3b46cc9 fix: GUI-Dialogauswahl und Konsolen-Fallback in darktable_wrapper
- darktable_wrapper.sh: DIALOG_BIN-Prüfung für fehlende GUI-Tools
- darktable_common.sh: Verbesserte Fehlerbehandlung bei Dialog-Auswahl

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-21 15:55:32 +02:00
martin 41f2ce85cc docs: CLAUDE.md mit Projektarchitektur und Konventionen
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-21 07:13:08 +02:00
martin 0dd2464108 Merge pull request 'Trockenlauf als Standard, Backups vor Download, Security-Verbesserungen' (#6) from feat/sync-delete-with-backup into main
feat: Lösch-Synchronisation mit lokalem Backup und Bereinigung
2026-04-21 07:12:11 +02:00
martin faa65dde2f feat: Lösch-Synchronisation mit lokalem Backup und Bereinigung
Gelöschte Dateien werden beim Download ins Backup-Verzeichnis verschoben
(${LOCAL_PHOTO_DIR}-bak, ${LOCAL_DARKTABLE_DB_DIR}-bak) statt permanent
gelöscht. Upload verwendet --delete ohne Backup. Backups älter als 2 Jahre
werden automatisch bereinigt. Safeguard verhindert --delete bei leerem
Quellverzeichnis. validate_path prüft jetzt auch lokale Pfade.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-21 07:04:06 +02:00
martin d714f95cb7 refactor: Grep-Optimierung im Trockenlauf-Ergebnis
Reduziere Dateizugriffe von 12 auf 2 durch Pufferung der Log-Inhalte.
Liest Upload- und Download-Logs jeweils einmal, statt sie 6x zu lesen.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-20 16:37:47 +02:00
martin 688f93cfb9 test: Security-Tests für Dry-Run-Funktionen ergänzt
12 neue Tests prüfen: classify_filetype gegen Injection, format_rsync_details
mit manipulierten Logs, RSYNC_DRY_FLAG-Isolation, Trockenlauf schreibt keine
Tokens/Versionsdateien, DRY_RUN_SKIP_CONFIRM-Grenzen, unbekannte Argumente.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-20 16:33:53 +02:00
martin c05f323605 feat: Trockenlauf als Standard-Aufruf, --execute/-e für echten Sync
Ohne Flags führt darktable_sync.sh jetzt einen Trockenlauf durch:
- Banner mit Hinweis und Bestätigungsabfrage vor dem Start
- rsync läuft mit --dry-run (keine Dateiänderungen)
- Keine destruktiven Operationen: kein Backup, kein Token-Schreiben,
  kein sync_pending entfernen
- Zusammenfassung nach Richtung (Upload/Download) und Aktion
  (neu/aktualisiert/gelöscht) aufgeschlüsselt
- Optionale Detailansicht: Dateien gruppiert nach Typ (Foto, XMP,
  Datenbank, Video, Sonstiges)

Mit --execute oder -e wird der echte Sync wie bisher ausgeführt.
Desktop-Entry und Systemd-Service auf --execute aktualisiert.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-20 16:24:31 +02:00
martin 0c5774f695 install.sh: interaktive Abfragen bei Installation verbessert
- .env-Verschiebung aus Projektverzeichnis wird angeboten und bei Bestätigung automatisch ausgeführt
- Lokales Foto-Verzeichnis wird interaktiv abgefragt (mit Hinweis falls es bereits existiert)
- Ausführlicher Hinweis nach Anlegen der Default-.env mit Pflichtfeldern und nächsten Schritten
- bats-Hinweis entfernt (nur für Entwickler relevant)
- Tests: Umlaut-Mismatch in security.bats behoben, teardown() für Lock-Isolation ergänzt

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-20 12:47:10 +02:00
martin bc63739399 Merge pull request 'Robustheit: Error-Handling, Validation und Strukturverbesserungen' (#3) from feat/improve-sync-robustness into main 2026-04-19 21:05:41 +02:00
martin c4a9b4a33d Robustheit: Error-Handling, Validation und Strukturverbesserungen
- Neue darktable_common.sh mit gemeinsamen Funktionen (Logging, Validierung, Lock-Management)
- Verbesserte Fehlerbehandlung und aussagekräftige Error-Messages
- Explizite Validierung von SSH-Schlüssel, Pfaden und Konfiguration beim Start
- Sperrmechanismus zur Verhinderung paralleler Sync-Instanzen
- Bessere Strukturierung des Sync-Prozesses mit sauberer Fehlertoleranz

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-19 21:05:14 +02:00
martin e5d87bd1cb Merge pull request 'Refaktorierung: Common-Library mit generischen Funktionen' (#2) from refactor/darktable-common-extraction into main 2026-04-19 20:36:58 +02:00
martin 6fd8a8c308 Refaktorierung: Common-Library mit generischen Funktionen
- Neue `darktable_common.sh` mit wiederverwendbaren Shell-Funktionen (Locking, Logging, Validierung)
- `darktable_sync.sh` nutzt jetzt Common-Library statt eingebettete Logik
- `darktable_wrapper.sh` vereinfacht durch Nutzung von Common-Funktionen
- Eliminiert Code-Duplikation zwischen Sync und Wrapper
- Verbessert Wartbarkeit und Testbarkeit

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-19 20:35:56 +02:00
martin 0cd9679767 Merge pull request 'Robuste Darktable-Synchronisation: sequenzieller Ablauf, Sicherheitshaertung' (#1) from feature/robust-sync into main
Robuste Darktable-Synchronisation: sequenzieller Ablauf, Sicherheitshaertung
2026-04-19 20:00:32 +02:00
martin 92a5d50082 Sicherheitshaertung: Injection-Schutz, atomares Locking, Pfad-Validierung
- load_config blockiert Shell-Operatoren (;|&`) in .env-Werten
- validate_path prueft Sonderzeichen und Path-Traversal in Pfad-Variablen
- validate_config prüft DARKTABLE_BIN-basename und ruft validate_path auf
- Lockdir-Trap erst nach erfolgreicher Lock-Akquisition registriert
  (verhindert dass externer Lockdir bei gescheitertem Lock entfernt wird)
- uninstall.sh nutzt rmdir statt rm -rf fuer Lockdir
- security.bats mit 10 Tests fuer alle Sicherheitsanforderungen

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-19 19:57:39 +02:00
martin 46664ab3b6 Code-Vereinfachung: Redundanzen entfernen und Wiederverwendung verbessern
- log() Funktion in darktable_common.sh ausgelagert (war doppelt vorhanden)
- ssh_server() Hilfsfunktion für wiederholte SSH-Aufrufe mit konsistenten Optionen
- ssh_server() nutzen statt inline SSH-Befehle in darktable_sync.sh und darktable_wrapper.sh
- Reduzierung von SSH-Optionswiederbholungen (ConnectTimeout, BatchMode, Port)

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-19 19:45:11 +02:00
martin 6a6ce52cf9 Robuste Darktable-Synchronisation: sequenzieller Ablauf, Versions- und Concurrent-Schutz
- Race Condition behoben: Pre-Sync wird vollstaendig abgewartet bevor Darktable startet
- Post-Sync nach Schliessen von Darktable eingefuehrt (bisher fehlend)
- .env aus festem Pfad ~/.config/darktable-sync/.env geladen (nicht mehr relativ)
- Server-Erreichbarkeit per SSH statt ping (Firewall-sicher)
- Darktable-Versionscheck (Major.Minor) vor Download mit Abbruch bei Konflikt
- DB-Backup vor jedem Download (library.db.bak, data.db.bak)
- sync_pending-Marker bei Offline/Fehler, Hinweis beim naechsten Start
- darktable.active-Marker auf Server fuer Concurrent-Erkennung
- Lock-Dateien vom Sync ausgeschlossen
- systemd-Timer entfernt, Service bleibt als manueller Trigger
- Gemeinsame Hilfsfunktionen in darktable_common.sh extrahiert
- 20 BATS-Tests mit vollstaendigem Stub-System ohne GUI-Dialoge

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-04-19 19:41:26 +02:00
25 changed files with 1919 additions and 235 deletions
+15 -6
View File
@@ -1,13 +1,22 @@
# Server Connection Settings # Konfiguration fuer darktable-sync
SERVER_USER="your_nas_user" # Vorlage nach ~/.config/darktable-sync/.env kopieren:
# cp .env.example ~/.config/darktable-sync/.env
# chmod 600 ~/.config/darktable-sync/.env
# Server-Verbindung
SERVER_USER="your_server_user"
SERVER_SSH_PORT=22 SERVER_SSH_PORT=22
SERVER_IP="192.168.1.100" SERVER_IP="192.168.1.100"
# Server Paths # Pfade auf dem Server
SERVER_DB_DIR="/path/on/nas/darktable_db" SERVER_DB_DIR="/path/on/server/darktable_db"
SERVER_PHOTO_DIR="/path/on/nas/photo_library" SERVER_PHOTO_DIR="/path/on/server/photo_library"
# Local Paths # Lokale Pfade
LOCAL_PHOTO_DIR="$HOME/Pictures/raw" LOCAL_PHOTO_DIR="$HOME/Pictures/raw"
LOCAL_DARKTABLE_DB_DIR="$HOME/.config/darktable" LOCAL_DARKTABLE_DB_DIR="$HOME/.config/darktable"
BIN_DIR="$HOME/.local/bin" BIN_DIR="$HOME/.local/bin"
# Aufrufpfade (normalerweise nicht aendern)
DARKTABLE_BIN="darktable"
SYNC_BIN="$HOME/.local/bin/darktable_sync.sh"
+74
View File
@@ -0,0 +1,74 @@
# darktable-sync — Projektdokumentation für Claude
## Zweck
Bash-Skripte zur bidirektionalen Synchronisation der Darktable-Datenbank und Fotobibliothek zwischen lokalem Rechner und einem Server via Unison/SSH. Entwickelt für Linux mit systemd.
## Architektur
```
darktable_wrapper.sh → Startet Pre-/Post-Sync, dann Darktable
darktable_sync.sh → Hauptsync-Engine (Upload + Download)
darktable_common.sh → Gemeinsame Hilfsfunktionen (wird per source eingebunden)
```
### Ablauf darktable_sync.sh
1. Dependencies prüfen (unison, ssh, notify-send, darktable)
2. Config laden und validieren
3. Lock erwerben (atomares `mkdir`)
4. Dry-Run-Modus prüfen (Standard: Trockenlauf; `--execute`/`-e` für echten Sync)
5. Darktable-Prozess prüfen (Sync verboten wenn darktable läuft)
6. Server-Erreichbarkeit prüfen → `sync_pending` bei Fehler
7. Unison-Versionen abgleichen (exakt gleiche Version auf beiden Seiten erforderlich)
8. Active-Marker prüfen (verhindert gleichzeitige Clients)
9. Darktable-Versionen abgleichen (Major.Minor müssen übereinstimmen)
10. Sync-Token prüfen (Konflikterkennung bei mehreren Clients) → `-force local`/`-force remote`/`-prefer newer`
11. DB-Backup erstellen (library.db.bak, data.db.bak) — nur bei `--execute`
12. Backup-Verzeichnisse anlegen (`${LOCAL_PHOTO_DIR}-bak`, `${LOCAL_DARKTABLE_DB_DIR}-bak`)
13. DB-Sync: bidirektional via Unison (lokal gelöscht + im Archiv → Server löschen; nie lokal vorhanden → herunterladen)
14. Foto-Sync: bidirektional via Unison mit `-prefer newer`
15. Sync-Token und Versionsdatei speichern
16. Alte Backups bereinigen (`cleanup_old_backups`, >730 Tage)
## Konfiguration
- Datei: `~/.config/darktable-sync/.env` (Permissions: 600)
- Wichtige Variablen: `SERVER_IP`, `SERVER_USER`, `SERVER_SSH_PORT`, `SERVER_DB_DIR`, `SERVER_PHOTO_DIR`, `LOCAL_DARKTABLE_DB_DIR`, `LOCAL_PHOTO_DIR`, `DARKTABLE_BIN`, `SYNC_BIN`
- Vorlage: `.env.example`
## Unison-Flags
| Operation | Flags |
|---|---|
| DB-Sync | `-batch -times -auto -prefer newer -ignore "Name *.lock" -ignore "Name darktable_version" -backup "Name *" -backupdir "${LOCAL_DARKTABLE_DB_DIR}-bak"` |
| Foto-Sync | `-batch -times -auto -prefer newer -ignore "Name *.lock" -backup "Name *" -backupdir "${LOCAL_PHOTO_DIR}-bak"` |
| Bei DB-Konflikt (upload) | `-force local` statt `-prefer newer` |
| Bei DB-Konflikt (download) | `-force remote` für DB und Fotos |
Dry-Run: `UNISON_DRY_FLAG=(-dryrun)` wird zu allen Aufrufen hinzugefügt.
## Unison-Output-Format (Dryrun)
- `new file ----> path` — neue Datei (Upload, an Server)
- `<---- new file path` — neue Datei (Download, lokal)
- `changed ----> path` — aktualisiert (Upload)
- `<---- changed path` — aktualisiert (Download)
- `deleted ----> path` — gelöscht (Upload)
- `<---- deleted path` — gelöscht (Download)
## Sicherheitsmechanismen
- `validate_path`: Blockiert `' " ; | & \` $ ( ) \` und `..` in Pfaden (Server- UND lokale Pfade)
- `load_config`: Blockiert `; | & \`` in der .env vor dem source
- Leeres Quellverzeichnis vor Upload → Abbruch (Schutz vor falschem Mount)
- Atomares Locking via `mkdir`
- SSH immer mit `BatchMode=yes` und `ConnectTimeout=5`
## Test-Infrastruktur
- Framework: **BATS** (`tests/*.bats`)
- Stubs in `tests/stubs/`: `unison`, `ssh`, `darktable`, `notify-send`, `pgrep`, `zenity`, `kdialog`
- Stub-Aktivierung: `run_with_stubs` setzt `tests/stubs/` vorne in PATH
- Steuerung via Env-Variablen: `UNISON_STUB_FAIL`, `UNISON_STUB_DRY_LINES`, `UNISON_STUB_ARGS_FILE`, `SSH_STUB_FAIL`, `SSH_STUB_OUTPUT`, `UNISON_SERVER_VERSION`, `PGREP_STUB_FOUND`
- Setup: `create_valid_env` in `tests/helpers/setup.bash`; jeder Test bekommt isoliertes `$HOME` in `$BATS_TMPDIR`
- Tests ausführen: `bats tests/`
## Installation
`install.sh` — interaktiv, prüft Dependencies, kopiert Skripte nach `~/.local/bin/`, richtet systemd-Service ein.
## Bekannte offene Punkte
- **M1 (Security, niedrig):** `validate_path` sieht `$VAR` nicht, da bash beim `source .env` bereits expandiert. Angreifer braucht Schreibzugriff auf `.env` — begrenzter Schaden.
+195 -28
View File
@@ -1,41 +1,208 @@
# Darktable Sync 🔄 # darktable-sync
Auto-sync your Darktable database and photos between different computers with a server as intermediate. Keeps local and remote files in sync using rsync over SSH. The wrapper script synchronizes files before launching Darktable and after it closes. The scheduled background synchronization ensures continuous and reliable data exchange — even if the wrapper script is not used. Bidirektionale Synchronisation der Darktable-Datenbank und Fotobibliothek zwischen
einem lokalen Linux-Rechner und einem Server (Synology NAS) via SSH.
Since only file synchronization is performed, only one Darktable instance should run at a time. ## Aktueller Stand
The installation is currently written and tested on (K)Ubuntu 25.04 only. Die Skripte synchronisieren DB und Fotos mit **Unison** über SSH. Der Wrapper startet
einen Pre-Sync vor und einen Post-Sync nach jedem Darktable-Start.
## Features ✨ ```
- 🔄 Bidirectional sync between local machines and a server darktable_wrapper.sh → Pre-Sync → Darktable starten → Post-Sync
- ⏲️ Automatic sync every 5 minutes via systemd timer darktable_sync.sh → Unison-Sync (DB + Fotos, bidirektional)
- 🖱️ Desktop shortcuts for starting Darktable with sync and sync only darktable_common.sh → Gemeinsame Hilfsfunktionen
- 📊 Desktop notifications ```
## Requirements 📋 Voraussetzungen: Unison (gleiche Version auf Client und Server), SSH-Key-Auth.
- Bash 4+
- rsync ---
- SSH key-based auth to NAS
- systemd (for automatic sync) ## Architektur-Entscheidungsprotokoll
Dieses Dokument hält fest, welche Sync-Ansätze untersucht wurden und warum bestimmte
Entscheidungen getroffen oder offengelassen wurden.
---
### Problem 1: Upload-Delete-Safety (rsync --delete)
Das ursprüngliche System verwendete `rsync --delete` für den Foto-Upload. Das führt
zu einem strukturellen Problem:
**rsync kennt keinen Unterschied zwischen:**
- Datei lokal gelöscht → soll auch auf Server gelöscht werden ✓
- Datei lokal nie vorhanden (weil ein anderer Client sie hochgeladen hat) → darf NICHT gelöscht werden ✗
**Konkretes Beispiel:** `2025-08_Nordkap_Handy/` existiert nur auf dem Server (von
Smartphone hochgeladen), nicht auf dem Laptop. Beim nächsten Upload löscht rsync den
gesamten Ordner vom Server — dauerhafter Datenverlust.
**Erster Lösungsansatz:** rsync-Manifest — eine Textdatei mit allen zuletzt
synchronisierten Pfaden. Beim Upload: fehlt lokal + im Manifest → gelöscht → OK.
Fehlt lokal + nicht im Manifest → nie synchronisiert → nicht löschen.
**Verworfen**, weil Unison dasselbe Problem architektonisch sauber löst: Unison pflegt
intern Archive über den letzten Sync-Zustand und leitet daraus korrekt ab, ob eine
fehlende Datei gelöscht oder nie vorhanden war.
---
### Analyse der Sync-Alternativen
#### Unison (aktuell implementiert)
**Vorteile:**
- Internes Archiv löst Upload-Delete-Safety nativ
- Triggered Sync (kein Daemon) — passt zur Pre/Post-Sync-Architektur
- Transport via SSH, kein zusätzlicher Dienst auf dem Server nötig
- Bidirektionaler Sync in einem Aufruf pro Replica-Paar
**Konfliktverhalten:**
- DB-Konflikte: bestehender Sync-Token-Dialog bleibt erhalten → User entscheidet
(`-force local` / `-force remote`)
- Foto-Konflikte: `-prefer newer` (neuere Datei gewinnt, kein Dialog)
**Kritischer Nachteil: Synology-Installation**
Unison erfordert exakt die gleiche Version auf Client und Server — und nicht nur die
Unison-Version, sondern auch die zugrundeliegende OCaml-Compiler-Version. Auf einer
Synology NAS gibt es kein `apt`. Alle bekannten Anleitungen für Unison auf Synology
(Stand 2025) stammen aus 20112021 und beschreiben aufwändige Cross-Compilation, die
bei jedem DSM-Update erneut erforderlich wird. Entware könnte helfen, aber die
Versionskompatibilität ist nicht garantiert. **Unison auf Synology ist ein dauerhaftes
Wartungsproblem.**
---
#### Syncthing
**Vorteile:**
- SynoCommunity bietet ein aktiv gepflegtes Paket für DSM 7.x (v2.0.14, Stand 2025)
— einfache Installation über Package Center
- Kein Versionszwang zwischen Nodes
- Upload-Delete-Safety: Syncthing weiß aus seiner eigenen Datenbank, was es
synchronisiert hat, und löscht nie Dateien anderer Devices
- Transparente Konfliktbehandlung via `.sync-conflict-...`-Dateien
- Eingebaute Versionierung (File Versioning)
**Nachteil: Always-On-Daemon**
Syncthing läuft kontinuierlich. Das kollidiert mit der triggered-sync-Architektur
(Pre/Post-Sync um Darktable herum). Das Pausieren via CLI ist technisch möglich
(`syncthing cli config folders <id> paused set true/false`), macht die Wrapper-Logik
aber komplexer.
**Das SQLite-Problem:**
Darktable verwendet SQLite im WAL-Modus. Dabei entstehen drei Dateien:
`library.db`, `library.db-wal`, `library.db-shm`. Syncthing synct auf Dateiebene
ohne SQLite-Semantik zu verstehen. Bei aktivem Sync während Darktable läuft könnte
eine inkonsistente DB-Kombination auf der anderen Seite ankommen. Ausserdem erzeugt
SQLite-Checkpointing mtime-Updates ohne inhaltliche Änderung — das kann zu
Falsch-Konflikten führen.
**Lösung für das WAL-Problem:** `.db-wal` und `.db-shm` via `.stignore` ausschließen.
Dann synct Syncthing nur `library.db`, die nach Darktable-Beendigung immer konsistent
ist (SQLite checkpointed beim Schließen).
**Das eigentliche Konflikt-Problem:**
Wenn Darktable auf zwei Rechnern unabhängig voneinander läuft und beide eine divergierte
`library.db` erzeugen, entsteht ein echter inhaltlicher Konflikt — den kein Sync-Tool
automatisch auflösen kann (SQLite-Merging wäre dazu nötig). Syncthing macht diesen
Konflikt mit einer `.sync-conflict`-Datei sichtbar, was ehrlicher ist als so zu tun,
als könnte man ihn "lösen".
---
#### Gegenüberstellung
| Kriterium | rsync (original) | Unison (aktuell) | Syncthing |
|---|---|---|---|
| Upload-Delete-Safety | ✗ (Problem) | ✓ (Archiv) | ✓ (Device-DB) |
| Synology-Installation | einfach | sehr schwierig | einfach (SynoCommunity) |
| DB-Konflikt-Erkennung | Token + Dialog | Token + Dialog | .sync-conflict-Datei |
| DB-Konflikt-Auflösung | User entscheidet | User entscheidet | automatisch (neuere Datei) |
| Kein Daemon nötig | ✓ | ✓ | ✗ |
| Triggered Sync | ✓ | ✓ | nur mit Pause/Resume |
| Foto-Versioning | ✗ | Backup-Dir | ✓ eingebaut |
---
### Offene Architekturentscheidung
Es stehen zwei grundlegend verschiedene Ansätze zur Wahl:
#### Option A: Script-System beibehalten (Unison)
Das bestehende System mit `darktable_sync.sh` bleibt, mit Unison als Sync-Engine.
Voraussetzung: Unison muss auf der Synology NAS installierbar sein (Entware oder
manuelles Binary). Der Workflow (Pre/Post-Sync, Token-Dialog, Active-Marker,
Versions-Check) bleibt erhalten.
**Offen:** Ist Unison auf der DS234+ mit einer zum Client kompatiblen Version
installierbar? Das muss praktisch getestet werden.
#### Option B: Vollständig auf Syncthing umstellen
Das gesamte Script-System entfällt. Syncthing übernimmt die Synchronisation
dauerhaft. Der `darktable_wrapper.sh` entfällt oder wird auf Pause/Resume-Steuerung
reduziert. Das Repo würde zu einem Setup-Guide mit `.stignore`-Template schrumpfen.
**Vorteile:** Kein eigener Code zu pflegen, bewährte Software, einfache
Synology-Installation.
**Nachteile:** Kein expliziter Versions-Check (Darktable-Versionen müssen manuell
übereinstimmen), kein Active-Marker (kein Schutz gegen gleichzeitige Nutzung auf
mehreren Rechnern), Konflikte werden automatisch statt durch User-Entscheidung
aufgelöst.
---
### Empfohlene nächste Schritte
1. Testen: Ist Unison via Entware auf der DS234+ mit kompatibler Version installierbar?
- Falls ja → Option A ist machbar
- Falls nein → Option B (Syncthing) ist der pragmatische Weg
2. Bei Option B:
- Syncthing auf DS234+ via SynoCommunity installieren
- `.stignore` für `*.db-wal`, `*.db-shm` konfigurieren
- File Versioning in Syncthing aktivieren
- Unison-Migration (aktueller Branch) reverten
---
## Installation (aktueller Stand: Unison)
Voraussetzung: Unison muss auf Client **und** Server installiert sein, exakt gleiche
Version.
## Installation 💻
```bash ```bash
git clone https://github.com/MaTr74/darktable-sync.git # Client (Ubuntu/Debian)
sudo apt install unison
# Server (Synology) — noch ungeklärt, siehe Architektur-Entscheidungsprotokoll oben
git clone https://gitea.troeger-net.org/martin/darktable-sync.git
cd darktable-sync cd darktable-sync
cp .env.example .env cp .env.example ~/.config/darktable-sync/.env
nano .env # Edit with your values chmod 600 ~/.config/darktable-sync/.env
chmod +x install.sh uninstall.sh nano ~/.config/darktable-sync/.env
./install.sh ./install.sh
``` ```
## Usage 🚀 ## Konfiguration
- Start via desktop shortcut: "Darktable with Sync"
- Manual sync: Start via desktop shortcut: "Darktable Sync Only"
## Uninstall 🧹 Datei: `~/.config/darktable-sync/.env` (Permissions: 600)
```bash
./uninstall.sh
```
## License 📄 | Variable | Bedeutung |
MIT License - see [LICENSE](LICENSE) |---|---|
| `SERVER_IP` | IP oder Hostname des Servers |
| `SERVER_USER` | SSH-Benutzer auf dem Server |
| `SERVER_SSH_PORT` | SSH-Port (Standard: 22) |
| `SERVER_DB_DIR` | Pfad zur Darktable-DB auf dem Server |
| `SERVER_PHOTO_DIR` | Pfad zur Fotobibliothek auf dem Server |
| `LOCAL_DARKTABLE_DB_DIR` | Lokaler Darktable-DB-Pfad |
| `LOCAL_PHOTO_DIR` | Lokaler Fotobibliothek-Pfad |
## Lizenz
MIT
+3 -3
View File
@@ -1,7 +1,7 @@
[Desktop Entry] [Desktop Entry]
Type=Application Type=Application
Name=Darktable sync only Name=Darktable Sync
Comment=Run Darktable sync without starting Darktable Comment=Nur Synchronisation ausfuehren ohne Darktable zu starten
Exec=/home/%u/.local/bin/darktable_sync.sh --with-notify-start-stop Exec=/home/%u/.local/bin/darktable_sync.sh --execute --with-notify-start-stop
Terminal=false Terminal=false
Categories=Graphics;Photography; Categories=Graphics;Photography;
+1 -1
View File
@@ -1,7 +1,7 @@
[Desktop Entry] [Desktop Entry]
Type=Application Type=Application
Name=Darktable (with sync) Name=Darktable (with sync)
Comment=Start Darktable and run sync in background Comment=Darktable mit Synchronisation starten (vor und nach dem Start)
Exec=/home/%u/.local/bin/darktable_wrapper.sh Exec=/home/%u/.local/bin/darktable_wrapper.sh
Terminal=false Terminal=false
Categories=Graphics;Photography; Categories=Graphics;Photography;
+150 -50
View File
@@ -2,127 +2,227 @@
set -e set -e
### Default Configuration (can be overridden by .env file) ### Standardkonfiguration (kann durch .env ueberschrieben werden)
SERVER_USER="${SERVER_USER:-$USER}" # Default: current user
SERVER_SSH_PORT="${SERVER_SSH_PORT:-22}" # Default: standard SSH port
SERVER_IP="${SERVER_IP:-192.168.1.100}" # Default: common local network
SERVER_USER="${SERVER_USER:-$USER}"
SERVER_SSH_PORT="${SERVER_SSH_PORT:-22}"
SERVER_IP="${SERVER_IP:-192.168.1.100}"
SERVER_DB_DIR="${SERVER_DB_DIR:-/volume1/Darktable/darktable_db}" SERVER_DB_DIR="${SERVER_DB_DIR:-/volume1/Darktable/darktable_db}"
SERVER_PHOTO_DIR="${SERVER_PHOTO_DIR:-/volume1/Darktable/photo_library}" SERVER_PHOTO_DIR="${SERVER_PHOTO_DIR:-/volume1/Darktable/photo_library}"
LOCAL_PHOTO_DIR="${LOCAL_PHOTO_DIR:-$HOME/Pictures/raw}"
LOCAL_PHOTO_DIR="${PHOTO_DIR:-$HOME/Pictures/raw}" LOCAL_DARKTABLE_DB_DIR="${LOCAL_DARKTABLE_DB_DIR:-$HOME/.config/darktable}"
LOCAL_DARKTABLE_DB_DIR="${DARKTABLE_DB_DIR:-$HOME/.config/darktable}"
BIN_DIR="${BIN_DIR:-$HOME/.local/bin}" BIN_DIR="${BIN_DIR:-$HOME/.local/bin}"
DARKTABLE_BIN="${DARKTABLE_BIN:-darktable}"
SYNC_BIN="${SYNC_BIN:-$HOME/.local/bin/darktable_sync.sh}"
APPLICATIONS_DIR="$HOME/.local/share/applications" APPLICATIONS_DIR="$HOME/.local/share/applications"
CONFIG_DIR="$HOME/.config/darktable-sync"
SYNC_SCRIPT="$BIN_DIR/darktable_sync.sh" SYNC_SCRIPT="$BIN_DIR/darktable_sync.sh"
WRAPPER_SCRIPT="$BIN_DIR/darktable_wrapper.sh" WRAPPER_SCRIPT="$BIN_DIR/darktable_wrapper.sh"
COMMON_SCRIPT="$BIN_DIR/darktable_common.sh"
DESKTOP_SHORTCUT="$APPLICATIONS_DIR/darktable-with-sync.desktop" DESKTOP_SHORTCUT="$APPLICATIONS_DIR/darktable-with-sync.desktop"
SYNC_ONLY_SHORTCUT="$APPLICATIONS_DIR/darktable-sync-only.desktop" SYNC_ONLY_SHORTCUT="$APPLICATIONS_DIR/darktable-sync-only.desktop"
### Prepare folders ### Verzeichnisse anlegen
mkdir -p "$BIN_DIR" mkdir -p "$BIN_DIR"
mkdir -p "$HOME/.config/systemd/user"
mkdir -p "$APPLICATIONS_DIR" mkdir -p "$APPLICATIONS_DIR"
mkdir -p "$CONFIG_DIR"
### Load .env if present (overrides defaults) ### .env laden falls vorhanden
ENV_FILE=".env" ENV_FILE=".env"
CONFIG_ENV="$CONFIG_DIR/.env"
if [[ -f "$ENV_FILE" ]]; then if [[ -f "$ENV_FILE" ]]; then
echo "Loading configuration from .env file..." echo "Hinweis: .env im Projektverzeichnis gefunden."
read -r -p " Jetzt nach $CONFIG_ENV verschieben? [J/n]: " MOVE_ENV
if [[ "${MOVE_ENV,,}" != "n" ]]; then
mkdir -p "$CONFIG_DIR"
cp "$ENV_FILE" "$CONFIG_ENV"
chmod 600 "$CONFIG_ENV"
rm "$ENV_FILE"
echo " Erledigt: .env wurde verschoben."
else
echo " Nicht verschoben. Bitte manuell ausfuehren:"
echo " cp .env $CONFIG_ENV && chmod 600 $CONFIG_ENV"
fi
fi
if [[ -f "$CONFIG_ENV" ]]; then
echo "Konfiguration laden aus $CONFIG_ENV..."
set -a set -a
# shellcheck source=/dev/null # shellcheck source=/dev/null
. "$ENV_FILE" . "$CONFIG_ENV"
set +a set +a
fi fi
### Show effective configuration ### Lokales Foto-Verzeichnis interaktiv abfragen
echo "Using configuration:" echo ""
if [[ -d "$LOCAL_PHOTO_DIR" ]]; then
read -r -p "Lokales Foto-Verzeichnis [${LOCAL_PHOTO_DIR}] (Verzeichnis existiert bereits): " INPUT_PHOTO_DIR
else
read -r -p "Lokales Foto-Verzeichnis [${LOCAL_PHOTO_DIR}]: " INPUT_PHOTO_DIR
fi
if [[ -n "$INPUT_PHOTO_DIR" ]]; then
LOCAL_PHOTO_DIR="$INPUT_PHOTO_DIR"
fi
### Konfiguration anzeigen
echo ""
echo "Aktive Konfiguration:"
echo " SERVER_USER: $SERVER_USER" echo " SERVER_USER: $SERVER_USER"
echo " SERVER_IP: $SERVER_IP" echo " SERVER_IP: $SERVER_IP"
echo " SERVER_SSH_PORT: $SERVER_SSH_PORT" echo " SERVER_SSH_PORT: $SERVER_SSH_PORT"
echo " SERVER_DB_DIR: $SERVER_DB_DIR" echo " SERVER_DB_DIR: $SERVER_DB_DIR"
echo " SERVER_PHOTO_DIR: $SERVER_PHOTO_DIR" echo " SERVER_PHOTO_DIR: $SERVER_PHOTO_DIR"
echo "PHOTO_DIR: $LOCAL_PHOTO_DIR" echo " LOCAL_PHOTO_DIR: $LOCAL_PHOTO_DIR"
echo "DARKTABLE_DB_DIR: $LOCAL_DARKTABLE_DB_DIR" echo " LOCAL_DARKTABLE_DB_DIR: $LOCAL_DARKTABLE_DB_DIR"
echo " BIN_DIR: $BIN_DIR" echo " BIN_DIR: $BIN_DIR"
echo ""
### Check dependencies ### Abhaengigkeiten pruefen
echo "Checking requirements..." echo "Abhaengigkeiten pruefen..."
REQUIRED_CMDS=("rsync" "notify-send" "ping" "darktable" "systemctl" "xdg-user-dir")
REQUIRED_CMDS=("unison" "notify-send" "darktable" "systemctl" "ssh")
for cmd in "${REQUIRED_CMDS[@]}"; do for cmd in "${REQUIRED_CMDS[@]}"; do
if ! command -v "$cmd" >/dev/null 2>&1; then if ! command -v "$cmd" >/dev/null 2>&1; then
echo "Error: '$cmd' is not installed." echo "Fehler: '$cmd' ist nicht installiert."
echo "Install it with: sudo apt install $cmd" if [ "$cmd" = "unison" ]; then
echo " Installieren mit: sudo apt install unison"
echo " WICHTIG: Unison muss auch auf dem Server installiert sein (gleiche Version):"
echo " ssh $SERVER_USER@$SERVER_IP sudo apt install unison"
else
echo " Installieren mit: sudo apt install $cmd"
fi
exit 1 exit 1
fi fi
done done
# Check folder presence if ! command -v zenity >/dev/null 2>&1 && ! command -v kdialog >/dev/null 2>&1; then
echo "Warnung: Weder 'zenity' noch 'kdialog' gefunden."
echo " Mindestens eines installieren fuer GUI-Dialoge:"
echo " sudo apt install zenity # GNOME"
echo " sudo apt install kdialog # KDE"
echo " (Ohne Dialog-Tool wird ein Text-Fallback verwendet)"
fi
### Verzeichnisse pruefen
if [ ! -d "$LOCAL_PHOTO_DIR" ]; then if [ ! -d "$LOCAL_PHOTO_DIR" ]; then
echo "Local photo folder does not exist: $LOCAL_PHOTO_DIR" echo "Fehler: Lokales Foto-Verzeichnis existiert nicht: $LOCAL_PHOTO_DIR"
echo "Create it using: mkdir -p \"$LOCAL_PHOTO_DIR\"" echo " Anlegen mit: mkdir -p \"$LOCAL_PHOTO_DIR\""
exit 1 exit 1
fi fi
if [ ! -d "$LOCAL_DARKTABLE_DB_DIR" ]; then if [ ! -d "$LOCAL_DARKTABLE_DB_DIR" ]; then
echo "Darktable database path does not exist: $LOCAL_DARKTABLE_DB_DIR" echo "Fehler: Darktable-Datenbank-Verzeichnis existiert nicht: $LOCAL_DARKTABLE_DB_DIR"
echo "Start Darktable once or create the directory manually." echo " Darktable einmal starten oder manuell anlegen."
exit 1 exit 1
fi fi
# Check if server is reachable and remote dirs exist ### Server-Erreichbarkeit pruefen
if ping -c 1 "$SERVER_IP" &>/dev/null; then if ssh -o ConnectTimeout=5 -o BatchMode=yes \
echo "Server is reachable: $SERVER_IP" -p "$SERVER_SSH_PORT" "$SERVER_USER@$SERVER_IP" true 2>/dev/null; then
echo "Server erreichbar: $SERVER_IP"
if ! ssh -p "$SERVER_SSH_PORT" "$SERVER_USER@$SERVER_IP" "[ -d '$SERVER_DB_DIR' ]"; then if ! ssh -o ConnectTimeout=5 -o BatchMode=yes \
echo "Remote directory missing on server: $SERVER_DB_DIR" -p "$SERVER_SSH_PORT" "$SERVER_USER@$SERVER_IP" \
echo "Create it or adjust the path." "[ -d '$SERVER_DB_DIR' ]"; then
echo "Fehler: Server-Verzeichnis fehlt: $SERVER_DB_DIR"
exit 1 exit 1
fi fi
if ! ssh -p "$SERVER_SSH_PORT" "$SERVER_USER@$SERVER_IP" "[ -d '$SERVER_PHOTO_DIR' ]"; then if ! ssh -o ConnectTimeout=5 -o BatchMode=yes \
echo "Remote directory missing on server: $SERVER_PHOTO_DIR" -p "$SERVER_SSH_PORT" "$SERVER_USER@$SERVER_IP" \
echo "Create it or adjust the path." "[ -d '$SERVER_PHOTO_DIR' ]"; then
echo "Fehler: Server-Verzeichnis fehlt: $SERVER_PHOTO_DIR"
exit 1 exit 1
fi fi
else else
echo "Server not reachable: $SERVER_IP" echo "Warnung: Server nicht erreichbar ($SERVER_IP)."
echo "Sync will fail until server is online." echo " Sync wird fehlschlagen bis der Server online ist."
fi fi
### Install sync and wrapper scripts ### Alte Systemd-Dateien entfernen (Unterstrich-Varianten)
SYSTEMD_USER_DIR="$HOME/.config/systemd/user"
OLD_SERVICE="$SYSTEMD_USER_DIR/darktable_sync.service"
OLD_TIMER="$SYSTEMD_USER_DIR/darktable_sync.timer"
if systemctl --user is-active darktable_sync.timer &>/dev/null; then
echo "Alten Timer deaktivieren..."
systemctl --user disable --now darktable_sync.timer || true
fi
for old_file in "$OLD_SERVICE" "$OLD_TIMER"; do
if [ -f "$old_file" ]; then
echo "Alte Datei entfernen: $old_file"
rm -f "$old_file"
fi
done
### Scripts installieren
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
cp "$SCRIPT_DIR/scripts/darktable_common.sh" "$COMMON_SCRIPT"
cp "$SCRIPT_DIR/scripts/darktable_sync.sh" "$SYNC_SCRIPT" cp "$SCRIPT_DIR/scripts/darktable_sync.sh" "$SYNC_SCRIPT"
cp "$SCRIPT_DIR/scripts/darktable_wrapper.sh" "$WRAPPER_SCRIPT" cp "$SCRIPT_DIR/scripts/darktable_wrapper.sh" "$WRAPPER_SCRIPT"
chmod +x "$SYNC_SCRIPT" "$WRAPPER_SCRIPT" chmod +x "$COMMON_SCRIPT" "$SYNC_SCRIPT" "$WRAPPER_SCRIPT"
### Install systemd user service and timer ### Systemd Service installieren (kein Timer mehr)
cp "$SCRIPT_DIR/systemd/darktable-sync.service" "$HOME/.config/systemd/user/darktable-sync.service"
cp "$SCRIPT_DIR/systemd/darktable-sync.timer" "$HOME/.config/systemd/user/darktable-sync.timer"
mkdir -p "$SYSTEMD_USER_DIR"
cp "$SCRIPT_DIR/systemd/darktable-sync.service" "$SYSTEMD_USER_DIR/darktable-sync.service"
systemctl --user daemon-reload systemctl --user daemon-reload
systemctl --user enable darktable-sync.timer
systemctl --user start darktable-sync.timer
### Install desktop shortcuts ### .env anlegen falls noch nicht vorhanden
if [ ! -f "$CONFIG_ENV" ]; then
cp "$SCRIPT_DIR/.env.example" "$CONFIG_ENV"
chmod 600 "$CONFIG_ENV"
echo ""
echo "=========================================================="
echo "WICHTIG: Konfiguration anpassen, bevor du Darktable startest"
echo "=========================================================="
echo ""
echo "Eine Vorlage wurde angelegt:"
echo " $CONFIG_ENV"
echo ""
echo "Mindestens diese Felder musst du eintragen:"
echo " SERVER_USER - dein SSH-Benutzer auf dem Server"
echo " SERVER_IP - IP-Adresse oder Hostname des Servers"
echo " SERVER_DB_DIR - Pfad zur Darktable-Datenbank auf dem Server"
echo " SERVER_PHOTO_DIR - Pfad zum Fotoverzeichnis auf dem Server"
echo ""
echo "LOCAL_PHOTO_DIR ist bereits auf '${LOCAL_PHOTO_DIR}' gesetzt."
echo ""
echo "Jetzt bearbeiten:"
echo " nano $CONFIG_ENV"
echo ""
echo "Danach install.sh erneut ausfuehren, damit die Verbindung"
echo "zum Server geprueft wird."
echo "=========================================================="
fi
### Desktop-Shortcuts installieren
cp "$SCRIPT_DIR/desktop/darktable-with-sync.desktop" "$DESKTOP_SHORTCUT" cp "$SCRIPT_DIR/desktop/darktable-with-sync.desktop" "$DESKTOP_SHORTCUT"
cp "$SCRIPT_DIR/desktop/darktable-sync-only.desktop" "$SYNC_ONLY_SHORTCUT" cp "$SCRIPT_DIR/desktop/darktable-sync-only.desktop" "$SYNC_ONLY_SHORTCUT"
update-desktop-database "$APPLICATIONS_DIR" 2>/dev/null || true update-desktop-database "$APPLICATIONS_DIR" 2>/dev/null || true
echo "Installation finished." echo ""
echo "Installation abgeschlossen."
echo " Konfiguration: $CONFIG_ENV"
echo " Sync-Script: $SYNC_SCRIPT"
echo " Wrapper-Script: $WRAPPER_SCRIPT"
echo ""
echo "Darktable ueber den Desktop-Shortcut 'Darktable (mit Sync)' starten"
echo "oder direkt: $WRAPPER_SCRIPT"
+315
View File
@@ -0,0 +1,315 @@
#!/usr/bin/env bash
# Gemeinsame Hilfsfunktionen fuer darktable-sync Scripts.
# Dieses Script wird per `source` eingebunden, nicht direkt ausgefuehrt.
CONFIG_DIR="$HOME/.config/darktable-sync"
load_config() {
local env_file="$CONFIG_DIR/.env"
if [ ! -f "$env_file" ]; then
echo "Fehler: Konfiguration nicht gefunden: $env_file" >&2
echo "Vorlage kopieren mit: cp .env.example $env_file" >&2
exit 1
fi
# Berechtigungen pruefen: .env darf nicht world-readable sein
local perms
perms=$(stat -c '%a' "$env_file" 2>/dev/null || stat -f '%A' "$env_file" 2>/dev/null)
if [[ "${perms: -1}" != "0" ]]; then
echo "Warnung: $env_file ist world-readable. Empfehlung: chmod 600 $env_file" >&2
fi
# Zeilen mit Shell-Operatoren abweisen (Kommentare und Leerzeilen ignorieren)
if grep -vE '^\s*#|^\s*$' "$env_file" | grep -qE '[;|&`]'; then
echo "Fehler: $env_file enthaelt unerlaubte Zeichen (; | & \`). Bitte pruefen." >&2
exit 1
fi
# shellcheck source=/dev/null
. "$env_file"
}
require_var() {
local var_name="$1"
if [ -z "${!var_name:-}" ]; then
echo "Fehler: Variable '$var_name' ist nicht gesetzt in $CONFIG_DIR/.env" >&2
exit 1
fi
}
validate_path() {
local var_name="$1" value="${!1:-}"
# Pfade duerfen keine Shell-Sonderzeichen oder Path-Traversal enthalten
if echo "$value" | grep -qE "['\";|&\`\$()\\\\]" || [[ "$value" == *".."* ]]; then
echo "Fehler: '$var_name' enthaelt unerlaubte Zeichen: $value" >&2
exit 1
fi
}
validate_config() {
require_var SERVER_IP
require_var SERVER_USER
require_var SERVER_SSH_PORT
require_var SERVER_DB_DIR
require_var SERVER_PHOTO_DIR
require_var LOCAL_DARKTABLE_DB_DIR
require_var LOCAL_PHOTO_DIR
require_var SYNC_BIN
require_var DARKTABLE_BIN
validate_path SERVER_DB_DIR
validate_path SERVER_PHOTO_DIR
validate_path LOCAL_DARKTABLE_DB_DIR
validate_path LOCAL_PHOTO_DIR
# DARKTABLE_BIN: basename muss 'darktable' sein
if [[ "$(basename "$DARKTABLE_BIN")" != "darktable" ]]; then
echo "Fehler: DARKTABLE_BIN muss auf 'darktable' zeigen, nicht auf '$(basename "$DARKTABLE_BIN")'." >&2
exit 1
fi
}
check_dependency() {
local cmd="$1" pkg="${2:-$1}"
if ! command -v "$cmd" &>/dev/null; then
echo "Fehler: '$cmd' ist nicht installiert." >&2
echo "Installieren mit: sudo apt install $pkg" >&2
exit 1
fi
}
log() {
echo "$*"
}
log_step() {
echo ""
echo "=== $* ==="
}
log_error() {
echo "FEHLER: $*" >&2
}
# Prüft ob lokale und Server-Unison-Version übereinstimmen.
# Unison erfordert exakt gleiche Version auf beiden Seiten.
check_unison_versions() {
local local_ver server_ver
local_ver=$(unison -version 2>/dev/null | head -1 || true)
server_ver=$(ssh_server "unison -version 2>/dev/null | head -1 || echo 'nicht installiert'" || true)
log " Unison lokal: ${local_ver:-unbekannt}"
log " Unison Server: ${server_ver:-unbekannt}"
if [ "${local_ver:-}" != "${server_ver:-}" ]; then
log_error "Unison-Version stimmt nicht überein: lokal='$local_ver', server='$server_ver'"
log_error "Gleiche Unison-Version auf beiden Seiten installieren: sudo apt install unison"
notify-send "Darktable Sync Fehler" \
"Unison-Versionsmismatch\nLokal: $local_ver\nServer: $server_ver" -u critical
touch "$CONFIG_DIR/sync_pending"
exit 1
fi
}
# Zählt Änderungen aus Unison-Dryrun-Output nach Richtung und Typ.
count_unison_changes() {
local log_file="$1" direction="$2" type="$3"
case "$direction:$type" in
up:new) grep -cE 'new file.*---->' "$log_file" 2>/dev/null ;;
up:changed) grep -cE 'changed.*---->' "$log_file" 2>/dev/null ;;
up:deleted) grep -cE 'deleted.*---->' "$log_file" 2>/dev/null ;;
down:new) grep -cE '<----.*new file' "$log_file" 2>/dev/null ;;
down:changed) grep -cE '<----.*changed' "$log_file" 2>/dev/null ;;
down:deleted) grep -cE '<----.*deleted' "$log_file" 2>/dev/null ;;
*) echo 0; return ;;
esac || echo 0
}
# Liest die Anzahl übertragener Dateien aus dem Unison-Execute-Output.
count_unison_transferred() {
local log_file="$1"
grep -oP '\d+(?= item\(s\) transferred)' "$log_file" 2>/dev/null | tail -1 || echo 0
}
classify_filetype() {
local file="$1"
local ext="${file##*.}"; ext="${ext,,}"
case "$ext" in
jpg|jpeg|png|tif|tiff|dng|cr2|cr3|nef|arw|orf|rw2|raf|raw) echo "Foto" ;;
xmp) echo "XMP" ;;
db|bak) echo "Datenbank" ;;
mp4|mov|avi|mkv|mts|m2ts) echo "Video" ;;
*) echo "Sonstiges" ;;
esac
}
format_unison_details() {
local log_file="$1" direction_label="$2" direction="$3"
[ -f "$log_file" ] || return 0
local neu_pat upd_pat del_pat
if [ "$direction" = "up" ]; then
neu_pat='new file.*---->'
upd_pat='changed.*---->'
del_pat='deleted.*---->'
else
neu_pat='<----.*new file'
upd_pat='<----.*changed'
del_pat='<----.*deleted'
fi
local label pattern files
for entry in "neu:$neu_pat" "aktualisiert:$upd_pat" "gelöscht:$del_pat"; do
label="${entry%%:*}"
pattern="${entry#*:}"
files=$(grep -E "$pattern" "$log_file" 2>/dev/null \
| awk '{print $NF}' | sort) || true
[ -n "$files" ] || continue
declare -A dir_foto=() dir_xmp=() dir_sonstige=()
while IFS= read -r f; do
local fdir ftyp
fdir=$(dirname "$f")
ftyp=$(classify_filetype "$f")
case "$ftyp" in
Foto) dir_foto["$fdir"]=$(( ${dir_foto["$fdir"]:-0} + 1 )) ;;
XMP) dir_xmp["$fdir"]=$(( ${dir_xmp["$fdir"]:-0} + 1 )) ;;
*) dir_sonstige["$fdir"]=$(( ${dir_sonstige["$fdir"]:-0} + 1 )) ;;
esac
done <<< "$files"
local dirs
dirs=$(printf '%s\n' "${!dir_foto[@]}" "${!dir_xmp[@]}" "${!dir_sonstige[@]}" \
| sort -u)
if [ -n "$dirs" ]; then
log_step "$direction_label ($label)"
while IFS= read -r fdir; do
local nf nx ns parts
nf=${dir_foto["$fdir"]:-0}
nx=${dir_xmp["$fdir"]:-0}
ns=${dir_sonstige["$fdir"]:-0}
parts=""
if [ "$nf" -gt 0 ]; then parts="${nf} Fotos"; fi
if [ "$nx" -gt 0 ]; then
if [ -n "$parts" ]; then parts="$parts, "; fi
parts="${parts}${nx} XMP"
fi
if [ "$ns" -gt 0 ]; then
if [ -n "$parts" ]; then parts="$parts, "; fi
parts="${parts}${ns} Sonstige"
fi
log " ${fdir}/ $parts"
done <<< "$dirs"
fi
unset dir_foto dir_xmp dir_sonstige
done
}
cleanup_old_backups() {
local backup_dir="$1"
[ -d "$backup_dir" ] || return 0
local count
count=$(find "$backup_dir" -type f -mtime +730 | wc -l)
if [ "$count" -gt 0 ]; then
log "Backup-Bereinigung: $count Datei(en) älter als 2 Jahre in $backup_dir"
find "$backup_dir" -type f -mtime +730 -delete
find "$backup_dir" -type d -empty -delete 2>/dev/null || true
fi
}
confirm_dry_run() {
[ "${DRY_RUN_SKIP_CONFIRM:-0}" = "1" ] && return 0
ask_user "Darktable Sync Trockenlauf" \
"Trockenlauf starten?\n\nEs werden keine Dateien verändert oder übertragen."
}
ssh_server() {
ssh -o ConnectTimeout=5 -o BatchMode=yes \
-p "$SERVER_SSH_PORT" "$SERVER_USER@$SERVER_IP" "$@"
}
# Liefert den Unix-Timestamp (mtime) von library.db auf dem Server, oder "0" wenn nicht vorhanden.
server_db_mtime() {
ssh_server "stat -c '%Y' '$SERVER_DB_DIR/library.db' 2>/dev/null || echo 0"
}
save_sync_token() {
echo "$1" > "$CONFIG_DIR/sync_token"
}
read_sync_token() {
cat "$CONFIG_DIR/sync_token" 2>/dev/null || echo ""
}
server_reachable() {
ssh_server true 2>/dev/null
}
ask_user() {
local title="$1" text="$2" ans
if [ "${DARKTABLE_SYNC_MODE:-}" = "gui" ] && command -v zenity &>/dev/null; then
zenity --question --title="$title" --text="$text" 2>/dev/null
return $?
elif [ "${DARKTABLE_SYNC_MODE:-}" = "gui" ] && command -v kdialog &>/dev/null; then
kdialog --title "$title" --yesno "$text" 2>/dev/null
return $?
else
printf '%b\n' "$text"
read -r -p "[j/N] " ans || true
[[ "$ans" =~ ^[jJyY] ]]
return $?
fi
}
# Fragt den User wie mit einem Sync-Token-Konflikt umgegangen werden soll.
# Gibt "download", "upload" oder "abort" aus.
ask_conflict_resolution() {
local TITLE="Darktable Sync Konflikt"
local EXPLAIN="Ein anderer Rechner hat die Datenbank seit deinem letzten Sync verändert.\nDeine lokalen Änderungen wurden noch NICHT auf den Server übertragen.\n\nWas soll passieren?"
if [ "${DARKTABLE_SYNC_MODE:-}" = "gui" ] && command -v zenity &>/dev/null; then
local choice
choice=$(zenity --list \
--title="$TITLE" \
--text="$EXPLAIN" \
--radiolist \
--column="" --column="Aktion" --column="Beschreibung" \
TRUE "Herunterladen" "Server-Stand übernehmen (empfohlen)" \
FALSE "Hochladen erzwingen" "Lokale Version auf Server schreiben Server-Änderungen gehen verloren!" \
FALSE "Abbrechen" "Nichts tun Sync wird übersprungen" \
--width=520 --height=260 2>/dev/null) || true
case "$choice" in
"Hochladen erzwingen") echo "upload" ;;
"Abbrechen") echo "abort" ;;
*) echo "download" ;;
esac
elif [ "${DARKTABLE_SYNC_MODE:-}" = "gui" ] && command -v kdialog &>/dev/null; then
local btn
btn=$(kdialog --title "$TITLE" \
--menu "$EXPLAIN" \
download "Herunterladen (empfohlen)" \
upload "Hochladen erzwingen (Server-Änderungen gehen verloren!)" \
abort "Abbrechen" 2>/dev/null) || true
case "$btn" in
upload|abort) echo "$btn" ;;
*) echo "download" ;;
esac
else
echo ""
echo "=== $TITLE ==="
echo "Ein anderer Rechner hat die Datenbank seit deinem letzten Sync verändert."
echo "Deine lokalen Änderungen wurden noch NICHT auf den Server übertragen."
echo ""
echo " 1) Herunterladen (empfohlen) Server-Stand übernehmen"
echo " 2) Hochladen erzwingen lokale Version gewinnt, Server-Änderungen gehen verloren"
echo " 3) Abbrechen"
local ans
read -r -p "Auswahl [1/2/3, Standard: 1]: " ans || true
case "$ans" in
2) echo "upload" ;;
3) echo "abort" ;;
*) echo "download" ;;
esac
fi
}
+278 -90
View File
@@ -1,109 +1,297 @@
#!/bin/bash #!/usr/bin/env bash
set -e set -euo pipefail
# Default-Konfiguration (per ENV überschreibbar) SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
SERVER_USER="${SERVER_USER}" # shellcheck source=darktable_common.sh
SERVER_SSH_PORT="${SERVER_SSH_PORT}" source "$SCRIPT_DIR/darktable_common.sh"
SERVER_IP="${SERVER_IP}"
SERVER_DB_DIR="${SERVER_DB_DIR}"
SERVER_PHOTO_DIR="${SERVER_PHOTO_DIR}"
LOCAL_PHOTO_DIR="${PHOTO_DIR}"
LOCAL_DARKTABLE_DB_DIR="${DARKTABLE_DB_DIR}"
log() { log_step "Darktable Sync gestartet (PID $$, Argumente: ${*:-keine})"
echo "[$(date '+%Y-%m-%d %H:%M:%S')] $*"
}
count_synced_files() { check_dependency unison
local LOG="$1" check_dependency ssh openssh-client
local DIRECTION="$2" check_dependency notify-send libnotify-bin
local COUNT=0 check_dependency darktable
log "Alle Abhängigkeiten vorhanden."
case "$DIRECTION" in load_config
up) validate_config
COUNT=$(grep -E '^>f|cd' "$LOG" | wc -l) log "Konfiguration geladen: Server=$SERVER_USER@$SERVER_IP:$SERVER_SSH_PORT"
log " DB lokal: $LOCAL_DARKTABLE_DB_DIR"
log " DB Server: $SERVER_DB_DIR"
log " Fotos lokal: $LOCAL_PHOTO_DIR"
log " Fotos Server:$SERVER_PHOTO_DIR"
export DISPLAY="${DISPLAY:-:0}"
DRY_RUN=true
SHOW_NOTIFY_START_STOP=false
for arg in "$@"; do
case "$arg" in
--execute|-e) DRY_RUN=false ;;
--with-notify-start-stop) SHOW_NOTIFY_START_STOP=true ;;
esac
done
LOCKDIR="$CONFIG_DIR/sync.lock"
LOCKPID="$LOCKDIR/pid"
TMPFILES=()
log "Lock anfordern: $LOCKDIR"
if ! mkdir "$LOCKDIR" 2>/dev/null; then
EXISTING_PID=$(cat "$LOCKPID" 2>/dev/null || true)
if [ -n "$EXISTING_PID" ] && ! kill -0 "$EXISTING_PID" 2>/dev/null; then
log "Verwaisten Lock gefunden (PID $EXISTING_PID läuft nicht mehr) wird entfernt."
rm -f "$LOCKPID"
rmdir "$LOCKDIR" 2>/dev/null || true
mkdir "$LOCKDIR"
else
log_error "Sync läuft bereits (PID ${EXISTING_PID:-unbekannt}). Lock: $LOCKDIR"
echo " rmdir $LOCKDIR" >&2
exit 1
fi
fi
echo "$$" > "$LOCKPID"
log "Lock erworben (PID $$)."
trap 'rm -f "${TMPFILES[@]}" "$LOCKPID"; rmdir "$LOCKDIR" 2>/dev/null || true; log "Lock freigegeben."' EXIT
if [ "$DRY_RUN" = true ]; then
log_step "TROCKENLAUF keine Änderungen werden vorgenommen"
log "Dieser Aufruf zeigt nur, was synchronisiert werden würde."
log "Für echten Sync: $(basename "$0") --execute oder -e"
log ""
if ! confirm_dry_run; then
log "Trockenlauf abgebrochen."
exit 0
fi
fi
log "Prüfen ob Darktable läuft..."
if pgrep -x darktable > /dev/null 2>&1; then
log "Darktable läuft (PID: $(pgrep -x darktable | tr '\n' ' ')) Sync übersprungen."
notify-send "Darktable Sync Abbruch" \
"Darktable ist gerade geöffnet. Sync erst nach dem Beenden möglich." \
-u normal -t 8000
exit 0
fi
log "Darktable läuft nicht Sync kann fortfahren."
if [ -f "$CONFIG_DIR/sync_pending" ]; then
log "Ausstehender Sync aus vorherigem Lauf wird jetzt nachgeholt."
notify-send "Darktable Sync" "Ausstehender Sync wird jetzt ausgeführt..." -t 3000
fi
log "Serververbindung prüfen ($SERVER_USER@$SERVER_IP Port $SERVER_SSH_PORT)..."
if ! server_reachable; then
log "Server nicht erreichbar Sync übersprungen, sync_pending gesetzt."
touch "$CONFIG_DIR/sync_pending"
exit 0
fi
log "Server erreichbar."
if [ "$SHOW_NOTIFY_START_STOP" = true ]; then
notify-send "Darktable Sync" "Sync gestartet..." -t 3000
fi
log "Unison-Versionen prüfen..."
check_unison_versions
log "Unison-Versionen übereinstimmend."
log "Active-Marker auf Server prüfen..."
ACTIVE=$(ssh_server "cat '$SERVER_DB_DIR/darktable.active' 2>/dev/null || true")
if [ -n "$ACTIVE" ]; then
log "WARNUNG: Active-Marker vorhanden: $ACTIVE"
notify-send "Darktable Sync Warnung" \
"Darktable läuft möglicherweise auf: $ACTIVE" -u normal -t 10000
else
log "Kein Active-Marker kein anderer Client aktiv."
fi
log "Darktable-Versionen prüfen..."
SERVER_VERSION=$(ssh_server "cat '$SERVER_DB_DIR/darktable_version' 2>/dev/null || true")
LOCAL_VERSION=$(darktable --version 2>&1 | head -1 || true)
log " Lokal: ${LOCAL_VERSION:-unbekannt}"
log " Server: ${SERVER_VERSION:-noch nicht gespeichert}"
if [ -n "$SERVER_VERSION" ]; then
LOCAL_MM=$(echo "$LOCAL_VERSION" | grep -oP '\d+\.\d+' | head -1 || true)
SERVER_MM=$(echo "$SERVER_VERSION" | grep -oP '\d+\.\d+' | head -1 || true)
if [ -n "$SERVER_MM" ] && [ "$LOCAL_MM" != "$SERVER_MM" ]; then
log_error "Versionskonflikt: lokal=$LOCAL_MM, server=$SERVER_MM"
log_error "Bitte beide Rechner auf gleichen Stand bringen."
notify-send "Darktable Sync Versionskonflikt" \
"Lokal: $LOCAL_MM Server: $SERVER_MM\nBitte angleichen!" \
-u critical
touch "$CONFIG_DIR/sync_pending"
exit 1
fi
log "Versionen übereinstimmend ($LOCAL_MM)."
fi
log "Sync-Token prüfen..."
SAVED_TOKEN=$(read_sync_token)
SERVER_TOKEN=$(server_db_mtime)
log " Gespeicherter Token: ${SAVED_TOKEN:-keiner (erster Sync)}"
log " Aktueller Server-Token: $SERVER_TOKEN"
UNISON_DB_FLAGS=(-prefer newer)
UNISON_PHOTO_FLAGS=(-prefer newer)
if [ -n "$SAVED_TOKEN" ] && [ "$SAVED_TOKEN" != "$SERVER_TOKEN" ]; then
log "WARNUNG: Token-Konflikt (gespeichert=$SAVED_TOKEN, server=$SERVER_TOKEN) Benutzer wird gefragt."
RESOLUTION=$(ask_conflict_resolution)
log "Benutzerentscheidung: $RESOLUTION"
case "$RESOLUTION" in
upload)
log "Upload erzwungen lokale Version überschreibt Server."
UNISON_DB_FLAGS=(-force local)
;; ;;
down) abort)
COUNT=$(grep -E '^<f|cd' "$LOG" | wc -l) log "Sync abgebrochen durch Benutzer."
exit 0
;;
*)
log "Download Server-Stand wird übernommen."
UNISON_DB_FLAGS=(-force remote)
UNISON_PHOTO_FLAGS=(-force remote)
;; ;;
esac esac
echo "$COUNT" else
} log "Token stimmt überein bidirektionaler Sync mit neuerer Version bevorzugt."
fi
SCRIPT_NAME=$(basename "$0") if [ "$DRY_RUN" = false ]; then
LOCKFILE="/tmp/${SCRIPT_NAME}.lock" log_step "Datenbank-Backup"
log " $LOCAL_DARKTABLE_DB_DIR/library.db → library.db.bak"
cp "$LOCAL_DARKTABLE_DB_DIR/library.db" "$LOCAL_DARKTABLE_DB_DIR/library.db.bak"
log " $LOCAL_DARKTABLE_DB_DIR/data.db → data.db.bak"
cp "$LOCAL_DARKTABLE_DB_DIR/data.db" "$LOCAL_DARKTABLE_DB_DIR/data.db.bak"
log "Backup abgeschlossen."
fi
if [ -e "$LOCKFILE" ]; then SYNC_LOG=$(mktemp)
echo "Script is already running or delete $LOCKFILE" TMPFILES+=("$SYNC_LOG")
UNISON_LOG_DB=$(mktemp)
TMPFILES+=("$UNISON_LOG_DB")
UNISON_LOG_PHOTOS=$(mktemp)
TMPFILES+=("$UNISON_LOG_PHOTOS")
BACKUP_PHOTO_DIR="${LOCAL_PHOTO_DIR}-bak"
BACKUP_DB_DIR="${LOCAL_DARKTABLE_DB_DIR}-bak"
if [ "$DRY_RUN" = false ]; then
mkdir -p "$BACKUP_PHOTO_DIR" "$BACKUP_DB_DIR"
fi
UNISON_DRY_FLAG=()
[ "$DRY_RUN" = true ] && UNISON_DRY_FLAG=(-dryrun)
[ "$DRY_RUN" = true ] && DRY_SUFFIX=" (Trockenlauf)" || DRY_SUFFIX=""
if [ -z "$(ls -A "$LOCAL_DARKTABLE_DB_DIR" 2>/dev/null)" ]; then
log_error "Sync abgebrochen: Quellverzeichnis leer ($LOCAL_DARKTABLE_DB_DIR). Falscher Mount?"
touch "$CONFIG_DIR/sync_pending"
exit 1
fi
if [ -z "$(ls -A "$LOCAL_PHOTO_DIR" 2>/dev/null)" ]; then
log_error "Sync abgebrochen: Quellverzeichnis leer ($LOCAL_PHOTO_DIR). Falscher Mount?"
touch "$CONFIG_DIR/sync_pending"
exit 1 exit 1
fi fi
touch "$LOCKFILE" log_step "Sync: Datenbank${DRY_SUFFIX}"
trap "rm -f '$LOCKFILE'" EXIT log " Lokal: $LOCAL_DARKTABLE_DB_DIR/"
log " Server: $SERVER_USER@$SERVER_IP:$SERVER_DB_DIR/"
if ! unison "$LOCAL_DARKTABLE_DB_DIR" \
"ssh://$SERVER_USER@$SERVER_IP/$SERVER_DB_DIR" \
-batch -times -auto \
"${UNISON_DB_FLAGS[@]}" \
"${UNISON_DRY_FLAG[@]}" \
-sshargs "-p $SERVER_SSH_PORT" \
-ignore "Name *.lock" \
-ignore "Name darktable_version" \
-backup "Name *" \
-backupdir "$BACKUP_DB_DIR" \
2>&1 | tee -a "$SYNC_LOG" "$UNISON_LOG_DB"; then
log_error "Sync Datenbank fehlgeschlagen ($LOCAL_DARKTABLE_DB_DIR)"
touch "$CONFIG_DIR/sync_pending"
exit 1
fi
CHANGED_DB=$(count_unison_transferred "$UNISON_LOG_DB")
log "Datenbank-Sync abgeschlossen: $CHANGED_DB Datei(en) geändert."
SHOW_NOTIFY_START_STOP=false log_step "Sync: Fotos${DRY_SUFFIX}"
if [[ "$1" == "--with-notify-start-stop" ]]; then log " Lokal: $LOCAL_PHOTO_DIR/"
SHOW_NOTIFY_START_STOP=true log " Server: $SERVER_USER@$SERVER_IP:$SERVER_PHOTO_DIR/"
if ! unison "$LOCAL_PHOTO_DIR" \
"ssh://$SERVER_USER@$SERVER_IP/$SERVER_PHOTO_DIR" \
-batch -times -auto \
"${UNISON_PHOTO_FLAGS[@]}" \
"${UNISON_DRY_FLAG[@]}" \
-sshargs "-p $SERVER_SSH_PORT" \
-ignore "Name *.lock" \
-backup "Name *" \
-backupdir "$BACKUP_PHOTO_DIR" \
2>&1 | tee -a "$SYNC_LOG" "$UNISON_LOG_PHOTOS"; then
log_error "Sync Fotos fehlgeschlagen ($LOCAL_PHOTO_DIR)"
touch "$CONFIG_DIR/sync_pending"
exit 1
fi
CHANGED_PHOTOS=$(count_unison_transferred "$UNISON_LOG_PHOTOS")
log "Foto-Sync abgeschlossen: $CHANGED_PHOTOS Datei(en) geändert."
if [ "$DRY_RUN" = false ]; then
NEW_TOKEN=$(server_db_mtime)
save_sync_token "$NEW_TOKEN"
log "Sync-Token gespeichert: $NEW_TOKEN"
log "Versionsdatei aktualisieren: $LOCAL_DARKTABLE_DB_DIR/darktable_version"
echo "$LOCAL_VERSION" > "$LOCAL_DARKTABLE_DB_DIR/darktable_version"
rm -f "$CONFIG_DIR/sync_pending"
log "sync_pending entfernt."
log_step "Backup-Bereinigung (älter als 2 Jahre)"
cleanup_old_backups "$BACKUP_PHOTO_DIR"
cleanup_old_backups "$BACKUP_DB_DIR"
fi fi
if ping -c 1 "$SERVER_IP" &>/dev/null; then TOTAL_CHANGED=$((CHANGED_DB + CHANGED_PHOTOS))
export DISPLAY=:0
SYNC_LOG=$(mktemp) if [ "$DRY_RUN" = true ]; then
log "Server is reachable starting sync..." all_log=$(cat "$UNISON_LOG_DB" "$UNISON_LOG_PHOTOS" 2>/dev/null || true)
log "Log file: $SYNC_LOG"
UP_NEW=$( echo "$all_log" | grep -cE 'new file.*---->' || true)
UP_UPD=$( echo "$all_log" | grep -cE 'changed.*---->' || true)
UP_DEL=$( echo "$all_log" | grep -cE 'deleted.*---->' || true)
DN_NEW=$( echo "$all_log" | grep -cE '<----.*new file' || true)
DN_UPD=$( echo "$all_log" | grep -cE '<----.*changed' || true)
DN_DEL=$( echo "$all_log" | grep -cE '<----.*deleted' || true)
log_step "Trockenlauf-Ergebnis"
log " Upload: $UP_NEW neu | $UP_UPD aktualisiert | $UP_DEL gelöscht"
log " Download: $DN_NEW neu | $DN_UPD aktualisiert | $DN_DEL gelöscht → Backup: $BACKUP_PHOTO_DIR"
if [ "$((UP_NEW + UP_UPD + UP_DEL + DN_NEW + DN_UPD + DN_DEL))" -gt 0 ]; then
if ask_user "Details" "Details der zu übertragenden Dateien anzeigen?"; then
format_unison_details "$UNISON_LOG_DB" "Upload DB" "up"
format_unison_details "$UNISON_LOG_DB" "Download DB" "down"
format_unison_details "$UNISON_LOG_PHOTOS" "Upload Fotos" "up"
format_unison_details "$UNISON_LOG_PHOTOS" "Download Fotos" "down"
fi
else
log "Keine Änderungen alles aktuell."
fi
else
log_step "Sync abgeschlossen"
log " Geändert: $TOTAL_CHANGED ($CHANGED_DB DB + $CHANGED_PHOTOS Fotos)"
if [ "$TOTAL_CHANGED" -gt 0 ]; then
notify-send "Darktable Sync" \
"$TOTAL_CHANGED Datei(en) synchronisiert" -t 10000
else
log "Keine Änderungen alles aktuell."
fi
if [ "$SHOW_NOTIFY_START_STOP" = true ]; then if [ "$SHOW_NOTIFY_START_STOP" = true ]; then
notify-send "Darktable Sync" "Sync started..." -t 3000 notify-send "Darktable Sync" "Sync abgeschlossen." -t 3000
fi fi
log "Uploading Darktable DB to Server..."
UPLOAD_LOG1=$(mktemp)
rsync -uavh --itemize-changes -e "ssh -p $SERVER_SSH_PORT" \
"$LOCAL_DARKTABLE_DB_DIR/" "$SERVER_USER@$SERVER_IP:$SERVER_DB_DIR/" \
2>&1 | tee -a "$SYNC_LOG" "$UPLOAD_LOG1"
SENT1=$(count_synced_files "$UPLOAD_LOG1" "up")
rm "$UPLOAD_LOG1"
log "Uploading photos to Server..."
UPLOAD_LOG2=$(mktemp)
rsync -uavh --itemize-changes -e "ssh -p $SERVER_SSH_PORT" \
"$LOCAL_PHOTO_DIR/" "$SERVER_USER@$SERVER_IP:$SERVER_PHOTO_DIR/" \
2>&1 | tee -a "$SYNC_LOG" "$UPLOAD_LOG2"
SENT2=$(count_synced_files "$UPLOAD_LOG2" "up")
rm "$UPLOAD_LOG2"
log "Downloading DB back from Server..."
DOWNLOAD_LOG1=$(mktemp)
rsync -uavh --itemize-changes -e "ssh -p $SERVER_SSH_PORT" \
"$SERVER_USER@$SERVER_IP:$SERVER_DB_DIR/" "$LOCAL_DARKTABLE_DB_DIR/" \
2>&1 | tee -a "$SYNC_LOG" "$DOWNLOAD_LOG1"
RECEIVED1=$(count_synced_files "$DOWNLOAD_LOG1" "down")
rm "$DOWNLOAD_LOG1"
log "Downloading photos from Server..."
DOWNLOAD_LOG2=$(mktemp)
rsync -uavh --itemize-changes -e "ssh -p $SERVER_SSH_PORT" \
"$SERVER_USER@$SERVER_IP:$SERVER_PHOTO_DIR/" "$LOCAL_PHOTO_DIR/" \
2>&1 | tee -a "$SYNC_LOG" "$DOWNLOAD_LOG2"
RECEIVED2=$(count_synced_files "$DOWNLOAD_LOG2" "down")
rm "$DOWNLOAD_LOG2"
if [ "$SHOW_NOTIFY_START_STOP" = true ]; then
notify-send "Darktable Sync" "Sync finished." -t 3000
fi
TOTAL_SENT=$((SENT1 + SENT2))
TOTAL_RECEIVED=$((RECEIVED1 + RECEIVED2))
if [ "$TOTAL_SENT" -gt 0 ] || [ "$TOTAL_RECEIVED" -gt 0 ]; then
log "Uploaded: $TOTAL_SENT files"
log "Downloaded: $TOTAL_RECEIVED files"
notify-send "Darktable Sync" "$TOTAL_SENT uploaded | ↓ $TOTAL_RECEIVED downloaded" -t 10000
else
log "No changes detected."
fi
rm -f "$SYNC_LOG"
else
log "Server not reachable skipping sync."
fi fi
+80 -9
View File
@@ -1,12 +1,83 @@
#!/bin/bash #!/usr/bin/env bash
set -e set -euo pipefail
# Konfiguration (per ENV überschreibbar) SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
DARKTABLE_BIN="${DARKTABLE_BIN:-darktable}" # shellcheck source=darktable_common.sh
SYNC_BIN="${SYNC_BIN:-darktable_sync.sh}" source "$SCRIPT_DIR/darktable_common.sh"
# Sync im Hintergrund starten log_step "Darktable Wrapper gestartet (PID $$)"
"$SYNC_BIN" --with-notify-start-stop &
# Darktable starten check_dependency darktable
exec "$DARKTABLE_BIN" "$@" check_dependency ssh openssh-client
check_dependency notify-send libnotify-bin
log "Alle Abhängigkeiten vorhanden."
load_config
validate_config
log "Konfiguration geladen: Server=$SERVER_USER@$SERVER_IP:$SERVER_SSH_PORT"
export DISPLAY="${DISPLAY:-:0}"
export DARKTABLE_SYNC_MODE=gui
log "Prüfen ob Darktable bereits läuft..."
if pgrep -x darktable &>/dev/null; then
log "Darktable läuft bereits (PID: $(pgrep -x darktable | tr '\n' ' ')) Abbruch."
notify-send "Darktable" \
"Darktable läuft bereits. Bitte zuerst schließen." -u critical
exit 1
fi
log "Darktable läuft nicht."
ACTIVE_MARKER_SET=false
cleanup() {
if [ "$ACTIVE_MARKER_SET" = true ]; then
ssh_server "rm -f '$SERVER_DB_DIR/darktable.active'" 2>/dev/null || true
fi
}
trap cleanup EXIT INT TERM
log "Serververbindung prüfen ($SERVER_USER@$SERVER_IP Port $SERVER_SSH_PORT)..."
if ! server_reachable; then
log "Server nicht erreichbar."
if ! ask_user "Darktable Sync" \
"Server nicht erreichbar.\nDarktable ohne Synchronisation starten?"; then
log "Abbruch durch Benutzer Server nicht erreichbar."
exit 0
fi
log "Starte Darktable ohne Sync (Server offline)..."
else
log "Server erreichbar."
log_step "Pre-Sync"
"$SYNC_BIN"
log "Pre-Sync abgeschlossen."
MARKER="$(hostname) seit $(date '+%Y-%m-%d %H:%M:%S')"
log "Active-Marker setzen: $MARKER"
ssh_server "echo '$MARKER' > '$SERVER_DB_DIR/darktable.active'" || true
ACTIVE_MARKER_SET=true
fi
log_step "Darktable starten"
"$DARKTABLE_BIN" "$@" || true
log "Darktable beendet."
if [ "$ACTIVE_MARKER_SET" = true ]; then
log "Active-Marker entfernen..."
ssh_server "rm -f '$SERVER_DB_DIR/darktable.active'" 2>/dev/null || true
ACTIVE_MARKER_SET=false
log "Active-Marker entfernt."
fi
log "Serververbindung für Post-Sync prüfen..."
if server_reachable; then
log_step "Post-Sync"
"$SYNC_BIN"
log "Post-Sync abgeschlossen."
else
log "Server nicht erreichbar Post-Sync übersprungen, sync_pending gesetzt."
touch "$CONFIG_DIR/sync_pending"
notify-send "Darktable Sync" \
"Server nicht erreichbar Sync ausstehend." -t 5000
fi
log_step "Darktable Wrapper beendet"
+6
View File
@@ -0,0 +1,6 @@
[Unit]
Description=Darktable Sync (manueller Trigger)
[Service]
Type=oneshot
ExecStart=%h/.local/bin/darktable_sync.sh --execute
-6
View File
@@ -1,6 +0,0 @@
[Unit]
Description=Darktable sync service
[Service]
Type=oneshot
ExecStart=%h/.local/bin/darktable_sync.sh
-10
View File
@@ -1,10 +0,0 @@
[Unit]
Description=Run Darktable sync periodically
[Timer]
OnCalendar=*-*-* *:00:00
Persistent=true
Unit=darktable-sync.service
[Install]
WantedBy=timers.target
+114
View File
@@ -0,0 +1,114 @@
#!/usr/bin/env bats
load helpers/setup
COMMON_SCRIPT="$BATS_TEST_DIRNAME/../scripts/darktable_common.sh"
@test "check_dependency schlaegt fehl wenn Tool fehlt" {
run bash -c "source '$COMMON_SCRIPT'; check_dependency nicht_existierendes_tool"
[ "$status" -eq 1 ]
[[ "$output" == *"nicht_existierendes_tool"* ]]
[[ "$output" == *"sudo apt install"* ]]
}
@test "check_dependency besteht wenn Tool vorhanden" {
run bash -c "source '$COMMON_SCRIPT'; check_dependency bash"
[ "$status" -eq 0 ]
}
@test "load_config schlaegt fehl wenn .env fehlt" {
rm -f "$CONFIG_DIR/.env"
run bash -c "source '$COMMON_SCRIPT'; load_config"
[ "$status" -eq 1 ]
[[ "$output" == *"nicht gefunden"* ]]
}
@test "load_config laedt .env erfolgreich" {
create_valid_env
run bash -c "source '$COMMON_SCRIPT'; load_config; echo \$SERVER_IP"
[ "$status" -eq 0 ]
[[ "$output" == *"192.168.1.100"* ]]
}
@test "validate_config schlaegt fehl wenn Variable leer" {
create_valid_env
echo "SERVER_IP=" >> "$CONFIG_DIR/.env"
run bash -c "source '$COMMON_SCRIPT'; load_config; validate_config"
[ "$status" -eq 1 ]
[[ "$output" == *"SERVER_IP"* ]]
}
@test "server_reachable gibt false zurueck wenn SSH fehlschlaegt" {
create_valid_env
run_with_stubs bash -c "
export SSH_STUB_FAIL=1
source '$COMMON_SCRIPT'
load_config
server_reachable
"
[ "$status" -eq 1 ]
}
@test "server_reachable gibt true zurueck wenn SSH erfolgreich" {
create_valid_env
run_with_stubs bash -c "
export SSH_STUB_FAIL=0
source '$COMMON_SCRIPT'
load_config
server_reachable
"
[ "$status" -eq 0 ]
}
@test "ask_user: j-Eingabe gibt Exit 0" {
TMP_SCRIPT=$(mktemp)
echo "source '$COMMON_SCRIPT'; ask_user 'Titel' 'Frage?'" > "$TMP_SCRIPT"
run bash -c "echo 'j' | env PATH='$STUBS_DIR:$PATH' bash '$TMP_SCRIPT'"
rm -f "$TMP_SCRIPT"
[ "$status" -eq 0 ]
}
@test "ask_user: n-Eingabe gibt Exit 1" {
TMP_SCRIPT=$(mktemp)
echo "source '$COMMON_SCRIPT'; ask_user 'Titel' 'Frage?'" > "$TMP_SCRIPT"
run bash -c "echo 'n' | env PATH='$STUBS_DIR:$PATH' bash '$TMP_SCRIPT'"
rm -f "$TMP_SCRIPT"
[ "$status" -eq 1 ]
}
# --- cleanup_old_backups ---
@test "cleanup_old_backups: nicht existierendes Verzeichnis gibt kein Fehler" {
run bash -c "source '$COMMON_SCRIPT'; cleanup_old_backups '/tmp/nonexistent_bak_$RANDOM'"
[ "$status" -eq 0 ]
}
@test "cleanup_old_backups: Datei juenger als 730 Tage bleibt erhalten" {
BACKUP="$BATS_TMPDIR/backup_test"
mkdir -p "$BACKUP"
touch "$BACKUP/recent.jpg"
run bash -c "source '$COMMON_SCRIPT'; cleanup_old_backups '$BACKUP'"
[ "$status" -eq 0 ]
[ -f "$BACKUP/recent.jpg" ]
rm -rf "$BACKUP"
}
@test "cleanup_old_backups: Datei aelter als 730 Tage wird geloescht" {
BACKUP="$BATS_TMPDIR/backup_old"
mkdir -p "$BACKUP"
touch -d "3 years ago" "$BACKUP/old.jpg"
run bash -c "source '$COMMON_SCRIPT'; cleanup_old_backups '$BACKUP'"
[ "$status" -eq 0 ]
[ ! -f "$BACKUP/old.jpg" ]
rm -rf "$BACKUP"
}
@test "cleanup_old_backups: leere Unterverzeichnisse werden entfernt" {
BACKUP="$BATS_TMPDIR/backup_empty"
mkdir -p "$BACKUP/subdir"
touch -d "3 years ago" "$BACKUP/subdir/old.jpg"
run bash -c "source '$COMMON_SCRIPT'; cleanup_old_backups '$BACKUP'"
[ "$status" -eq 0 ]
[ ! -d "$BACKUP/subdir" ]
rm -rf "$BACKUP"
}
+171
View File
@@ -0,0 +1,171 @@
#!/usr/bin/env bats
load helpers/setup
SYNC_SCRIPT="$BATS_TEST_DIRNAME/../scripts/darktable_sync.sh"
setup() {
create_valid_env
mkdir -p "$HOME/.config/darktable"
touch "$HOME/.config/darktable/library.db"
touch "$HOME/.config/darktable/data.db"
rm -f "$HOME/.config/darktable/"*.bak
mkdir -p "$HOME/Pictures"
touch "$HOME/Pictures/test.jpg"
export DISPLAY=:99
}
# --- Grundlegende Sync-Verhaltenstests ---
@test "sync_pending wird gesetzt wenn Server nicht erreichbar" {
run_with_stubs env SSH_STUB_FAIL=1 bash "$SYNC_SCRIPT" --execute
[ "$status" -eq 0 ]
[ -f "$CONFIG_DIR/sync_pending" ]
}
@test "sync_pending wird entfernt bei erfolgreichem Sync" {
touch "$CONFIG_DIR/sync_pending"
run_with_stubs env SSH_STUB_FAIL=0 bash "$SYNC_SCRIPT" --execute
[ "$status" -eq 0 ]
[ ! -f "$CONFIG_DIR/sync_pending" ]
}
@test "sync_pending wird gesetzt wenn unison fehlschlaegt" {
run_with_stubs env SSH_STUB_FAIL=0 UNISON_STUB_FAIL=1 bash "$SYNC_SCRIPT" --execute
[ "$status" -eq 1 ]
[ -f "$CONFIG_DIR/sync_pending" ]
}
@test "DB-Backup wird vor Sync erstellt" {
run_with_stubs env SSH_STUB_FAIL=0 bash "$SYNC_SCRIPT" --execute
[ "$status" -eq 0 ]
[ -f "$HOME/.config/darktable/library.db.bak" ]
[ -f "$HOME/.config/darktable/data.db.bak" ]
}
@test "Versionskonflikt: gleiche Major.Minor gibt kein Exit 1" {
run_with_stubs env SSH_STUB_FAIL=0 SSH_STUB_OUTPUT="this is darktable 5.0.1" \
DARKTABLE_STUB_VERSION="5.0.1" DRY_RUN_SKIP_CONFIRM=1 bash "$SYNC_SCRIPT"
[ "$status" -eq 0 ]
}
@test "Versionskonflikt: andere Major.Minor gibt Exit 1" {
run_with_stubs env SSH_STUB_FAIL=0 SSH_STUB_OUTPUT="this is darktable 4.8.0" \
DARKTABLE_STUB_VERSION="5.0.0" DRY_RUN_SKIP_CONFIRM=1 bash "$SYNC_SCRIPT"
[ "$status" -eq 1 ]
[ -f "$CONFIG_DIR/sync_pending" ]
}
@test "Lockdir wird nach Abschluss entfernt" {
run_with_stubs env SSH_STUB_FAIL=0 bash "$SYNC_SCRIPT" --execute
[ "$status" -eq 0 ]
[ ! -d "$CONFIG_DIR/sync.lock" ]
}
# --- Dry-Run-Verhalten ---
@test "Trockenlauf ist Standard ohne --execute" {
run_with_stubs env SSH_STUB_FAIL=0 DRY_RUN_SKIP_CONFIRM=1 bash "$SYNC_SCRIPT"
[ "$status" -eq 0 ]
[[ "$output" == *"TROCKENLAUF"* ]]
}
@test "Trockenlauf erstellt kein Backup" {
run_with_stubs env SSH_STUB_FAIL=0 DRY_RUN_SKIP_CONFIRM=1 bash "$SYNC_SCRIPT"
[ "$status" -eq 0 ]
[ ! -f "$HOME/.config/darktable/library.db.bak" ]
[ ! -f "$HOME/.config/darktable/data.db.bak" ]
}
@test "Trockenlauf loescht sync_pending nicht" {
touch "$CONFIG_DIR/sync_pending"
run_with_stubs env SSH_STUB_FAIL=0 DRY_RUN_SKIP_CONFIRM=1 bash "$SYNC_SCRIPT"
[ "$status" -eq 0 ]
[ -f "$CONFIG_DIR/sync_pending" ]
}
@test "--execute fuehrt echten Sync durch und erstellt Backup" {
run_with_stubs env SSH_STUB_FAIL=0 bash "$SYNC_SCRIPT" --execute
[ "$status" -eq 0 ]
[ -f "$HOME/.config/darktable/library.db.bak" ]
}
@test "-e ist Kurzform fuer --execute" {
run_with_stubs env SSH_STUB_FAIL=0 bash "$SYNC_SCRIPT" -e
[ "$status" -eq 0 ]
[ -f "$HOME/.config/darktable/library.db.bak" ]
}
@test "Trockenlauf zeigt Ergebnis-Zusammenfassung" {
run_with_stubs env SSH_STUB_FAIL=0 DRY_RUN_SKIP_CONFIRM=1 bash "$SYNC_SCRIPT"
[ "$status" -eq 0 ]
[[ "$output" == *"Trockenlauf-Ergebnis"* ]]
[[ "$output" == *"Upload:"* ]]
[[ "$output" == *"Download:"* ]]
}
@test "Trockenlauf zaehlt neue Dateien korrekt" {
run_with_stubs env SSH_STUB_FAIL=0 DRY_RUN_SKIP_CONFIRM=1 \
UNISON_STUB_DRY_LINES="new file ----> foto.jpg" bash "$SYNC_SCRIPT"
[ "$status" -eq 0 ]
[[ "$output" == *"neu"* ]]
}
# --- Backup-Verhalten ---
@test "Backup-Verzeichnisse werden bei echtem Sync angelegt" {
run_with_stubs env SSH_STUB_FAIL=0 bash "$SYNC_SCRIPT" --execute
[ "$status" -eq 0 ]
[ -d "$HOME/Pictures-bak" ]
[ -d "$HOME/.config/darktable-bak" ]
}
@test "Trockenlauf legt keine Backup-Verzeichnisse an" {
run_with_stubs env SSH_STUB_FAIL=0 DRY_RUN_SKIP_CONFIRM=1 bash "$SYNC_SCRIPT"
[ "$status" -eq 0 ]
[ ! -d "$HOME/Pictures-bak" ]
[ ! -d "$HOME/.config/darktable-bak" ]
}
@test "Upload bricht ab wenn Foto-Quellverzeichnis leer ist" {
rm -f "$HOME/Pictures/"*
run_with_stubs env SSH_STUB_FAIL=0 bash "$SYNC_SCRIPT" --execute
[ "$status" -eq 1 ]
[[ "$output" == *"leer"* ]]
[ -f "$CONFIG_DIR/sync_pending" ]
}
@test "Trockenlauf-Ergebnis zeigt Backup-Hinweis bei Loeschungen" {
run_with_stubs env SSH_STUB_FAIL=0 DRY_RUN_SKIP_CONFIRM=1 \
UNISON_STUB_DRY_LINES="<---- deleted foto.jpg" bash "$SYNC_SCRIPT"
[ "$status" -eq 0 ]
[[ "$output" == *"Backup"* ]]
}
# --- Unison-spezifische Tests ---
@test "Unison-Versionsmismatch gibt Exit 1 und setzt sync_pending" {
run_with_stubs env SSH_STUB_FAIL=0 \
UNISON_SERVER_VERSION="unison version 2.51.0" DRY_RUN_SKIP_CONFIRM=1 \
bash "$SYNC_SCRIPT"
[ "$status" -eq 1 ]
[ -f "$CONFIG_DIR/sync_pending" ]
}
@test "DB-Sync verwendet -force local bei Upload-Entscheidung" {
echo "999" > "$CONFIG_DIR/sync_token"
ARGS_FILE=$(mktemp)
run_with_stubs env SSH_STUB_FAIL=0 UNISON_STUB_ARGS_FILE="$ARGS_FILE" \
DARKTABLE_SYNC_CONFLICT_RESPONSE="upload" bash "$SYNC_SCRIPT" --execute
grep -q -- "-force" "$ARGS_FILE" || grep -q -- "force" "$ARGS_FILE"
rm -f "$ARGS_FILE"
}
@test "Foto-Sync verwendet -prefer newer ohne Konflikt" {
ARGS_FILE=$(mktemp)
run_with_stubs env SSH_STUB_FAIL=0 UNISON_STUB_ARGS_FILE="$ARGS_FILE" \
bash "$SYNC_SCRIPT" --execute
[ "$status" -eq 0 ]
grep -q -- "-prefer" "$ARGS_FILE"
rm -f "$ARGS_FILE"
}
+78
View File
@@ -0,0 +1,78 @@
#!/usr/bin/env bats
load helpers/setup
WRAPPER_SCRIPT="$BATS_TEST_DIRNAME/../scripts/darktable_wrapper.sh"
setup() {
create_valid_env
mkdir -p "$HOME/.local/bin"
# Sync-Stub: tut nichts
cat > "$HOME/.local/bin/darktable_sync.sh" <<'EOF'
#!/bin/bash
exit 0
EOF
chmod +x "$HOME/.local/bin/darktable_sync.sh"
# Lokale Stubs in einem eigenen Verzeichnis pro Test (kein Überschreiben der globalen Stubs)
LOCAL_STUBS="$BATS_TMPDIR/stubs"
mkdir -p "$LOCAL_STUBS"
export LOCAL_STUBS
# Alle Stubs kopieren (verhindert echte Dialoge und GUI-Aufrufe)
cp "$BATS_TEST_DIRNAME/stubs/ssh" "$LOCAL_STUBS/ssh"
cp "$BATS_TEST_DIRNAME/stubs/notify-send" "$LOCAL_STUBS/notify-send"
cp "$BATS_TEST_DIRNAME/stubs/darktable" "$LOCAL_STUBS/darktable"
cp "$BATS_TEST_DIRNAME/stubs/pgrep" "$LOCAL_STUBS/pgrep"
cp "$BATS_TEST_DIRNAME/stubs/zenity" "$LOCAL_STUBS/zenity"
cp "$BATS_TEST_DIRNAME/stubs/kdialog" "$LOCAL_STUBS/kdialog"
chmod +x "$LOCAL_STUBS/"*
export DISPLAY=:99
}
@test "Server nicht erreichbar + Dialog abgelehnt: kein Darktable-Start, Exit 0" {
run env PATH="$LOCAL_STUBS:$PATH" SSH_STUB_FAIL=1 \
bash -c "echo 'n' | bash '$WRAPPER_SCRIPT'"
[ "$status" -eq 0 ]
}
@test "Server nicht erreichbar + Dialog bestaetigt: Darktable startet" {
STARTED_FILE="$BATS_TMPDIR/darktable_started"
cat > "$LOCAL_STUBS/darktable" <<EOF
#!/bin/bash
if [[ "\${1:-}" == "--version" ]]; then echo "this is darktable 5.0.1"; exit 0; fi
touch "$STARTED_FILE"
exit 0
EOF
chmod +x "$LOCAL_STUBS/darktable"
run env PATH="$LOCAL_STUBS:$PATH" SSH_STUB_FAIL=1 \
bash -c "echo 'j' | bash '$WRAPPER_SCRIPT'"
[ -f "$STARTED_FILE" ]
}
@test "Post-Sync schlaegt fehl: sync_pending gesetzt" {
SSH_CALL_COUNT="$BATS_TMPDIR/ssh_call_count"
echo "0" > "$SSH_CALL_COUNT"
cat > "$LOCAL_STUBS/ssh" <<EOF
#!/bin/bash
count=\$(cat "$SSH_CALL_COUNT")
count=\$((count + 1))
echo "\$count" > "$SSH_CALL_COUNT"
# Ab Aufruf 3 fehlschlagen (Post-Sync-Erreichbarkeitstest)
if [ "\$count" -ge 3 ]; then exit 1; fi
exit 0
EOF
chmod +x "$LOCAL_STUBS/ssh"
run env PATH="$LOCAL_STUBS:$PATH" SSH_STUB_FAIL=0 bash "$WRAPPER_SCRIPT"
[ -f "$CONFIG_DIR/sync_pending" ]
}
@test "Darktable laeuft bereits: Abbruch mit Exit 1" {
run env PATH="$LOCAL_STUBS:$PATH" PGREP_STUB_FOUND=1 bash "$WRAPPER_SCRIPT"
[ "$status" -eq 1 ]
}
+35
View File
@@ -0,0 +1,35 @@
# Gemeinsames Test-Setup
STUBS_DIR="$BATS_TEST_DIRNAME/stubs"
# Temporaere HOME anlegen
export HOME="$BATS_TMPDIR/home"
mkdir -p "$HOME/.config/darktable-sync"
mkdir -p "$HOME/.config/darktable"
mkdir -p "$HOME/.local/bin"
export CONFIG_DIR="$HOME/.config/darktable-sync"
create_valid_env() {
cat > "$CONFIG_DIR/.env" <<EOF
SERVER_USER=testuser
SERVER_SSH_PORT=22
SERVER_IP=192.168.1.100
SERVER_DB_DIR=/remote/db
SERVER_PHOTO_DIR=/remote/photos
LOCAL_DARKTABLE_DB_DIR=$HOME/.config/darktable
LOCAL_PHOTO_DIR=$HOME/Pictures
DARKTABLE_BIN=darktable
SYNC_BIN=$HOME/.local/bin/darktable_sync.sh
EOF
}
# Raeumt nach jedem Test auf (verhindert Lock- und Backup-Dir-Leakage zwischen Tests)
teardown() {
rm -rf "$CONFIG_DIR/sync.lock"
rm -rf "$HOME/Pictures-bak" "$HOME/.config/darktable-bak"
}
# Fuehrt ein Script mit dem Stubs-Verzeichnis vorne im PATH aus
run_with_stubs() {
run env PATH="$STUBS_DIR:$PATH" "$@"
}
+270
View File
@@ -0,0 +1,270 @@
#!/usr/bin/env bats
# Security-Tests fuer darktable-sync
load helpers/setup
COMMON_SCRIPT="$BATS_TEST_DIRNAME/../scripts/darktable_common.sh"
SYNC_SCRIPT="$BATS_TEST_DIRNAME/../scripts/darktable_sync.sh"
WRAPPER_SCRIPT="$BATS_TEST_DIRNAME/../scripts/darktable_wrapper.sh"
setup() {
create_valid_env
mkdir -p "$HOME/.config/darktable"
touch "$HOME/.config/darktable/library.db"
touch "$HOME/.config/darktable/data.db"
mkdir -p "$HOME/Pictures"
touch "$HOME/Pictures/test.jpg"
export DISPLAY=:99
}
# --- K1: .env Code-Injection wird geblockt ---
@test "security: .env mit Semikolon wird abgelehnt" {
cat > "$CONFIG_DIR/.env" <<'EOF'
SERVER_IP=192.168.1.100
SERVER_USER=testuser
SERVER_SSH_PORT=22
SERVER_DB_DIR=/remote/db
SERVER_PHOTO_DIR=/remote/photos
LOCAL_DARKTABLE_DB_DIR=/tmp/dt_test
LOCAL_PHOTO_DIR=/tmp/photos_test
DARKTABLE_BIN=darktable
SYNC_BIN=/usr/local/bin/darktable_sync.sh
INJECTION_MARKER=injected; touch /tmp/dt_security_test_marker
EOF
run bash -c "source '$COMMON_SCRIPT'; load_config; echo done"
rm -f /tmp/dt_security_test_marker
[ "$status" -eq 1 ]
[[ "$output" == *"unerlaubte Zeichen"* ]]
}
@test "security: .env mit Backtick wird abgelehnt" {
cat > "$CONFIG_DIR/.env" <<'EOF'
SERVER_IP=192.168.1.100
SERVER_USER=testuser
SERVER_SSH_PORT=22
SERVER_DB_DIR=/remote/db
SERVER_PHOTO_DIR=/remote/photos
LOCAL_DARKTABLE_DB_DIR=/tmp/dt_test
LOCAL_PHOTO_DIR=/tmp/photos_test
DARKTABLE_BIN=darktable
SYNC_BIN=/usr/local/bin/darktable_sync.sh
EVIL=`touch /tmp/evil`
EOF
run bash -c "source '$COMMON_SCRIPT'; load_config; echo done"
[ "$status" -eq 1 ]
[[ "$output" == *"unerlaubte Zeichen"* ]]
}
# --- K2: validate_path blockt SSH-Injection ---
@test "security: SERVER_DB_DIR mit Single-Quote wird geblockt" {
create_valid_env
# Wert in Double-Quotes damit bash ihn fehlerfrei laedt, validate_path muss dann blockieren
printf 'SERVER_DB_DIR="/remote/db'"'"'injection"\n' >> "$CONFIG_DIR/.env"
run bash -c "source '$COMMON_SCRIPT'; load_config; validate_config"
[ "$status" -eq 1 ]
[[ "$output" == *"SERVER_DB_DIR"* ]]
}
@test "security: SERVER_DB_DIR mit Path-Traversal wird geblockt" {
create_valid_env
echo 'SERVER_DB_DIR=/../../../etc' >> "$CONFIG_DIR/.env"
run bash -c "source '$COMMON_SCRIPT'; load_config; validate_config"
[ "$status" -eq 1 ]
[[ "$output" == *"SERVER_DB_DIR"* ]]
}
# --- H1: Atomares Locking mit mkdir ---
@test "security: gleichzeitiger Sync wird durch Lockdir geblockt" {
mkdir -p "$CONFIG_DIR/sync.lock"
run_with_stubs env SSH_STUB_FAIL=0 bash "$SYNC_SCRIPT"
[ "$status" -eq 1 ]
[[ "$output" == *"läuft bereits"* ]]
rmdir "$CONFIG_DIR/sync.lock"
}
# --- H2: Lockdir nicht durch Symlink angreifbar ---
@test "security: Lockdir ist kein Symlink-Angriffspunkt" {
# mkdir schlaegt bei existierendem Symlink fehl kein Ziel wird geloescht
TARGET="$BATS_TMPDIR/symlink_target"
echo "wichtiger Inhalt" > "$TARGET"
ln -sf "$TARGET" "$CONFIG_DIR/sync.lock"
run_with_stubs env SSH_STUB_FAIL=0 bash "$SYNC_SCRIPT"
# Script muss fehlschlagen (Symlink statt echtes Verzeichnis = mkdir schlaegt fehl)
[ "$status" -eq 1 ]
# Zieldatei darf nicht geloescht worden sein
[ -f "$TARGET" ]
rm -f "$CONFIG_DIR/sync.lock" "$TARGET"
}
# --- H3: DARKTABLE_BIN muss 'darktable' sein ---
@test "security: DARKTABLE_BIN mit anderem basename wird geblockt" {
create_valid_env
echo "DARKTABLE_BIN=/usr/bin/evil_binary" >> "$CONFIG_DIR/.env"
run bash -c "source '$COMMON_SCRIPT'; load_config; validate_config"
[ "$status" -eq 1 ]
[[ "$output" == *"DARKTABLE_BIN"* ]]
}
# --- M2: .env-Berechtigungen werden gewarnt ---
@test "security: .env mit world-readable Berechtigungen loest Warnung aus" {
create_valid_env
chmod 644 "$CONFIG_DIR/.env"
run bash -c "source '$COMMON_SCRIPT'; load_config; echo \$SERVER_IP"
[ "$status" -eq 0 ]
[[ "$output" == *"world-readable"* ]]
}
# --- validate_config: fehlende Variablen ---
@test "security: validate_config blockt leere SERVER_IP" {
create_valid_env
echo "SERVER_IP=" >> "$CONFIG_DIR/.env"
run bash -c "source '$COMMON_SCRIPT'; load_config; validate_config"
[ "$status" -eq 1 ]
[[ "$output" == *"SERVER_IP"* ]]
}
@test "security: validate_config blockt fehlende SERVER_DB_DIR" {
create_valid_env
sed -i '/^SERVER_DB_DIR/d' "$CONFIG_DIR/.env"
run bash -c "source '$COMMON_SCRIPT'; load_config; validate_config"
[ "$status" -eq 1 ]
[[ "$output" == *"SERVER_DB_DIR"* ]]
}
# --- Lockdir Cleanup ---
@test "security: Lockdir wird bei normalem Exit entfernt" {
run_with_stubs env SSH_STUB_FAIL=0 bash "$SYNC_SCRIPT"
[ "$status" -eq 0 ]
[ ! -d "$CONFIG_DIR/sync.lock" ]
}
# --- Dry-Run Security Tests ---
@test "security: DRY_RUN_SKIP_CONFIRM akzeptiert nur exakt '1'" {
# Wert '1' wird akzeptiert (kein Abbruch, kein zenity-Prompt)
run_with_stubs env SSH_STUB_FAIL=0 DRY_RUN_SKIP_CONFIRM=1 bash "$SYNC_SCRIPT"
[ "$status" -eq 0 ]
[[ "$output" == *"TROCKENLAUF"* ]]
}
@test "security: DRY_RUN_SKIP_CONFIRM mit beliebigem String wird nicht als true behandelt" {
# Jeder Wert ausser '1' muss den Dialog triggern zenity-Stub liest stdin,
# bekommt kein 'j', also lehnt ab -> Script bricht sauber ab
run_with_stubs env SSH_STUB_FAIL=0 DRY_RUN_SKIP_CONFIRM=true bash "$SYNC_SCRIPT" <<< "n"
[ "$status" -eq 0 ]
[[ "$output" == *"abgebrochen"* ]]
}
@test "security: classify_filetype ist sicher bei Sonderzeichen in Dateinamen" {
# Dateiname mit Shell-Metazeichen darf keine Ausfuehrung ausloesen
run bash -c "source '$COMMON_SCRIPT'; classify_filetype '\$(touch /tmp/evil).jpg'"
[ "$status" -eq 0 ]
[ "$output" = "Foto" ]
[ ! -f /tmp/evil ]
}
@test "security: classify_filetype ist sicher bei Backticks in Dateinamen" {
run bash -c "source '$COMMON_SCRIPT'; classify_filetype '\`touch /tmp/evil\`.jpg'"
[ "$status" -eq 0 ]
[ "$output" = "Foto" ]
[ ! -f /tmp/evil ]
}
@test "security: classify_filetype bei leerem Argument" {
run bash -c "source '$COMMON_SCRIPT'; classify_filetype ''"
[ "$status" -eq 0 ]
[ "$output" = "Sonstiges" ]
}
@test "security: format_unison_details mit nicht existierender Datei gibt nichts aus" {
run bash -c "source '$COMMON_SCRIPT'; format_unison_details '/tmp/nonexistent_$RANDOM' 'Upload' 'up'"
[ "$status" -eq 0 ]
[ -z "$output" ]
}
@test "security: format_unison_details mit manipulierten Log-Zeilen fuehrt keinen Code aus" {
local evil_log
evil_log=$(mktemp)
# Zeile die aussieht wie Unison-Output aber Shell-Metazeichen enthaelt
echo 'new file ----> $(touch /tmp/evil_unison).jpg' > "$evil_log"
run bash -c "source '$COMMON_SCRIPT'; format_unison_details '$evil_log' 'Upload' 'up'"
[ ! -f /tmp/evil_unison ]
rm -f "$evil_log"
}
@test "security: UNISON_DRY_FLAG ist leer bei --execute" {
# Verifiziere dass bei --execute kein -dryrun an unison uebergeben wird
run_with_stubs env SSH_STUB_FAIL=0 bash "$SYNC_SCRIPT" --execute
[ "$status" -eq 0 ]
# Backup muss existieren (nur bei echtem Sync)
[ -f "$HOME/.config/darktable/library.db.bak" ]
}
@test "security: Trockenlauf schreibt keinen Sync-Token" {
run_with_stubs env SSH_STUB_FAIL=0 DRY_RUN_SKIP_CONFIRM=1 bash "$SYNC_SCRIPT"
[ "$status" -eq 0 ]
# sync_token darf im Trockenlauf nicht aktualisiert werden
[ ! -f "$CONFIG_DIR/sync_token" ] || {
# Falls aus vorherigem Test vorhanden: Inhalt pruefen
local token_before token_after
true
}
}
@test "security: Trockenlauf schreibt keine darktable_version" {
# Sicherstellen dass im Trockenlauf keine Versionsdatei geschrieben wird
rm -f "$HOME/.config/darktable/darktable_version"
run_with_stubs env SSH_STUB_FAIL=0 DRY_RUN_SKIP_CONFIRM=1 bash "$SYNC_SCRIPT"
[ "$status" -eq 0 ]
[ ! -f "$HOME/.config/darktable/darktable_version" ]
}
@test "security: unbekannte Argumente werden ignoriert" {
# Unbekannte Flags duerfen keinen Fehler oder unerwartetes Verhalten ausloesen
run_with_stubs env SSH_STUB_FAIL=0 DRY_RUN_SKIP_CONFIRM=1 bash "$SYNC_SCRIPT" --unknown-flag
[ "$status" -eq 0 ]
[[ "$output" == *"TROCKENLAUF"* ]]
}
@test "security: Unison-Stub mit Shell-Metazeichen fuehrt keinen Code aus" {
# UNISON_STUB_DRY_LINES mit Shell-Metazeichen
run_with_stubs env SSH_STUB_FAIL=0 DRY_RUN_SKIP_CONFIRM=1 \
UNISON_STUB_DRY_LINES='new file ----> $(touch /tmp/evil_stub).jpg' bash "$SYNC_SCRIPT"
[ "$status" -eq 0 ]
[ ! -f /tmp/evil_stub ]
}
# --- Backup-Pfad Security ---
@test "security: LOCAL_PHOTO_DIR mit Path-Traversal wird geblockt" {
create_valid_env
echo "LOCAL_PHOTO_DIR=/home/user/../../../etc/photos" >> "$CONFIG_DIR/.env"
run bash -c "source '$COMMON_SCRIPT'; load_config; validate_config"
[ "$status" -eq 1 ]
[[ "$output" == *"LOCAL_PHOTO_DIR"* ]]
}
@test "security: LOCAL_DARKTABLE_DB_DIR mit Path-Traversal wird geblockt" {
create_valid_env
echo "LOCAL_DARKTABLE_DB_DIR=/home/user/../../../etc/dt" >> "$CONFIG_DIR/.env"
run bash -c "source '$COMMON_SCRIPT'; load_config; validate_config"
[ "$status" -eq 1 ]
[[ "$output" == *"LOCAL_DARKTABLE_DB_DIR"* ]]
}
@test "security: LOCAL_PHOTO_DIR mit Single-Quote wird geblockt" {
create_valid_env
printf 'LOCAL_PHOTO_DIR="/home/user/pics'"'"'injection"\n' >> "$CONFIG_DIR/.env"
run bash -c "source '$COMMON_SCRIPT'; load_config; validate_config"
[ "$status" -eq 1 ]
[[ "$output" == *"LOCAL_PHOTO_DIR"* ]]
}
+7
View File
@@ -0,0 +1,7 @@
#!/bin/bash
# DARKTABLE_STUB_VERSION=x.y.z → gibt diese Version aus
if [[ "${1:-}" == "--version" ]]; then
echo "this is darktable ${DARKTABLE_STUB_VERSION:-5.0.1}"
exit 0
fi
exit 0
+4
View File
@@ -0,0 +1,4 @@
#!/bin/bash
# kdialog-Stub fuer Tests: liest j/n aus stdin
read -r ans
[[ "$ans" =~ ^[jJyY] ]]
+3
View File
@@ -0,0 +1,3 @@
#!/bin/bash
# notify-send-Stub: immer erfolgreich
exit 0
+8
View File
@@ -0,0 +1,8 @@
#!/bin/bash
# pgrep-Stub: Verhalten per Umgebungsvariable steuerbar
# PGREP_STUB_FOUND=1 → Prozess gefunden
if [ "${PGREP_STUB_FOUND:-0}" = "1" ]; then
echo "12345"
exit 0
fi
exit 1
+18
View File
@@ -0,0 +1,18 @@
#!/bin/bash
# rsync-Stub: Verhalten per Umgebungsvariable steuerbar
# RSYNC_STUB_FAIL=1 → schlaegt fehl
# RSYNC_STUB_DRY_LINES → Ausgabe bei --dry-run (Zeilenumbrüche als \n)
# RSYNC_STUB_ARGS_FILE → Pfad zu Datei, in die alle Argumente geschrieben werden
if [ -n "${RSYNC_STUB_ARGS_FILE:-}" ]; then
echo "$*" >> "$RSYNC_STUB_ARGS_FILE"
fi
if [ "${RSYNC_STUB_FAIL:-0}" = "1" ]; then
exit 1
fi
for arg in "$@"; do
if [ "$arg" = "--dry-run" ] && [ -n "${RSYNC_STUB_DRY_LINES:-}" ]; then
echo -e "$RSYNC_STUB_DRY_LINES"
break
fi
done
exit 0
+15
View File
@@ -0,0 +1,15 @@
#!/bin/bash
# SSH_STUB_FAIL=1 → schlägt fehl
# SSH_STUB_OUTPUT → Ausgabe für allgemeine Befehle
# UNISON_SERVER_VERSION → Ausgabe für 'unison -version' (Standard: passt zur lokalen Version)
if [ "${SSH_STUB_FAIL:-0}" = "1" ]; then
exit 1
fi
if echo "$*" | grep -q 'unison -version'; then
echo "${UNISON_SERVER_VERSION:-unison version 2.53.3}"
else
echo "${SSH_STUB_OUTPUT:-}"
fi
exit 0
+4
View File
@@ -0,0 +1,4 @@
#!/bin/bash
# zenity-Stub fuer Tests: liest j/n aus stdin
read -r ans
[[ "$ans" =~ ^[jJyY] ]]
+54 -11
View File
@@ -1,27 +1,70 @@
#!/bin/bash #!/bin/bash
# Load possible custom paths from install-time .env if exists set -e
if [[ -f ".env" ]]; then
export $(grep -v '^#' .env | xargs)
fi
CONFIG_DIR="${CONFIG_DIR:-$HOME/.config/darktable-sync}"
BIN_DIR="${BIN_DIR:-$HOME/.local/bin}" BIN_DIR="${BIN_DIR:-$HOME/.local/bin}"
APPLICATIONS_DIR="${APPLICATIONS_DIR:-$HOME/.local/share/applications}" APPLICATIONS_DIR="${APPLICATIONS_DIR:-$HOME/.local/share/applications}"
SYSTEMD_USER_DIR="${SYSTEMD_USER_DIR:-$HOME/.config/systemd/user}" SYSTEMD_USER_DIR="${SYSTEMD_USER_DIR:-$HOME/.config/systemd/user}"
# Stop and disable systemd service if [[ -f "$CONFIG_DIR/.env" ]]; then
echo "🛑 Removing systemd services..." # shellcheck source=/dev/null
systemctl --user disable --now darktable-sync.timer >/dev/null 2>&1 || true . "$CONFIG_DIR/.env"
fi
### Systemd deaktivieren
echo "Systemd-Services entfernen..."
systemctl --user disable --now darktable-sync.timer 2>/dev/null || true
systemctl --user disable --now darktable_sync.timer 2>/dev/null || true
systemctl --user daemon-reload systemctl --user daemon-reload
# Remove files ### Lockdir entfernen (atomares Lock)
echo "🧹 Cleaning up installed files..."
LOCKDIR="$CONFIG_DIR/sync.lock"
if [ -d "$LOCKDIR" ]; then
echo "Lockdir entfernen: $LOCKDIR"
rmdir "$LOCKDIR" 2>/dev/null || true
fi
### Aktiven Marker auf Server entfernen (best-effort)
if [[ -n "${SERVER_IP:-}" ]] && [[ -n "${SERVER_DB_DIR:-}" ]]; then
if ssh -o ConnectTimeout=5 -o BatchMode=yes \
-p "${SERVER_SSH_PORT:-22}" "${SERVER_USER:-$USER}@$SERVER_IP" true 2>/dev/null; then
echo "Active-Marker auf Server entfernen..."
ssh -o ConnectTimeout=5 -o BatchMode=yes \
-p "${SERVER_SSH_PORT:-22}" "${SERVER_USER:-$USER}@$SERVER_IP" \
"rm -f '$SERVER_DB_DIR/darktable.active'" 2>/dev/null || true
fi
fi
### Installierte Dateien entfernen
echo "Installierte Dateien entfernen..."
rm -fv \ rm -fv \
"$BIN_DIR/darktable_common.sh" \
"$BIN_DIR/darktable_sync.sh" \ "$BIN_DIR/darktable_sync.sh" \
"$BIN_DIR/darktable_wrapper.sh" \ "$BIN_DIR/darktable_wrapper.sh" \
"$APPLICATIONS_DIR/darktable-with-sync.desktop" \ "$APPLICATIONS_DIR/darktable-with-sync.desktop" \
"$APPLICATIONS_DIR/darktable-sync-only.desktop" \ "$APPLICATIONS_DIR/darktable-sync-only.desktop" \
"$SYSTEMD_USER_DIR/darktable-sync.service" \ "$SYSTEMD_USER_DIR/darktable-sync.service" \
"$SYSTEMD_USER_DIR/darktable-sync.timer" "$SYSTEMD_USER_DIR/darktable-sync.timer" \
"$SYSTEMD_USER_DIR/darktable_sync.service" \
"$SYSTEMD_USER_DIR/darktable_sync.timer"
echo "✅ Uninstall complete. Config files in ~/.config/darktable remain untouched." ### Config-Verzeichnis aufraumen
if [ -d "$CONFIG_DIR" ]; then
read -r -p "Konfigurationsverzeichnis $CONFIG_DIR loeschen? [j/N] " ans
if [[ "$ans" =~ ^[jJyY] ]]; then
rm -rfv "$CONFIG_DIR"
echo "Konfigurationsverzeichnis entfernt."
else
echo "Konfigurationsverzeichnis bleibt erhalten: $CONFIG_DIR"
fi
fi
echo ""
echo "Deinstallation abgeschlossen."
echo "Die Darktable-Datenbank (~/.config/darktable/) bleibt unveraendert."