Files
darktable-sync/scripts/darktable_sync.sh
T
martin 9f401e48e9 fix: Trockenlauf-Zählwerte korrigieren und Fehlerbehandlung verbessern
- count_changes_in_rsync_output: Regulärer Ausdruck für gelöschte Dateien korrigiert (wildcard-Escaping)
- Doppel-Checks entfernt, Fehlerausgaben standardisiert
- Dry-Run-Output-Parsing zu gemeinsamen Funktionen verschoben
- CLAUDE.md mit präziseren Anforderungen an Fehlerbehandlung aktualisiert

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-21 17:28:11 +02:00

340 lines
12 KiB
Bash
Executable File
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
#!/usr/bin/env bash
set -euo pipefail
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
# shellcheck source=darktable_common.sh
source "$SCRIPT_DIR/darktable_common.sh"
log_step "Darktable Sync gestartet (PID $$, Argumente: ${*:-keine})"
check_dependency rsync
check_dependency ssh openssh-client
check_dependency notify-send libnotify-bin
check_dependency darktable
log "Alle Abhängigkeiten vorhanden."
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
count_synced_files() {
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"
}
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 "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"
UPLOAD_ALLOWED=true
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."
;;
abort)
log "Sync abgebrochen durch Benutzer."
exit 0
;;
*)
log "Nur Download Server-Stand wird übernommen."
UPLOAD_ALLOWED=false
;;
esac
else
log "Token stimmt überein Upload erlaubt."
fi
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
SYNC_LOG=$(mktemp)
TMPFILES+=("$SYNC_LOG")
UPLOAD_LOG_DB=$(mktemp)
TMPFILES+=("$UPLOAD_LOG_DB")
UPLOAD_LOG_PHOTOS=$(mktemp)
TMPFILES+=("$UPLOAD_LOG_PHOTOS")
DOWNLOAD_LOG_DB=$(mktemp)
TMPFILES+=("$DOWNLOAD_LOG_DB")
DOWNLOAD_LOG_PHOTOS=$(mktemp)
TMPFILES+=("$DOWNLOAD_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
RSYNC_DRY_FLAG=()
[ "$DRY_RUN" = true ] && RSYNC_DRY_FLAG=(--dry-run)
[ "$DRY_RUN" = true ] && DRY_SUFFIX=" (Trockenlauf)" || DRY_SUFFIX=""
SENT_DB=0
SENT_PHOTOS=0
if [ "$UPLOAD_ALLOWED" = true ]; then
log_step "Upload: Datenbank${DRY_SUFFIX}"
log " Quelle: $LOCAL_DARKTABLE_DB_DIR/"
log " Ziel: $SERVER_USER@$SERVER_IP:$SERVER_DB_DIR/"
if [ -z "$(ls -A "$LOCAL_DARKTABLE_DB_DIR" 2>/dev/null)" ]; then
log_error "Upload abgebrochen: Quellverzeichnis leer ($LOCAL_DARKTABLE_DB_DIR). Falscher Mount?"
touch "$CONFIG_DIR/sync_pending"
exit 1
fi
if ! rsync -uavh --itemize-changes --delete \
"${RSYNC_DRY_FLAG[@]}" \
--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_error "Upload Datenbank fehlgeschlagen (Quelle: $LOCAL_DARKTABLE_DB_DIR)"
touch "$CONFIG_DIR/sync_pending"
exit 1
fi
SENT_DB=$(count_synced_files "$UPLOAD_LOG_DB" "up")
log "Datenbank-Upload abgeschlossen: $SENT_DB Datei(en) übertragen."
log_step "Upload: Fotos${DRY_SUFFIX}"
log " Quelle: $LOCAL_PHOTO_DIR/"
log " Ziel: $SERVER_USER@$SERVER_IP:$SERVER_PHOTO_DIR/"
if [ -z "$(ls -A "$LOCAL_PHOTO_DIR" 2>/dev/null)" ]; then
log_error "Upload abgebrochen: Quellverzeichnis leer ($LOCAL_PHOTO_DIR). Falscher Mount?"
touch "$CONFIG_DIR/sync_pending"
exit 1
fi
if ! rsync -uavh --itemize-changes --delete \
"${RSYNC_DRY_FLAG[@]}" \
--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_error "Upload Fotos fehlgeschlagen (Quelle: $LOCAL_PHOTO_DIR)"
touch "$CONFIG_DIR/sync_pending"
exit 1
fi
SENT_PHOTOS=$(count_synced_files "$UPLOAD_LOG_PHOTOS" "up")
log "Foto-Upload abgeschlossen: $SENT_PHOTOS Datei(en) übertragen."
else
log "Upload übersprungen (Token-Konflikt)."
fi
log_step "Download: Datenbank${DRY_SUFFIX}"
log " Quelle: $SERVER_USER@$SERVER_IP:$SERVER_DB_DIR/"
log " Ziel: $LOCAL_DARKTABLE_DB_DIR/"
log " Backup: $BACKUP_DB_DIR/"
if ! rsync -uavh --itemize-changes --delete \
--backup --backup-dir="$BACKUP_DB_DIR" \
"${RSYNC_DRY_FLAG[@]}" \
--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_error "Download Datenbank fehlgeschlagen (Ziel: $LOCAL_DARKTABLE_DB_DIR)"
touch "$CONFIG_DIR/sync_pending"
exit 1
fi
RECEIVED_DB=$(count_synced_files "$DOWNLOAD_LOG_DB" "down")
log "Datenbank-Download abgeschlossen: $RECEIVED_DB Datei(en) empfangen."
log_step "Download: Fotos${DRY_SUFFIX}"
log " Quelle: $SERVER_USER@$SERVER_IP:$SERVER_PHOTO_DIR/"
log " Ziel: $LOCAL_PHOTO_DIR/"
log " Backup: $BACKUP_PHOTO_DIR/"
if ! rsync -uavh --itemize-changes --delete \
--backup --backup-dir="$BACKUP_PHOTO_DIR" \
"${RSYNC_DRY_FLAG[@]}" \
--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_error "Download Fotos fehlgeschlagen (Ziel: $LOCAL_PHOTO_DIR)"
touch "$CONFIG_DIR/sync_pending"
exit 1
fi
RECEIVED_PHOTOS=$(count_synced_files "$DOWNLOAD_LOG_PHOTOS" "down")
log "Foto-Download abgeschlossen: $RECEIVED_PHOTOS Datei(en) empfangen."
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
TOTAL_SENT=$((SENT_DB + SENT_PHOTOS))
TOTAL_RECEIVED=$((RECEIVED_DB + RECEIVED_PHOTOS))
if [ "$DRY_RUN" = true ]; then
upload_log=$(cat "$UPLOAD_LOG_DB" "$UPLOAD_LOG_PHOTOS" 2>/dev/null || true)
download_log=$(cat "$DOWNLOAD_LOG_DB" "$DOWNLOAD_LOG_PHOTOS" 2>/dev/null || true)
UP_NEW=$( echo "$upload_log" | grep -cE '^<f[+]{9}' || true)
UP_UPD=$( echo "$upload_log" | grep -E '^<f' | grep -cvE '^<f[+]{9}' || true)
UP_DEL=$( echo "$upload_log" | grep -cE '^\*deleting' || true)
DN_NEW=$( echo "$download_log" | grep -cE '^>f[+]{9}' || true)
DN_UPD=$( echo "$download_log" | grep -E '^>f' | grep -cvE '^>f[+]{9}' || true)
DN_DEL=$( echo "$download_log" | grep -cE '^\*deleting' || true)
log_step "Trockenlauf-Ergebnis"
log " Upload: $UP_NEW neu | $UP_UPD aktualisiert | $UP_DEL gelöscht"
log " Download: $DN_NEW neu | $DN_UPD aktualisiert | $DN_DEL gelöscht → Backup: $BACKUP_PHOTO_DIR"
if [ "$((UP_NEW + UP_UPD + UP_DEL + DN_NEW + DN_UPD + DN_DEL))" -gt 0 ]; then
if ask_user "Details" "Details der zu übertragenden Dateien anzeigen?"; then
format_rsync_details "$UPLOAD_LOG_DB" "Upload" "up"
format_rsync_details "$UPLOAD_LOG_PHOTOS" "Upload" "up"
format_rsync_details "$DOWNLOAD_LOG_DB" "Download" "down"
format_rsync_details "$DOWNLOAD_LOG_PHOTOS" "Download" "down"
fi
else
log "Keine Änderungen alles aktuell."
fi
else
log_step "Sync abgeschlossen"
log " Hochgeladen: $TOTAL_SENT ($SENT_DB DB + $SENT_PHOTOS Fotos)"
log " Heruntergeladen: $TOTAL_RECEIVED ($RECEIVED_DB DB + $RECEIVED_PHOTOS Fotos)"
if [ "$TOTAL_SENT" -gt 0 ] || [ "$TOTAL_RECEIVED" -gt 0 ]; then
notify-send "Darktable Sync" \
"$TOTAL_SENT hochgeladen | ↓ $TOTAL_RECEIVED heruntergeladen" -t 10000
else
log "Keine Änderungen alles aktuell."
fi
if [ "$SHOW_NOTIFY_START_STOP" = true ]; then
notify-send "Darktable Sync" "Sync abgeschlossen." -t 3000
fi
fi