From 6a6ce52cf95c8e35a1071e0d6399991313c6a3f8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Martin=20Tr=C3=B6ger?= Date: Sun, 19 Apr 2026 19:41:26 +0200 Subject: [PATCH 1/3] 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 --- .env.example | 21 +- desktop/darktable-sync-only.desktop | 4 +- desktop/darktable-with-sync.desktop | 2 +- install.sh | 197 ++++++++++----- scripts/darktable_common.sh | 65 +++++ scripts/darktable_sync.sh | 237 +++++++++++------- scripts/darktable_wrapper.sh | 80 +++++- ...le_sync.service => darktable-sync.service} | 2 +- systemd/darktable_sync.timer | 10 - tests/darktable_common.bats | 77 ++++++ tests/darktable_sync.bats | 59 +++++ tests/darktable_wrapper.bats | 78 ++++++ tests/helpers/setup.bash | 29 +++ tests/stubs/darktable | 7 + tests/stubs/kdialog | 4 + tests/stubs/notify-send | 3 + tests/stubs/pgrep | 8 + tests/stubs/rsync | 7 + tests/stubs/ssh | 7 + tests/stubs/zenity | 4 + uninstall.sh | 77 ++++-- 21 files changed, 777 insertions(+), 201 deletions(-) create mode 100644 scripts/darktable_common.sh rename systemd/{darktable_sync.service => darktable-sync.service} (60%) delete mode 100644 systemd/darktable_sync.timer create mode 100644 tests/darktable_common.bats create mode 100644 tests/darktable_sync.bats create mode 100644 tests/darktable_wrapper.bats create mode 100644 tests/helpers/setup.bash create mode 100755 tests/stubs/darktable create mode 100755 tests/stubs/kdialog create mode 100755 tests/stubs/notify-send create mode 100755 tests/stubs/pgrep create mode 100755 tests/stubs/rsync create mode 100755 tests/stubs/ssh create mode 100755 tests/stubs/zenity diff --git a/.env.example b/.env.example index 160e5e9..1318b52 100644 --- a/.env.example +++ b/.env.example @@ -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" diff --git a/desktop/darktable-sync-only.desktop b/desktop/darktable-sync-only.desktop index 876f387..ab1576e 100644 --- a/desktop/darktable-sync-only.desktop +++ b/desktop/darktable-sync-only.desktop @@ -1,7 +1,7 @@ [Desktop Entry] Type=Application -Name=Darktable sync only -Comment=Run Darktable sync without starting Darktable +Name=Darktable Sync +Comment=Nur Synchronisation ausfuehren ohne Darktable zu starten Exec=/home/%u/.local/bin/darktable_sync.sh --with-notify-start-stop Terminal=false Categories=Graphics;Photography; diff --git a/desktop/darktable-with-sync.desktop b/desktop/darktable-with-sync.desktop index 704a2ee..5f37879 100644 --- a/desktop/darktable-with-sync.desktop +++ b/desktop/darktable-with-sync.desktop @@ -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; diff --git a/install.sh b/install.sh index e634e31..f4095c0 100755 --- a/install.sh +++ b/install.sh @@ -2,127 +2,186 @@ 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." + echo " Bitte nach $CONFIG_ENV verschieben:" + echo " cp .env $CONFIG_ENV && chmod 600 $CONFIG_ENV" 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" +### Konfiguration anzeigen -### Check dependencies +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 "" -echo "Checking requirements..." +### Abhaengigkeiten pruefen -REQUIRED_CMDS=("rsync" "notify-send" "ping" "darktable" "systemctl" "xdg-user-dir") +echo "Abhaengigkeiten pruefen..." +REQUIRED_CMDS=("rsync" "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." + echo " Installieren mit: sudo apt install $cmd" + 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 + +if ! command -v bats >/dev/null 2>&1; then + echo "Hinweis: 'bats' nicht gefunden (nur fuer Tests benoetigt)." + echo " sudo apt install bats" +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 "WICHTIG: Konfiguration anpassen:" + echo " nano $CONFIG_ENV" +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" diff --git a/scripts/darktable_common.sh b/scripts/darktable_common.sh new file mode 100644 index 0000000..aa265ec --- /dev/null +++ b/scripts/darktable_common.sh @@ -0,0 +1,65 @@ +#!/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 + # 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_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 +} + +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 +} + +server_reachable() { + ssh -o ConnectTimeout=5 -o BatchMode=yes \ + -p "$SERVER_SSH_PORT" "$SERVER_USER@$SERVER_IP" true 2>/dev/null +} + +ask_user() { + local title="$1" text="$2" ans + if command -v zenity &>/dev/null; then + zenity --question --title="$title" --text="$text" 2>/dev/null + return $? + elif command -v kdialog &>/dev/null; then + kdialog --title "$title" --yesno "$text" 2>/dev/null + return $? + else + read -r -p "$text [j/N] " ans || true + [[ "$ans" =~ ^[jJyY] ]] + return $? + fi +} diff --git a/scripts/darktable_sync.sh b/scripts/darktable_sync.sh index 9511850..5e5eaeb 100755 --- a/scripts/darktable_sync.sh +++ b/scripts/darktable_sync.sh @@ -1,109 +1,174 @@ -#!/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" + +check_dependency rsync +check_dependency ssh openssh-client +check_dependency notify-send libnotify-bin +check_dependency darktable + +load_config +validate_config + +export DISPLAY="${DISPLAY:-:0}" log() { echo "[$(date '+%Y-%m-%d %H:%M:%S')] $*" } count_synced_files() { - local LOG="$1" - local DIRECTION="$2" - local COUNT=0 - - case "$DIRECTION" in - up) - COUNT=$(grep -E '^>f|cd' "$LOG" | wc -l) - ;; - down) - COUNT=$(grep -E '^f|^cd' "$log_file" 2>/dev/null) || count=0 ;; + down) count=$(grep -cE '^/dev/null) || count=0 ;; esac - echo "$COUNT" + echo "$count" } -SCRIPT_NAME=$(basename "$0") +SCRIPT_NAME="$(basename "$0")" LOCKFILE="/tmp/${SCRIPT_NAME}.lock" if [ -e "$LOCKFILE" ]; then - echo "Script is already running or delete $LOCKFILE" + echo "Script laeuft bereits oder Lockfile loeschen: $LOCKFILE" exit 1 fi touch "$LOCKFILE" -trap "rm -f '$LOCKFILE'" EXIT +TMPFILES=("$LOCKFILE") +trap 'rm -f "${TMPFILES[@]}"' EXIT SHOW_NOTIFY_START_STOP=false -if [[ "$1" == "--with-notify-start-stop" ]]; then +if [[ "${1:-}" == "--with-notify-start-stop" ]]; then SHOW_NOTIFY_START_STOP=true 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" - - if [ "$SHOW_NOTIFY_START_STOP" = true ]; then - notify-send "Darktable Sync" "Sync started..." -t 3000 - 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." +if [ -f "$CONFIG_DIR/sync_pending" ]; then + notify-send "Darktable Sync" "Ausstehender Sync wird jetzt ausgefuehrt..." -t 3000 +fi + +if ! server_reachable; then + log "Server nicht erreichbar – Sync uebersprungen." + touch "$CONFIG_DIR/sync_pending" + exit 0 +fi + +if [ "$SHOW_NOTIFY_START_STOP" = true ]; then + notify-send "Darktable Sync" "Sync gestartet..." -t 3000 +fi + +ACTIVE=$(ssh -o ConnectTimeout=5 -o BatchMode=yes \ + -p "$SERVER_SSH_PORT" "$SERVER_USER@$SERVER_IP" \ + "cat '$SERVER_DB_DIR/darktable.active' 2>/dev/null || true") +if [ -n "$ACTIVE" ]; then + notify-send "Darktable Sync – Warnung" \ + "Darktable laueft moeglicherweise auf: $ACTIVE" -u normal -t 10000 +fi + +SERVER_VERSION=$(ssh -o ConnectTimeout=5 -o BatchMode=yes \ + -p "$SERVER_SSH_PORT" "$SERVER_USER@$SERVER_IP" \ + "cat '$SERVER_DB_DIR/darktable_version' 2>/dev/null || true") + +LOCAL_VERSION=$(darktable --version 2>&1 | head -1) + +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 "WARNUNG: Darktable-Versionen unterschiedlich!" + log " Lokal: $LOCAL_VERSION" + log " Server: $SERVER_VERSION" + log " 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 +fi + +log "Datenbank-Backup erstellen..." +cp "$LOCAL_DARKTABLE_DB_DIR/library.db" "$LOCAL_DARKTABLE_DB_DIR/library.db.bak" +cp "$LOCAL_DARKTABLE_DB_DIR/data.db" "$LOCAL_DARKTABLE_DB_DIR/data.db.bak" + +SYNC_LOG=$(mktemp) +TMPFILES+=("$SYNC_LOG") + +log "Datenbank hochladen..." +UPLOAD_LOG_DB=$(mktemp) +TMPFILES+=("$UPLOAD_LOG_DB") +if ! rsync -uavh --itemize-changes \ + --exclude '*.lock' \ + --exclude 'darktable_version' \ + -e "ssh -p $SERVER_SSH_PORT" \ + "$LOCAL_DARKTABLE_DB_DIR/" "$SERVER_USER@$SERVER_IP:$SERVER_DB_DIR/" \ + 2>&1 | tee -a "$SYNC_LOG" "$UPLOAD_LOG_DB"; then + log "Fehler beim Hochladen der Datenbank." + touch "$CONFIG_DIR/sync_pending" + exit 1 +fi +SENT_DB=$(count_synced_files "$UPLOAD_LOG_DB" "up") + +log "Fotos hochladen..." +UPLOAD_LOG_PHOTOS=$(mktemp) +TMPFILES+=("$UPLOAD_LOG_PHOTOS") +if ! rsync -uavh --itemize-changes \ + --exclude '*.lock' \ + -e "ssh -p $SERVER_SSH_PORT" \ + "$LOCAL_PHOTO_DIR/" "$SERVER_USER@$SERVER_IP:$SERVER_PHOTO_DIR/" \ + 2>&1 | tee -a "$SYNC_LOG" "$UPLOAD_LOG_PHOTOS"; then + log "Fehler beim Hochladen der Fotos." + touch "$CONFIG_DIR/sync_pending" + exit 1 +fi +SENT_PHOTOS=$(count_synced_files "$UPLOAD_LOG_PHOTOS" "up") + +log "Datenbank herunterladen..." +DOWNLOAD_LOG_DB=$(mktemp) +TMPFILES+=("$DOWNLOAD_LOG_DB") +if ! rsync -uavh --itemize-changes \ + --exclude '*.lock' \ + -e "ssh -p $SERVER_SSH_PORT" \ + "$SERVER_USER@$SERVER_IP:$SERVER_DB_DIR/" "$LOCAL_DARKTABLE_DB_DIR/" \ + 2>&1 | tee -a "$SYNC_LOG" "$DOWNLOAD_LOG_DB"; then + log "Fehler beim Herunterladen der Datenbank." + touch "$CONFIG_DIR/sync_pending" + exit 1 +fi +RECEIVED_DB=$(count_synced_files "$DOWNLOAD_LOG_DB" "down") + +log "Fotos herunterladen..." +DOWNLOAD_LOG_PHOTOS=$(mktemp) +TMPFILES+=("$DOWNLOAD_LOG_PHOTOS") +if ! rsync -uavh --itemize-changes \ + --exclude '*.lock' \ + -e "ssh -p $SERVER_SSH_PORT" \ + "$SERVER_USER@$SERVER_IP:$SERVER_PHOTO_DIR/" "$LOCAL_PHOTO_DIR/" \ + 2>&1 | tee -a "$SYNC_LOG" "$DOWNLOAD_LOG_PHOTOS"; then + log "Fehler beim Herunterladen der Fotos." + touch "$CONFIG_DIR/sync_pending" + exit 1 +fi +RECEIVED_PHOTOS=$(count_synced_files "$DOWNLOAD_LOG_PHOTOS" "down") + +echo "$LOCAL_VERSION" > "$LOCAL_DARKTABLE_DB_DIR/darktable_version" + +rm -f "$CONFIG_DIR/sync_pending" + +TOTAL_SENT=$((SENT_DB + SENT_PHOTOS)) +TOTAL_RECEIVED=$((RECEIVED_DB + RECEIVED_PHOTOS)) + +if [ "$TOTAL_SENT" -gt 0 ] || [ "$TOTAL_RECEIVED" -gt 0 ]; then + log "Hochgeladen: $TOTAL_SENT Dateien" + log "Heruntergeladen: $TOTAL_RECEIVED Dateien" + notify-send "Darktable Sync" \ + "↑ $TOTAL_SENT hochgeladen | ↓ $TOTAL_RECEIVED heruntergeladen" -t 10000 +else + log "Keine Aenderungen." +fi + +if [ "$SHOW_NOTIFY_START_STOP" = true ]; then + notify-send "Darktable Sync" "Sync abgeschlossen." -t 3000 fi diff --git a/scripts/darktable_wrapper.sh b/scripts/darktable_wrapper.sh index 2f23b94..e6d026b 100755 --- a/scripts/darktable_wrapper.sh +++ b/scripts/darktable_wrapper.sh @@ -1,12 +1,74 @@ -#!/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 & +check_dependency darktable +check_dependency ssh openssh-client +check_dependency notify-send libnotify-bin -# Darktable starten -exec "$DARKTABLE_BIN" "$@" +load_config +validate_config + +export DISPLAY="${DISPLAY:-:0}" + +log() { + echo "[$(date '+%Y-%m-%d %H:%M:%S')] $*" +} + +if pgrep -x darktable &>/dev/null; then + notify-send "Darktable" \ + "Darktable laeuft bereits. Bitte zuerst schliessen." -u critical + exit 1 +fi + +ACTIVE_MARKER_SET=false + +cleanup() { + if [ "$ACTIVE_MARKER_SET" = true ]; then + ssh -o ConnectTimeout=5 -o BatchMode=yes \ + -p "$SERVER_SSH_PORT" "$SERVER_USER@$SERVER_IP" \ + "rm -f '$SERVER_DB_DIR/darktable.active'" 2>/dev/null || true + fi +} +trap cleanup EXIT INT TERM + +if ! server_reachable; then + 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..." +else + log "Pre-Sync..." + "$SYNC_BIN" + + MARKER="$(hostname) seit $(date '+%Y-%m-%d %H:%M:%S')" + ssh -o ConnectTimeout=5 -o BatchMode=yes \ + -p "$SERVER_SSH_PORT" "$SERVER_USER@$SERVER_IP" \ + "echo '$MARKER' > '$SERVER_DB_DIR/darktable.active'" || true + ACTIVE_MARKER_SET=true +fi + +log "Starte Darktable..." +"$DARKTABLE_BIN" "$@" || true +log "Darktable beendet." + +if [ "$ACTIVE_MARKER_SET" = true ]; then + ssh -o ConnectTimeout=5 -o BatchMode=yes \ + -p "$SERVER_SSH_PORT" "$SERVER_USER@$SERVER_IP" \ + "rm -f '$SERVER_DB_DIR/darktable.active'" 2>/dev/null || true + ACTIVE_MARKER_SET=false +fi + +if server_reachable; then + log "Post-Sync..." + "$SYNC_BIN" +else + touch "$CONFIG_DIR/sync_pending" + notify-send "Darktable Sync" \ + "Server nicht erreichbar – Sync ausstehend." -t 5000 +fi diff --git a/systemd/darktable_sync.service b/systemd/darktable-sync.service similarity index 60% rename from systemd/darktable_sync.service rename to systemd/darktable-sync.service index 6b25ead..bdaede5 100644 --- a/systemd/darktable_sync.service +++ b/systemd/darktable-sync.service @@ -1,5 +1,5 @@ [Unit] -Description=Darktable sync service +Description=Darktable Sync (manueller Trigger) [Service] Type=oneshot diff --git a/systemd/darktable_sync.timer b/systemd/darktable_sync.timer deleted file mode 100644 index e24468e..0000000 --- a/systemd/darktable_sync.timer +++ /dev/null @@ -1,10 +0,0 @@ -[Unit] -Description=Run Darktable sync periodically - -[Timer] -OnCalendar=*-*-* *:00:00 -Persistent=true -Unit=darktable-sync.service - -[Install] -WantedBy=timers.target diff --git a/tests/darktable_common.bats b/tests/darktable_common.bats new file mode 100644 index 0000000..eb2e3da --- /dev/null +++ b/tests/darktable_common.bats @@ -0,0 +1,77 @@ +#!/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 ] +} diff --git a/tests/darktable_sync.bats b/tests/darktable_sync.bats new file mode 100644 index 0000000..93fc27b --- /dev/null +++ b/tests/darktable_sync.bats @@ -0,0 +1,59 @@ +#!/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" + mkdir -p "$HOME/Pictures" + export DISPLAY=:99 +} + +@test "sync_pending wird gesetzt wenn Server nicht erreichbar" { + run_with_stubs env SSH_STUB_FAIL=1 bash "$SYNC_SCRIPT" + [ "$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" + [ "$status" -eq 0 ] + [ ! -f "$CONFIG_DIR/sync_pending" ] +} + +@test "sync_pending wird gesetzt wenn rsync fehlschlaegt" { + run_with_stubs env SSH_STUB_FAIL=0 RSYNC_STUB_FAIL=1 bash "$SYNC_SCRIPT" + [ "$status" -eq 1 ] + [ -f "$CONFIG_DIR/sync_pending" ] +} + +@test "DB-Backup wird vor Download erstellt" { + run_with_stubs env SSH_STUB_FAIL=0 bash "$SYNC_SCRIPT" + [ "$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" 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" bash "$SYNC_SCRIPT" + [ "$status" -eq 1 ] + [ -f "$CONFIG_DIR/sync_pending" ] +} + +@test "Lockfile wird nach Abschluss entfernt" { + run_with_stubs env SSH_STUB_FAIL=0 bash "$SYNC_SCRIPT" + [ "$status" -eq 0 ] + [ ! -f "/tmp/darktable_sync.sh.lock" ] +} diff --git a/tests/darktable_wrapper.bats b/tests/darktable_wrapper.bats new file mode 100644 index 0000000..e6a5c33 --- /dev/null +++ b/tests/darktable_wrapper.bats @@ -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" < "$SSH_CALL_COUNT" + + cat > "$LOCAL_STUBS/ssh" < "$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 ] +} diff --git a/tests/helpers/setup.bash b/tests/helpers/setup.bash new file mode 100644 index 0000000..d167205 --- /dev/null +++ b/tests/helpers/setup.bash @@ -0,0 +1,29 @@ +# 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" </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" +### Lockfile entfernen -echo "✅ Uninstall complete. Config files in ~/.config/darktable remain untouched." +LOCKFILE="/tmp/darktable_sync.sh.lock" +if [ -f "$LOCKFILE" ]; then + echo "Lockfile entfernen: $LOCKFILE" + rm -f "$LOCKFILE" +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." From 46664ab3b6bc3312f8f0eac92887e91cec7ecb37 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Martin=20Tr=C3=B6ger?= Date: Sun, 19 Apr 2026 19:45:11 +0200 Subject: [PATCH 2/3] Code-Vereinfachung: Redundanzen entfernen und Wiederverwendung verbessern MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 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 --- scripts/darktable_common.sh | 12 ++++++++++-- scripts/darktable_sync.sh | 12 ++---------- scripts/darktable_wrapper.sh | 16 +++------------- 3 files changed, 15 insertions(+), 25 deletions(-) diff --git a/scripts/darktable_common.sh b/scripts/darktable_common.sh index aa265ec..5e670da 100644 --- a/scripts/darktable_common.sh +++ b/scripts/darktable_common.sh @@ -44,9 +44,17 @@ check_dependency() { fi } -server_reachable() { +log() { + echo "[$(date '+%Y-%m-%d %H:%M:%S')] $*" +} + +ssh_server() { ssh -o ConnectTimeout=5 -o BatchMode=yes \ - -p "$SERVER_SSH_PORT" "$SERVER_USER@$SERVER_IP" true 2>/dev/null + -p "$SERVER_SSH_PORT" "$SERVER_USER@$SERVER_IP" "$@" +} + +server_reachable() { + ssh_server true 2>/dev/null } ask_user() { diff --git a/scripts/darktable_sync.sh b/scripts/darktable_sync.sh index 5e5eaeb..a10b9bd 100755 --- a/scripts/darktable_sync.sh +++ b/scripts/darktable_sync.sh @@ -15,10 +15,6 @@ validate_config export DISPLAY="${DISPLAY:-:0}" -log() { - echo "[$(date '+%Y-%m-%d %H:%M:%S')] $*" -} - count_synced_files() { local log_file="$1" direction="$2" count=0 case "$direction" in @@ -59,17 +55,13 @@ if [ "$SHOW_NOTIFY_START_STOP" = true ]; then notify-send "Darktable Sync" "Sync gestartet..." -t 3000 fi -ACTIVE=$(ssh -o ConnectTimeout=5 -o BatchMode=yes \ - -p "$SERVER_SSH_PORT" "$SERVER_USER@$SERVER_IP" \ - "cat '$SERVER_DB_DIR/darktable.active' 2>/dev/null || true") +ACTIVE=$(ssh_server "cat '$SERVER_DB_DIR/darktable.active' 2>/dev/null || true") if [ -n "$ACTIVE" ]; then notify-send "Darktable Sync – Warnung" \ "Darktable laueft moeglicherweise auf: $ACTIVE" -u normal -t 10000 fi -SERVER_VERSION=$(ssh -o ConnectTimeout=5 -o BatchMode=yes \ - -p "$SERVER_SSH_PORT" "$SERVER_USER@$SERVER_IP" \ - "cat '$SERVER_DB_DIR/darktable_version' 2>/dev/null || true") +SERVER_VERSION=$(ssh_server "cat '$SERVER_DB_DIR/darktable_version' 2>/dev/null || true") LOCAL_VERSION=$(darktable --version 2>&1 | head -1) diff --git a/scripts/darktable_wrapper.sh b/scripts/darktable_wrapper.sh index e6d026b..608184a 100755 --- a/scripts/darktable_wrapper.sh +++ b/scripts/darktable_wrapper.sh @@ -14,10 +14,6 @@ validate_config export DISPLAY="${DISPLAY:-:0}" -log() { - echo "[$(date '+%Y-%m-%d %H:%M:%S')] $*" -} - if pgrep -x darktable &>/dev/null; then notify-send "Darktable" \ "Darktable laeuft bereits. Bitte zuerst schliessen." -u critical @@ -28,9 +24,7 @@ ACTIVE_MARKER_SET=false cleanup() { if [ "$ACTIVE_MARKER_SET" = true ]; then - ssh -o ConnectTimeout=5 -o BatchMode=yes \ - -p "$SERVER_SSH_PORT" "$SERVER_USER@$SERVER_IP" \ - "rm -f '$SERVER_DB_DIR/darktable.active'" 2>/dev/null || true + ssh_server "rm -f '$SERVER_DB_DIR/darktable.active'" 2>/dev/null || true fi } trap cleanup EXIT INT TERM @@ -47,9 +41,7 @@ else "$SYNC_BIN" MARKER="$(hostname) seit $(date '+%Y-%m-%d %H:%M:%S')" - ssh -o ConnectTimeout=5 -o BatchMode=yes \ - -p "$SERVER_SSH_PORT" "$SERVER_USER@$SERVER_IP" \ - "echo '$MARKER' > '$SERVER_DB_DIR/darktable.active'" || true + ssh_server "echo '$MARKER' > '$SERVER_DB_DIR/darktable.active'" || true ACTIVE_MARKER_SET=true fi @@ -58,9 +50,7 @@ log "Starte Darktable..." log "Darktable beendet." if [ "$ACTIVE_MARKER_SET" = true ]; then - ssh -o ConnectTimeout=5 -o BatchMode=yes \ - -p "$SERVER_SSH_PORT" "$SERVER_USER@$SERVER_IP" \ - "rm -f '$SERVER_DB_DIR/darktable.active'" 2>/dev/null || true + ssh_server "rm -f '$SERVER_DB_DIR/darktable.active'" 2>/dev/null || true ACTIVE_MARKER_SET=false fi From 92a5d500823bce77025fbc9fe71d2e26a383f091 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Martin=20Tr=C3=B6ger?= Date: Sun, 19 Apr 2026 19:57:39 +0200 Subject: [PATCH 3/3] Sicherheitshaertung: Injection-Schutz, atomares Locking, Pfad-Validierung MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 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 --- scripts/darktable_common.sh | 32 ++++++++ scripts/darktable_sync.sh | 14 ++-- tests/darktable_sync.bats | 4 +- tests/security.bats | 147 ++++++++++++++++++++++++++++++++++++ uninstall.sh | 10 +-- 5 files changed, 192 insertions(+), 15 deletions(-) create mode 100644 tests/security.bats diff --git a/scripts/darktable_common.sh b/scripts/darktable_common.sh index 5e670da..edea257 100644 --- a/scripts/darktable_common.sh +++ b/scripts/darktable_common.sh @@ -11,6 +11,20 @@ load_config() { 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" } @@ -23,6 +37,15 @@ require_var() { 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 @@ -33,6 +56,15 @@ validate_config() { require_var LOCAL_PHOTO_DIR require_var SYNC_BIN require_var DARKTABLE_BIN + + validate_path SERVER_DB_DIR + validate_path SERVER_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() { diff --git a/scripts/darktable_sync.sh b/scripts/darktable_sync.sh index a10b9bd..1da50c0 100755 --- a/scripts/darktable_sync.sh +++ b/scripts/darktable_sync.sh @@ -24,17 +24,15 @@ count_synced_files() { echo "$count" } -SCRIPT_NAME="$(basename "$0")" -LOCKFILE="/tmp/${SCRIPT_NAME}.lock" +LOCKDIR="$CONFIG_DIR/sync.lock" +TMPFILES=() -if [ -e "$LOCKFILE" ]; then - echo "Script laeuft bereits oder Lockfile loeschen: $LOCKFILE" +# Atomares Locking per mkdir (verhindert Race Condition und Symlink-Attacken) +if ! mkdir "$LOCKDIR" 2>/dev/null; then + echo "Script laeuft bereits oder Lockdir loeschen: $LOCKDIR" exit 1 fi - -touch "$LOCKFILE" -TMPFILES=("$LOCKFILE") -trap 'rm -f "${TMPFILES[@]}"' EXIT +trap 'rm -f "${TMPFILES[@]}"; rmdir "$LOCKDIR" 2>/dev/null || true' EXIT SHOW_NOTIFY_START_STOP=false if [[ "${1:-}" == "--with-notify-start-stop" ]]; then diff --git a/tests/darktable_sync.bats b/tests/darktable_sync.bats index 93fc27b..2ed8be8 100644 --- a/tests/darktable_sync.bats +++ b/tests/darktable_sync.bats @@ -52,8 +52,8 @@ setup() { [ -f "$CONFIG_DIR/sync_pending" ] } -@test "Lockfile wird nach Abschluss entfernt" { +@test "Lockdir wird nach Abschluss entfernt" { run_with_stubs env SSH_STUB_FAIL=0 bash "$SYNC_SCRIPT" [ "$status" -eq 0 ] - [ ! -f "/tmp/darktable_sync.sh.lock" ] + [ ! -d "$CONFIG_DIR/sync.lock" ] } diff --git a/tests/security.bats b/tests/security.bats new file mode 100644 index 0000000..3b2f6bd --- /dev/null +++ b/tests/security.bats @@ -0,0 +1,147 @@ +#!/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" + 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" == *"laeuft 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" ] +} diff --git a/uninstall.sh b/uninstall.sh index 59924b2..0a9ce83 100755 --- a/uninstall.sh +++ b/uninstall.sh @@ -19,12 +19,12 @@ 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 -### Lockfile entfernen +### Lockdir entfernen (atomares Lock) -LOCKFILE="/tmp/darktable_sync.sh.lock" -if [ -f "$LOCKFILE" ]; then - echo "Lockfile entfernen: $LOCKFILE" - rm -f "$LOCKFILE" +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)