Compare commits
23 Commits
3bdd26ed81
..
main
| Author | SHA1 | Date | |
|---|---|---|---|
| 385fca3cc1 | |||
| 680c23a61c | |||
| 3441548066 | |||
| 6074f101ff | |||
| 6b47b8941c | |||
| 9f401e48e9 | |||
| 98722536f1 | |||
| 50e3b46cc9 | |||
| 41f2ce85cc | |||
| 0dd2464108 | |||
| faa65dde2f | |||
| d714f95cb7 | |||
| 688f93cfb9 | |||
| c05f323605 | |||
| 0c5774f695 | |||
| bc63739399 | |||
| c4a9b4a33d | |||
| e5d87bd1cb | |||
| 6fd8a8c308 | |||
| 0cd9679767 | |||
| 92a5d50082 | |||
| 46664ab3b6 | |||
| 6a6ce52cf9 |
+15
-6
@@ -1,13 +1,22 @@
|
||||
# Server Connection Settings
|
||||
SERVER_USER="your_nas_user"
|
||||
# Konfiguration fuer darktable-sync
|
||||
# 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_IP="192.168.1.100"
|
||||
|
||||
# Server Paths
|
||||
SERVER_DB_DIR="/path/on/nas/darktable_db"
|
||||
SERVER_PHOTO_DIR="/path/on/nas/photo_library"
|
||||
# Pfade auf dem Server
|
||||
SERVER_DB_DIR="/path/on/server/darktable_db"
|
||||
SERVER_PHOTO_DIR="/path/on/server/photo_library"
|
||||
|
||||
# Local Paths
|
||||
# Lokale Pfade
|
||||
LOCAL_PHOTO_DIR="$HOME/Pictures/raw"
|
||||
LOCAL_DARKTABLE_DB_DIR="$HOME/.config/darktable"
|
||||
BIN_DIR="$HOME/.local/bin"
|
||||
|
||||
# Aufrufpfade (normalerweise nicht aendern)
|
||||
DARKTABLE_BIN="darktable"
|
||||
SYNC_BIN="$HOME/.local/bin/darktable_sync.sh"
|
||||
|
||||
@@ -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.
|
||||
@@ -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
|
||||
- ⏲️ Automatic sync every 5 minutes via systemd timer
|
||||
- 🖱️ Desktop shortcuts for starting Darktable with sync and sync only
|
||||
- 📊 Desktop notifications
|
||||
```
|
||||
darktable_wrapper.sh → Pre-Sync → Darktable starten → Post-Sync
|
||||
darktable_sync.sh → Unison-Sync (DB + Fotos, bidirektional)
|
||||
darktable_common.sh → Gemeinsame Hilfsfunktionen
|
||||
```
|
||||
|
||||
## Requirements 📋
|
||||
- Bash 4+
|
||||
- rsync
|
||||
- SSH key-based auth to NAS
|
||||
- systemd (for automatic sync)
|
||||
Voraussetzungen: Unison (gleiche Version auf Client und Server), SSH-Key-Auth.
|
||||
|
||||
---
|
||||
|
||||
## 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 2011–2021 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
|
||||
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
|
||||
cp .env.example .env
|
||||
nano .env # Edit with your values
|
||||
chmod +x install.sh uninstall.sh
|
||||
cp .env.example ~/.config/darktable-sync/.env
|
||||
chmod 600 ~/.config/darktable-sync/.env
|
||||
nano ~/.config/darktable-sync/.env
|
||||
./install.sh
|
||||
```
|
||||
|
||||
## Usage 🚀
|
||||
- Start via desktop shortcut: "Darktable with Sync"
|
||||
- Manual sync: Start via desktop shortcut: "Darktable Sync Only"
|
||||
## Konfiguration
|
||||
|
||||
## Uninstall 🧹
|
||||
```bash
|
||||
./uninstall.sh
|
||||
```
|
||||
Datei: `~/.config/darktable-sync/.env` (Permissions: 600)
|
||||
|
||||
## License 📄
|
||||
MIT License - see [LICENSE](LICENSE)
|
||||
| Variable | Bedeutung |
|
||||
|---|---|
|
||||
| `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
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
[Desktop Entry]
|
||||
Type=Application
|
||||
Name=Darktable sync only
|
||||
Comment=Run Darktable sync without starting Darktable
|
||||
Exec=/home/%u/.local/bin/darktable_sync.sh --with-notify-start-stop
|
||||
Name=Darktable Sync
|
||||
Comment=Nur Synchronisation ausfuehren ohne Darktable zu starten
|
||||
Exec=/home/%u/.local/bin/darktable_sync.sh --execute --with-notify-start-stop
|
||||
Terminal=false
|
||||
Categories=Graphics;Photography;
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
[Desktop Entry]
|
||||
Type=Application
|
||||
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
|
||||
Terminal=false
|
||||
Categories=Graphics;Photography;
|
||||
|
||||
+169
-69
@@ -2,127 +2,227 @@
|
||||
|
||||
set -e
|
||||
|
||||
### Default Configuration (can be overridden by .env file)
|
||||
|
||||
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
|
||||
### Standardkonfiguration (kann durch .env ueberschrieben werden)
|
||||
|
||||
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_PHOTO_DIR="${SERVER_PHOTO_DIR:-/volume1/Darktable/photo_library}"
|
||||
|
||||
LOCAL_PHOTO_DIR="${PHOTO_DIR:-$HOME/Pictures/raw}"
|
||||
LOCAL_DARKTABLE_DB_DIR="${DARKTABLE_DB_DIR:-$HOME/.config/darktable}"
|
||||
|
||||
LOCAL_PHOTO_DIR="${LOCAL_PHOTO_DIR:-$HOME/Pictures/raw}"
|
||||
LOCAL_DARKTABLE_DB_DIR="${LOCAL_DARKTABLE_DB_DIR:-$HOME/.config/darktable}"
|
||||
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"
|
||||
CONFIG_DIR="$HOME/.config/darktable-sync"
|
||||
|
||||
SYNC_SCRIPT="$BIN_DIR/darktable_sync.sh"
|
||||
WRAPPER_SCRIPT="$BIN_DIR/darktable_wrapper.sh"
|
||||
COMMON_SCRIPT="$BIN_DIR/darktable_common.sh"
|
||||
DESKTOP_SHORTCUT="$APPLICATIONS_DIR/darktable-with-sync.desktop"
|
||||
SYNC_ONLY_SHORTCUT="$APPLICATIONS_DIR/darktable-sync-only.desktop"
|
||||
|
||||
### Prepare folders
|
||||
### Verzeichnisse anlegen
|
||||
|
||||
mkdir -p "$BIN_DIR"
|
||||
mkdir -p "$HOME/.config/systemd/user"
|
||||
mkdir -p "$APPLICATIONS_DIR"
|
||||
mkdir -p "$CONFIG_DIR"
|
||||
|
||||
### Load .env if present (overrides defaults)
|
||||
### .env laden falls vorhanden
|
||||
|
||||
ENV_FILE=".env"
|
||||
CONFIG_ENV="$CONFIG_DIR/.env"
|
||||
|
||||
if [[ -f "$ENV_FILE" ]]; then
|
||||
echo "Loading configuration from .env file..."
|
||||
set -a
|
||||
# shellcheck source=/dev/null
|
||||
. "$ENV_FILE"
|
||||
set +a
|
||||
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
|
||||
|
||||
### Show effective configuration
|
||||
if [[ -f "$CONFIG_ENV" ]]; then
|
||||
echo "Konfiguration laden aus $CONFIG_ENV..."
|
||||
set -a
|
||||
# shellcheck source=/dev/null
|
||||
. "$CONFIG_ENV"
|
||||
set +a
|
||||
fi
|
||||
|
||||
echo "Using configuration:"
|
||||
echo "SERVER_USER: $SERVER_USER"
|
||||
echo "SERVER_IP: $SERVER_IP"
|
||||
echo "SERVER_SSH_PORT: $SERVER_SSH_PORT"
|
||||
echo "SERVER_DB_DIR: $SERVER_DB_DIR"
|
||||
echo "SERVER_PHOTO_DIR: $SERVER_PHOTO_DIR"
|
||||
echo "PHOTO_DIR: $LOCAL_PHOTO_DIR"
|
||||
echo "DARKTABLE_DB_DIR: $LOCAL_DARKTABLE_DB_DIR"
|
||||
echo "BIN_DIR: $BIN_DIR"
|
||||
### Lokales Foto-Verzeichnis interaktiv abfragen
|
||||
|
||||
### Check dependencies
|
||||
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
|
||||
|
||||
echo "Checking requirements..."
|
||||
### Konfiguration anzeigen
|
||||
|
||||
REQUIRED_CMDS=("rsync" "notify-send" "ping" "darktable" "systemctl" "xdg-user-dir")
|
||||
echo ""
|
||||
echo "Aktive Konfiguration:"
|
||||
echo " SERVER_USER: $SERVER_USER"
|
||||
echo " SERVER_IP: $SERVER_IP"
|
||||
echo " SERVER_SSH_PORT: $SERVER_SSH_PORT"
|
||||
echo " SERVER_DB_DIR: $SERVER_DB_DIR"
|
||||
echo " SERVER_PHOTO_DIR: $SERVER_PHOTO_DIR"
|
||||
echo " LOCAL_PHOTO_DIR: $LOCAL_PHOTO_DIR"
|
||||
echo " LOCAL_DARKTABLE_DB_DIR: $LOCAL_DARKTABLE_DB_DIR"
|
||||
echo " BIN_DIR: $BIN_DIR"
|
||||
echo ""
|
||||
|
||||
### Abhaengigkeiten pruefen
|
||||
|
||||
echo "Abhaengigkeiten pruefen..."
|
||||
|
||||
REQUIRED_CMDS=("unison" "notify-send" "darktable" "systemctl" "ssh")
|
||||
for cmd in "${REQUIRED_CMDS[@]}"; do
|
||||
if ! command -v "$cmd" >/dev/null 2>&1; then
|
||||
echo "Error: '$cmd' is not installed."
|
||||
echo "Install it with: sudo apt install $cmd"
|
||||
exit 1
|
||||
fi
|
||||
if ! command -v "$cmd" >/dev/null 2>&1; then
|
||||
echo "Fehler: '$cmd' ist nicht installiert."
|
||||
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
|
||||
fi
|
||||
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
|
||||
echo "Local photo folder does not exist: $LOCAL_PHOTO_DIR"
|
||||
echo "Create it using: mkdir -p \"$LOCAL_PHOTO_DIR\""
|
||||
exit 1
|
||||
echo "Fehler: Lokales Foto-Verzeichnis existiert nicht: $LOCAL_PHOTO_DIR"
|
||||
echo " Anlegen mit: mkdir -p \"$LOCAL_PHOTO_DIR\""
|
||||
exit 1
|
||||
fi
|
||||
|
||||
if [ ! -d "$LOCAL_DARKTABLE_DB_DIR" ]; then
|
||||
echo "Darktable database path does not exist: $LOCAL_DARKTABLE_DB_DIR"
|
||||
echo "Start Darktable once or create the directory manually."
|
||||
exit 1
|
||||
echo "Fehler: Darktable-Datenbank-Verzeichnis existiert nicht: $LOCAL_DARKTABLE_DB_DIR"
|
||||
echo " Darktable einmal starten oder manuell anlegen."
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Check if server is reachable and remote dirs exist
|
||||
### Server-Erreichbarkeit pruefen
|
||||
|
||||
if ping -c 1 "$SERVER_IP" &>/dev/null; then
|
||||
echo "Server is reachable: $SERVER_IP"
|
||||
if ssh -o ConnectTimeout=5 -o BatchMode=yes \
|
||||
-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
|
||||
echo "Remote directory missing on server: $SERVER_DB_DIR"
|
||||
echo "Create it or adjust the path."
|
||||
exit 1
|
||||
fi
|
||||
if ! ssh -o ConnectTimeout=5 -o BatchMode=yes \
|
||||
-p "$SERVER_SSH_PORT" "$SERVER_USER@$SERVER_IP" \
|
||||
"[ -d '$SERVER_DB_DIR' ]"; then
|
||||
echo "Fehler: Server-Verzeichnis fehlt: $SERVER_DB_DIR"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
if ! ssh -p "$SERVER_SSH_PORT" "$SERVER_USER@$SERVER_IP" "[ -d '$SERVER_PHOTO_DIR' ]"; then
|
||||
echo "Remote directory missing on server: $SERVER_PHOTO_DIR"
|
||||
echo "Create it or adjust the path."
|
||||
exit 1
|
||||
fi
|
||||
if ! ssh -o ConnectTimeout=5 -o BatchMode=yes \
|
||||
-p "$SERVER_SSH_PORT" "$SERVER_USER@$SERVER_IP" \
|
||||
"[ -d '$SERVER_PHOTO_DIR' ]"; then
|
||||
echo "Fehler: Server-Verzeichnis fehlt: $SERVER_PHOTO_DIR"
|
||||
exit 1
|
||||
fi
|
||||
else
|
||||
echo "Server not reachable: $SERVER_IP"
|
||||
echo "Sync will fail until server is online."
|
||||
echo "Warnung: Server nicht erreichbar ($SERVER_IP)."
|
||||
echo " Sync wird fehlschlagen bis der Server online ist."
|
||||
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)"
|
||||
|
||||
cp "$SCRIPT_DIR/scripts/darktable_sync.sh" "$SYNC_SCRIPT"
|
||||
cp "$SCRIPT_DIR/scripts/darktable_common.sh" "$COMMON_SCRIPT"
|
||||
cp "$SCRIPT_DIR/scripts/darktable_sync.sh" "$SYNC_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
|
||||
|
||||
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"
|
||||
### Systemd Service installieren (kein Timer mehr)
|
||||
|
||||
mkdir -p "$SYSTEMD_USER_DIR"
|
||||
cp "$SCRIPT_DIR/systemd/darktable-sync.service" "$SYSTEMD_USER_DIR/darktable-sync.service"
|
||||
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-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
|
||||
|
||||
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"
|
||||
|
||||
@@ -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
|
||||
}
|
||||
+274
-86
@@ -1,109 +1,297 @@
|
||||
#!/bin/bash
|
||||
set -e
|
||||
#!/usr/bin/env bash
|
||||
set -euo pipefail
|
||||
|
||||
# Default-Konfiguration (per ENV überschreibbar)
|
||||
SERVER_USER="${SERVER_USER}"
|
||||
SERVER_SSH_PORT="${SERVER_SSH_PORT}"
|
||||
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}"
|
||||
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||
# shellcheck source=darktable_common.sh
|
||||
source "$SCRIPT_DIR/darktable_common.sh"
|
||||
|
||||
log() {
|
||||
echo "[$(date '+%Y-%m-%d %H:%M:%S')] $*"
|
||||
}
|
||||
log_step "Darktable Sync gestartet (PID $$, Argumente: ${*:-keine})"
|
||||
|
||||
count_synced_files() {
|
||||
local LOG="$1"
|
||||
local DIRECTION="$2"
|
||||
local COUNT=0
|
||||
check_dependency unison
|
||||
check_dependency ssh openssh-client
|
||||
check_dependency notify-send libnotify-bin
|
||||
check_dependency darktable
|
||||
log "Alle Abhängigkeiten vorhanden."
|
||||
|
||||
case "$DIRECTION" in
|
||||
up)
|
||||
COUNT=$(grep -E '^>f|cd' "$LOG" | wc -l)
|
||||
load_config
|
||||
validate_config
|
||||
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)
|
||||
COUNT=$(grep -E '^<f|cd' "$LOG" | wc -l)
|
||||
abort)
|
||||
log "Sync abgebrochen durch Benutzer."
|
||||
exit 0
|
||||
;;
|
||||
*)
|
||||
log "Download – Server-Stand wird übernommen."
|
||||
UNISON_DB_FLAGS=(-force remote)
|
||||
UNISON_PHOTO_FLAGS=(-force remote)
|
||||
;;
|
||||
esac
|
||||
echo "$COUNT"
|
||||
}
|
||||
else
|
||||
log "Token stimmt überein – bidirektionaler Sync mit neuerer Version bevorzugt."
|
||||
fi
|
||||
|
||||
SCRIPT_NAME=$(basename "$0")
|
||||
LOCKFILE="/tmp/${SCRIPT_NAME}.lock"
|
||||
if [ "$DRY_RUN" = false ]; then
|
||||
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
|
||||
echo "Script is already running or delete $LOCKFILE"
|
||||
SYNC_LOG=$(mktemp)
|
||||
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
|
||||
fi
|
||||
|
||||
touch "$LOCKFILE"
|
||||
trap "rm -f '$LOCKFILE'" EXIT
|
||||
log_step "Sync: Datenbank${DRY_SUFFIX}"
|
||||
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
|
||||
if [[ "$1" == "--with-notify-start-stop" ]]; then
|
||||
SHOW_NOTIFY_START_STOP=true
|
||||
log_step "Sync: Fotos${DRY_SUFFIX}"
|
||||
log " Lokal: $LOCAL_PHOTO_DIR/"
|
||||
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
|
||||
|
||||
if ping -c 1 "$SERVER_IP" &>/dev/null; then
|
||||
export DISPLAY=:0
|
||||
SYNC_LOG=$(mktemp)
|
||||
log "Server is reachable – starting sync..."
|
||||
log "Log file: $SYNC_LOG"
|
||||
TOTAL_CHANGED=$((CHANGED_DB + CHANGED_PHOTOS))
|
||||
|
||||
if [ "$SHOW_NOTIFY_START_STOP" = true ]; then
|
||||
notify-send "Darktable Sync" "Sync started..." -t 3000
|
||||
fi
|
||||
if [ "$DRY_RUN" = true ]; then
|
||||
all_log=$(cat "$UNISON_LOG_DB" "$UNISON_LOG_PHOTOS" 2>/dev/null || true)
|
||||
|
||||
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"
|
||||
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 "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_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"
|
||||
|
||||
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
|
||||
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 "No changes detected."
|
||||
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
|
||||
|
||||
rm -f "$SYNC_LOG"
|
||||
else
|
||||
log "Server not reachable – skipping sync."
|
||||
if [ "$SHOW_NOTIFY_START_STOP" = true ]; then
|
||||
notify-send "Darktable Sync" "Sync abgeschlossen." -t 3000
|
||||
fi
|
||||
fi
|
||||
|
||||
@@ -1,12 +1,83 @@
|
||||
#!/bin/bash
|
||||
set -e
|
||||
#!/usr/bin/env bash
|
||||
set -euo pipefail
|
||||
|
||||
# Konfiguration (per ENV überschreibbar)
|
||||
DARKTABLE_BIN="${DARKTABLE_BIN:-darktable}"
|
||||
SYNC_BIN="${SYNC_BIN:-darktable_sync.sh}"
|
||||
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||
# shellcheck source=darktable_common.sh
|
||||
source "$SCRIPT_DIR/darktable_common.sh"
|
||||
|
||||
# Sync im Hintergrund starten
|
||||
"$SYNC_BIN" --with-notify-start-stop &
|
||||
log_step "Darktable Wrapper gestartet (PID $$)"
|
||||
|
||||
# Darktable starten
|
||||
exec "$DARKTABLE_BIN" "$@"
|
||||
check_dependency darktable
|
||||
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"
|
||||
|
||||
@@ -0,0 +1,6 @@
|
||||
[Unit]
|
||||
Description=Darktable Sync (manueller Trigger)
|
||||
|
||||
[Service]
|
||||
Type=oneshot
|
||||
ExecStart=%h/.local/bin/darktable_sync.sh --execute
|
||||
@@ -1,6 +0,0 @@
|
||||
[Unit]
|
||||
Description=Darktable sync service
|
||||
|
||||
[Service]
|
||||
Type=oneshot
|
||||
ExecStart=%h/.local/bin/darktable_sync.sh
|
||||
@@ -1,10 +0,0 @@
|
||||
[Unit]
|
||||
Description=Run Darktable sync periodically
|
||||
|
||||
[Timer]
|
||||
OnCalendar=*-*-* *:00:00
|
||||
Persistent=true
|
||||
Unit=darktable-sync.service
|
||||
|
||||
[Install]
|
||||
WantedBy=timers.target
|
||||
@@ -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"
|
||||
}
|
||||
@@ -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"
|
||||
}
|
||||
@@ -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 ]
|
||||
}
|
||||
@@ -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" "$@"
|
||||
}
|
||||
@@ -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"* ]]
|
||||
}
|
||||
Executable
+7
@@ -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
|
||||
Executable
+4
@@ -0,0 +1,4 @@
|
||||
#!/bin/bash
|
||||
# kdialog-Stub fuer Tests: liest j/n aus stdin
|
||||
read -r ans
|
||||
[[ "$ans" =~ ^[jJyY] ]]
|
||||
Executable
+3
@@ -0,0 +1,3 @@
|
||||
#!/bin/bash
|
||||
# notify-send-Stub: immer erfolgreich
|
||||
exit 0
|
||||
Executable
+8
@@ -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
|
||||
Executable
+18
@@ -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
|
||||
Executable
+15
@@ -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
|
||||
Executable
+4
@@ -0,0 +1,4 @@
|
||||
#!/bin/bash
|
||||
# zenity-Stub fuer Tests: liest j/n aus stdin
|
||||
read -r ans
|
||||
[[ "$ans" =~ ^[jJyY] ]]
|
||||
+60
-17
@@ -1,27 +1,70 @@
|
||||
#!/bin/bash
|
||||
|
||||
# Load possible custom paths from install-time .env if exists
|
||||
if [[ -f ".env" ]]; then
|
||||
export $(grep -v '^#' .env | xargs)
|
||||
fi
|
||||
set -e
|
||||
|
||||
CONFIG_DIR="${CONFIG_DIR:-$HOME/.config/darktable-sync}"
|
||||
BIN_DIR="${BIN_DIR:-$HOME/.local/bin}"
|
||||
APPLICATIONS_DIR="${APPLICATIONS_DIR:-$HOME/.local/share/applications}"
|
||||
SYSTEMD_USER_DIR="${SYSTEMD_USER_DIR:-$HOME/.config/systemd/user}"
|
||||
|
||||
# Stop and disable systemd service
|
||||
echo "🛑 Removing systemd services..."
|
||||
systemctl --user disable --now darktable-sync.timer >/dev/null 2>&1 || true
|
||||
if [[ -f "$CONFIG_DIR/.env" ]]; then
|
||||
# shellcheck source=/dev/null
|
||||
. "$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
|
||||
|
||||
# Remove files
|
||||
echo "🧹 Cleaning up installed files..."
|
||||
rm -fv \
|
||||
"$BIN_DIR/darktable_sync.sh" \
|
||||
"$BIN_DIR/darktable_wrapper.sh" \
|
||||
"$APPLICATIONS_DIR/darktable-with-sync.desktop" \
|
||||
"$APPLICATIONS_DIR/darktable-sync-only.desktop" \
|
||||
"$SYSTEMD_USER_DIR/darktable-sync.service" \
|
||||
"$SYSTEMD_USER_DIR/darktable-sync.timer"
|
||||
### Lockdir entfernen (atomares Lock)
|
||||
|
||||
echo "✅ Uninstall complete. Config files in ~/.config/darktable remain untouched."
|
||||
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 \
|
||||
"$BIN_DIR/darktable_common.sh" \
|
||||
"$BIN_DIR/darktable_sync.sh" \
|
||||
"$BIN_DIR/darktable_wrapper.sh" \
|
||||
"$APPLICATIONS_DIR/darktable-with-sync.desktop" \
|
||||
"$APPLICATIONS_DIR/darktable-sync-only.desktop" \
|
||||
"$SYSTEMD_USER_DIR/darktable-sync.service" \
|
||||
"$SYSTEMD_USER_DIR/darktable-sync.timer" \
|
||||
"$SYSTEMD_USER_DIR/darktable_sync.service" \
|
||||
"$SYSTEMD_USER_DIR/darktable_sync.timer"
|
||||
|
||||
### 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."
|
||||
|
||||
Reference in New Issue
Block a user