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:
@@ -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
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user