Robuste Darktable-Synchronisation: sequenzieller Ablauf, Versions- und Concurrent-Schutz

- Race Condition behoben: Pre-Sync wird vollstaendig abgewartet bevor Darktable startet
- Post-Sync nach Schliessen von Darktable eingefuehrt (bisher fehlend)
- .env aus festem Pfad ~/.config/darktable-sync/.env geladen (nicht mehr relativ)
- Server-Erreichbarkeit per SSH statt ping (Firewall-sicher)
- Darktable-Versionscheck (Major.Minor) vor Download mit Abbruch bei Konflikt
- DB-Backup vor jedem Download (library.db.bak, data.db.bak)
- sync_pending-Marker bei Offline/Fehler, Hinweis beim naechsten Start
- darktable.active-Marker auf Server fuer Concurrent-Erkennung
- Lock-Dateien vom Sync ausgeschlossen
- systemd-Timer entfernt, Service bleibt als manueller Trigger
- Gemeinsame Hilfsfunktionen in darktable_common.sh extrahiert
- 20 BATS-Tests mit vollstaendigem Stub-System ohne GUI-Dialoge

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-04-19 19:41:26 +02:00
parent 3bdd26ed81
commit 6a6ce52cf9
21 changed files with 777 additions and 201 deletions
+65
View File
@@ -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
}
+151 -86
View File
@@ -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" | wc -l)
;;
local log_file="$1" direction="$2" count=0
case "$direction" in
up) count=$(grep -cE '^>f|^cd' "$log_file" 2>/dev/null) || count=0 ;;
down) count=$(grep -cE '^<f|^cd' "$log_file" 2>/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
+71 -9
View File
@@ -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