From c05f3236055e77adef9c87abc97d3669d853d65c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Martin=20Tr=C3=B6ger?= Date: Mon, 20 Apr 2026 16:24:31 +0200 Subject: [PATCH] =?UTF-8?q?feat:=20Trockenlauf=20als=20Standard-Aufruf,=20?= =?UTF-8?q?--execute/-e=20f=C3=BCr=20echten=20Sync?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Ohne Flags führt darktable_sync.sh jetzt einen Trockenlauf durch: - Banner mit Hinweis und Bestätigungsabfrage vor dem Start - rsync läuft mit --dry-run (keine Dateiänderungen) - Keine destruktiven Operationen: kein Backup, kein Token-Schreiben, kein sync_pending entfernen - Zusammenfassung nach Richtung (Upload/Download) und Aktion (neu/aktualisiert/gelöscht) aufgeschlüsselt - Optionale Detailansicht: Dateien gruppiert nach Typ (Foto, XMP, Datenbank, Video, Sonstiges) Mit --execute oder -e wird der echte Sync wie bisher ausgeführt. Desktop-Entry und Systemd-Service auf --execute aktualisiert. Co-Authored-By: Claude Sonnet 4.6 --- desktop/darktable-sync-only.desktop | 2 +- scripts/darktable_common.sh | 45 ++++++++++ scripts/darktable_sync.sh | 127 +++++++++++++++++++--------- systemd/darktable-sync.service | 2 +- tests/darktable_sync.bats | 66 +++++++++++++-- tests/stubs/rsync | 7 ++ 6 files changed, 202 insertions(+), 47 deletions(-) diff --git a/desktop/darktable-sync-only.desktop b/desktop/darktable-sync-only.desktop index ab1576e..5922e03 100644 --- a/desktop/darktable-sync-only.desktop +++ b/desktop/darktable-sync-only.desktop @@ -2,6 +2,6 @@ Type=Application Name=Darktable Sync Comment=Nur Synchronisation ausfuehren ohne Darktable zu starten -Exec=/home/%u/.local/bin/darktable_sync.sh --with-notify-start-stop +Exec=/home/%u/.local/bin/darktable_sync.sh --execute --with-notify-start-stop Terminal=false Categories=Graphics;Photography; diff --git a/scripts/darktable_common.sh b/scripts/darktable_common.sh index 0c93b8c..6b7cedf 100644 --- a/scripts/darktable_common.sh +++ b/scripts/darktable_common.sh @@ -88,6 +88,51 @@ log_error() { echo "FEHLER: $*" >&2 } +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_rsync_details() { + local log_file="$1" direction_label="$2" direction="$3" + [ -f "$log_file" ] || return 0 + local prefix; [ "$direction" = "up" ] && prefix=">f" || prefix="/dev/null \ + | sed 's/^[^ ]* *//' | sort) || true + [ -n "$files" ] || continue + + local typ typed + for typ in Foto XMP Datenbank Video Sonstiges; do + typed=$(echo "$files" | while IFS= read -r f; do + [ "$(classify_filetype "$f")" = "$typ" ] && echo " $f" + done) + [ -n "$typed" ] || continue + log_step "$direction_label – $typ ($label)" + echo "$typed" + done + done < "$LOCKPID" log "Lock erworben (PID $$)." trap 'rm -f "${TMPFILES[@]}" "$LOCKPID"; rmdir "$LOCKDIR" 2>/dev/null || true; log "Lock freigegeben."' EXIT -SHOW_NOTIFY_START_STOP=false -if [[ "${1:-}" == "--with-notify-start-stop" ]]; then - SHOW_NOTIFY_START_STOP=true +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..." @@ -145,26 +160,39 @@ else log "Token stimmt überein – Upload erlaubt." fi -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." +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") + +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" + log_step "Upload: Datenbank${DRY_SUFFIX}" log " Quelle: $LOCAL_DARKTABLE_DB_DIR/" log " Ziel: $SERVER_USER@$SERVER_IP:$SERVER_DB_DIR/" - UPLOAD_LOG_DB=$(mktemp) - TMPFILES+=("$UPLOAD_LOG_DB") if ! rsync -uavh --itemize-changes \ + "${RSYNC_DRY_FLAG[@]}" \ --exclude '*.lock' \ --exclude 'darktable_version' \ -e "ssh -p $SERVER_SSH_PORT" \ @@ -177,12 +205,11 @@ if [ "$UPLOAD_ALLOWED" = true ]; then SENT_DB=$(count_synced_files "$UPLOAD_LOG_DB" "up") log "Datenbank-Upload abgeschlossen: $SENT_DB Datei(en) übertragen." - log_step "Upload: Fotos" + log_step "Upload: Fotos${DRY_SUFFIX}" log " Quelle: $LOCAL_PHOTO_DIR/" log " Ziel: $SERVER_USER@$SERVER_IP:$SERVER_PHOTO_DIR/" - UPLOAD_LOG_PHOTOS=$(mktemp) - TMPFILES+=("$UPLOAD_LOG_PHOTOS") if ! rsync -uavh --itemize-changes \ + "${RSYNC_DRY_FLAG[@]}" \ --exclude '*.lock' \ -e "ssh -p $SERVER_SSH_PORT" \ "$LOCAL_PHOTO_DIR/" "$SERVER_USER@$SERVER_IP:$SERVER_PHOTO_DIR/" \ @@ -197,12 +224,11 @@ else log "Upload übersprungen (Token-Konflikt)." fi -log_step "Download: Datenbank" +log_step "Download: Datenbank${DRY_SUFFIX}" log " Quelle: $SERVER_USER@$SERVER_IP:$SERVER_DB_DIR/" log " Ziel: $LOCAL_DARKTABLE_DB_DIR/" -DOWNLOAD_LOG_DB=$(mktemp) -TMPFILES+=("$DOWNLOAD_LOG_DB") if ! rsync -uavh --itemize-changes \ + "${RSYNC_DRY_FLAG[@]}" \ --exclude '*.lock' \ -e "ssh -p $SERVER_SSH_PORT" \ "$SERVER_USER@$SERVER_IP:$SERVER_DB_DIR/" "$LOCAL_DARKTABLE_DB_DIR/" \ @@ -214,12 +240,11 @@ fi RECEIVED_DB=$(count_synced_files "$DOWNLOAD_LOG_DB" "down") log "Datenbank-Download abgeschlossen: $RECEIVED_DB Datei(en) empfangen." -log_step "Download: Fotos" +log_step "Download: Fotos${DRY_SUFFIX}" log " Quelle: $SERVER_USER@$SERVER_IP:$SERVER_PHOTO_DIR/" log " Ziel: $LOCAL_PHOTO_DIR/" -DOWNLOAD_LOG_PHOTOS=$(mktemp) -TMPFILES+=("$DOWNLOAD_LOG_PHOTOS") if ! rsync -uavh --itemize-changes \ + "${RSYNC_DRY_FLAG[@]}" \ --exclude '*.lock' \ -e "ssh -p $SERVER_SSH_PORT" \ "$SERVER_USER@$SERVER_IP:$SERVER_PHOTO_DIR/" "$LOCAL_PHOTO_DIR/" \ @@ -231,30 +256,56 @@ fi RECEIVED_PHOTOS=$(count_synced_files "$DOWNLOAD_LOG_PHOTOS" "down") log "Foto-Download abgeschlossen: $RECEIVED_PHOTOS Datei(en) empfangen." -NEW_TOKEN=$(server_db_mtime) -save_sync_token "$NEW_TOKEN" -log "Sync-Token gespeichert: $NEW_TOKEN" +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" + 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." + rm -f "$CONFIG_DIR/sync_pending" + log "sync_pending entfernt." +fi TOTAL_SENT=$((SENT_DB + SENT_PHOTOS)) TOTAL_RECEIVED=$((RECEIVED_DB + RECEIVED_PHOTOS)) -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 [ "$DRY_RUN" = true ]; then + UP_NEW=$( cat "$UPLOAD_LOG_DB" "$UPLOAD_LOG_PHOTOS" 2>/dev/null | grep -cE '^>f[+]{9}' || echo 0) + UP_UPD=$( cat "$UPLOAD_LOG_DB" "$UPLOAD_LOG_PHOTOS" 2>/dev/null | grep -E '^>f' | grep -cvE '^>f[+]{9}' || echo 0) + UP_DEL=$( cat "$UPLOAD_LOG_DB" "$UPLOAD_LOG_PHOTOS" 2>/dev/null | grep -cE '^\*deleting' || echo 0) + DN_NEW=$( cat "$DOWNLOAD_LOG_DB" "$DOWNLOAD_LOG_PHOTOS" 2>/dev/null | grep -cE '^/dev/null | grep -E '^/dev/null | grep -cE '^\*deleting' || echo 0) -if [ "$TOTAL_SENT" -gt 0 ] || [ "$TOTAL_RECEIVED" -gt 0 ]; then - notify-send "Darktable Sync" \ - "↑ $TOTAL_SENT hochgeladen | ↓ $TOTAL_RECEIVED heruntergeladen" -t 10000 + 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" + + if [ "$((TOTAL_SENT + TOTAL_RECEIVED))" -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 "Keine Änderungen – alles aktuell." -fi + 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 [ "$SHOW_NOTIFY_START_STOP" = true ]; then - notify-send "Darktable Sync" "Sync abgeschlossen." -t 3000 + 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 diff --git a/systemd/darktable-sync.service b/systemd/darktable-sync.service index bdaede5..bbd29d6 100644 --- a/systemd/darktable-sync.service +++ b/systemd/darktable-sync.service @@ -3,4 +3,4 @@ Description=Darktable Sync (manueller Trigger) [Service] Type=oneshot -ExecStart=%h/.local/bin/darktable_sync.sh +ExecStart=%h/.local/bin/darktable_sync.sh --execute diff --git a/tests/darktable_sync.bats b/tests/darktable_sync.bats index 2ed8be8..532b192 100644 --- a/tests/darktable_sync.bats +++ b/tests/darktable_sync.bats @@ -9,31 +9,34 @@ setup() { 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" export DISPLAY=:99 } +# --- Bestehende Tests (echter Sync via --execute) --- + @test "sync_pending wird gesetzt wenn Server nicht erreichbar" { - run_with_stubs env SSH_STUB_FAIL=1 bash "$SYNC_SCRIPT" + 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" + 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 rsync fehlschlaegt" { - run_with_stubs env SSH_STUB_FAIL=0 RSYNC_STUB_FAIL=1 bash "$SYNC_SCRIPT" + run_with_stubs env SSH_STUB_FAIL=0 RSYNC_STUB_FAIL=1 bash "$SYNC_SCRIPT" --execute [ "$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" + 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" ] @@ -41,19 +44,68 @@ setup() { @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" + 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" bash "$SYNC_SCRIPT" + 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" + run_with_stubs env SSH_STUB_FAIL=0 bash "$SYNC_SCRIPT" --execute [ "$status" -eq 0 ] [ ! -d "$CONFIG_DIR/sync.lock" ] } + +# --- Neue Tests: 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 \ + RSYNC_STUB_DRY_LINES=">f+++++++++ foto.jpg" bash "$SYNC_SCRIPT" + [ "$status" -eq 0 ] + [[ "$output" == *"neu"* ]] +} diff --git a/tests/stubs/rsync b/tests/stubs/rsync index fc05f94..8cd3d1d 100755 --- a/tests/stubs/rsync +++ b/tests/stubs/rsync @@ -1,7 +1,14 @@ #!/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) 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