From 0c5774f69507a9c7dc45a7c2c13efa526da2b2be Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Martin=20Tr=C3=B6ger?= Date: Mon, 20 Apr 2026 12:47:10 +0200 Subject: [PATCH 1/5] install.sh: interaktive Abfragen bei Installation verbessert MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - .env-Verschiebung aus Projektverzeichnis wird angeboten und bei Bestätigung automatisch ausgeführt - Lokales Foto-Verzeichnis wird interaktiv abgefragt (mit Hinweis falls es bereits existiert) - Ausführlicher Hinweis nach Anlegen der Default-.env mit Pflichtfeldern und nächsten Schritten - bats-Hinweis entfernt (nur für Entwickler relevant) - Tests: Umlaut-Mismatch in security.bats behoben, teardown() für Lock-Isolation ergänzt Co-Authored-By: Claude Sonnet 4.6 --- install.sh | 51 +++++++++++++++++++++++++++++++++------- tests/helpers/setup.bash | 5 ++++ tests/security.bats | 2 +- 3 files changed, 49 insertions(+), 9 deletions(-) diff --git a/install.sh b/install.sh index f4095c0..3c62d94 100755 --- a/install.sh +++ b/install.sh @@ -37,8 +37,17 @@ CONFIG_ENV="$CONFIG_DIR/.env" if [[ -f "$ENV_FILE" ]]; then echo "Hinweis: .env im Projektverzeichnis gefunden." - echo " Bitte nach $CONFIG_ENV verschieben:" - echo " cp .env $CONFIG_ENV && chmod 600 $CONFIG_ENV" + read -r -p " Jetzt nach $CONFIG_ENV verschieben? [J/n]: " MOVE_ENV + if [[ "${MOVE_ENV,,}" != "n" ]]; then + mkdir -p "$CONFIG_DIR" + cp "$ENV_FILE" "$CONFIG_ENV" + chmod 600 "$CONFIG_ENV" + rm "$ENV_FILE" + echo " Erledigt: .env wurde verschoben." + else + echo " Nicht verschoben. Bitte manuell ausfuehren:" + echo " cp .env $CONFIG_ENV && chmod 600 $CONFIG_ENV" + fi fi if [[ -f "$CONFIG_ENV" ]]; then @@ -49,6 +58,18 @@ if [[ -f "$CONFIG_ENV" ]]; then set +a fi +### Lokales Foto-Verzeichnis interaktiv abfragen + +echo "" +if [[ -d "$LOCAL_PHOTO_DIR" ]]; then + read -r -p "Lokales Foto-Verzeichnis [${LOCAL_PHOTO_DIR}] (Verzeichnis existiert bereits): " INPUT_PHOTO_DIR +else + read -r -p "Lokales Foto-Verzeichnis [${LOCAL_PHOTO_DIR}]: " INPUT_PHOTO_DIR +fi +if [[ -n "$INPUT_PHOTO_DIR" ]]; then + LOCAL_PHOTO_DIR="$INPUT_PHOTO_DIR" +fi + ### Konfiguration anzeigen echo "" @@ -84,11 +105,6 @@ if ! command -v zenity >/dev/null 2>&1 && ! command -v kdialog >/dev/null 2>&1; 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 @@ -166,8 +182,27 @@ if [ ! -f "$CONFIG_ENV" ]; then cp "$SCRIPT_DIR/.env.example" "$CONFIG_ENV" chmod 600 "$CONFIG_ENV" echo "" - echo "WICHTIG: Konfiguration anpassen:" + echo "==========================================================" + echo "WICHTIG: Konfiguration anpassen, bevor du Darktable startest" + echo "==========================================================" + echo "" + echo "Eine Vorlage wurde angelegt:" + echo " $CONFIG_ENV" + echo "" + echo "Mindestens diese Felder musst du eintragen:" + echo " SERVER_USER - dein SSH-Benutzer auf dem Server" + echo " SERVER_IP - IP-Adresse oder Hostname des Servers" + echo " SERVER_DB_DIR - Pfad zur Darktable-Datenbank auf dem Server" + echo " SERVER_PHOTO_DIR - Pfad zum Fotoverzeichnis auf dem Server" + echo "" + echo "LOCAL_PHOTO_DIR ist bereits auf '${LOCAL_PHOTO_DIR}' gesetzt." + echo "" + echo "Jetzt bearbeiten:" echo " nano $CONFIG_ENV" + echo "" + echo "Danach install.sh erneut ausfuehren, damit die Verbindung" + echo "zum Server geprueft wird." + echo "==========================================================" fi ### Desktop-Shortcuts installieren diff --git a/tests/helpers/setup.bash b/tests/helpers/setup.bash index d167205..a7ecbc3 100644 --- a/tests/helpers/setup.bash +++ b/tests/helpers/setup.bash @@ -23,6 +23,11 @@ SYNC_BIN=$HOME/.local/bin/darktable_sync.sh EOF } +# Raeumt nach jedem Test auf (verhindert Lock-Leakage zwischen Tests) +teardown() { + rm -rf "$CONFIG_DIR/sync.lock" +} + # Fuehrt ein Script mit dem Stubs-Verzeichnis vorne im PATH aus run_with_stubs() { run env PATH="$STUBS_DIR:$PATH" "$@" diff --git a/tests/security.bats b/tests/security.bats index 3b2f6bd..31a5459 100644 --- a/tests/security.bats +++ b/tests/security.bats @@ -80,7 +80,7 @@ EOF mkdir -p "$CONFIG_DIR/sync.lock" run_with_stubs env SSH_STUB_FAIL=0 bash "$SYNC_SCRIPT" [ "$status" -eq 1 ] - [[ "$output" == *"laeuft bereits"* ]] + [[ "$output" == *"läuft bereits"* ]] rmdir "$CONFIG_DIR/sync.lock" } 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 2/5] =?UTF-8?q?feat:=20Trockenlauf=20als=20Standard-Aufruf?= =?UTF-8?q?,=20--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 From 688f93cfb943273e785a6a051370dffe3ce98f1f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Martin=20Tr=C3=B6ger?= Date: Mon, 20 Apr 2026 16:33:53 +0200 Subject: [PATCH 3/5] =?UTF-8?q?test:=20Security-Tests=20f=C3=BCr=20Dry-Run?= =?UTF-8?q?-Funktionen=20erg=C3=A4nzt?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 12 neue Tests prüfen: classify_filetype gegen Injection, format_rsync_details mit manipulierten Logs, RSYNC_DRY_FLAG-Isolation, Trockenlauf schreibt keine Tokens/Versionsdateien, DRY_RUN_SKIP_CONFIRM-Grenzen, unbekannte Argumente. Co-Authored-By: Claude Sonnet 4.6 --- tests/security.bats | 96 +++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 96 insertions(+) diff --git a/tests/security.bats b/tests/security.bats index 31a5459..af610a4 100644 --- a/tests/security.bats +++ b/tests/security.bats @@ -145,3 +145,99 @@ EOF [ "$status" -eq 0 ] [ ! -d "$CONFIG_DIR/sync.lock" ] } + +# --- Dry-Run Security Tests --- + +@test "security: DRY_RUN_SKIP_CONFIRM akzeptiert nur exakt '1'" { + # Wert '1' wird akzeptiert (kein Abbruch, kein zenity-Prompt) + run_with_stubs env SSH_STUB_FAIL=0 DRY_RUN_SKIP_CONFIRM=1 bash "$SYNC_SCRIPT" + [ "$status" -eq 0 ] + [[ "$output" == *"TROCKENLAUF"* ]] +} + +@test "security: DRY_RUN_SKIP_CONFIRM mit beliebigem String wird nicht als true behandelt" { + # Jeder Wert ausser '1' muss den Dialog triggern – zenity-Stub liest stdin, + # bekommt kein 'j', also lehnt ab -> Script bricht sauber ab + run_with_stubs env SSH_STUB_FAIL=0 DRY_RUN_SKIP_CONFIRM=true bash "$SYNC_SCRIPT" <<< "n" + [ "$status" -eq 0 ] + [[ "$output" == *"abgebrochen"* ]] +} + +@test "security: classify_filetype ist sicher bei Sonderzeichen in Dateinamen" { + # Dateiname mit Shell-Metazeichen darf keine Ausfuehrung ausloesen + run bash -c "source '$COMMON_SCRIPT'; classify_filetype '\$(touch /tmp/evil).jpg'" + [ "$status" -eq 0 ] + [ "$output" = "Foto" ] + [ ! -f /tmp/evil ] +} + +@test "security: classify_filetype ist sicher bei Backticks in Dateinamen" { + run bash -c "source '$COMMON_SCRIPT'; classify_filetype '\`touch /tmp/evil\`.jpg'" + [ "$status" -eq 0 ] + [ "$output" = "Foto" ] + [ ! -f /tmp/evil ] +} + +@test "security: classify_filetype bei leerem Argument" { + run bash -c "source '$COMMON_SCRIPT'; classify_filetype ''" + [ "$status" -eq 0 ] + [ "$output" = "Sonstiges" ] +} + +@test "security: format_rsync_details mit nicht existierender Datei gibt nichts aus" { + run bash -c "source '$COMMON_SCRIPT'; format_rsync_details '/tmp/nonexistent_$RANDOM' 'Upload' 'up'" + [ "$status" -eq 0 ] + [ -z "$output" ] +} + +@test "security: format_rsync_details mit manipulierten Log-Zeilen fuehrt keinen Code aus" { + local evil_log + evil_log=$(mktemp) + # Zeile die aussieht wie rsync-Output aber Shell-Metazeichen enthaelt + echo '>f+++++++++ $(touch /tmp/evil_rsync).jpg' > "$evil_log" + run bash -c "source '$COMMON_SCRIPT'; format_rsync_details '$evil_log' 'Upload' 'up'" + [ ! -f /tmp/evil_rsync ] + rm -f "$evil_log" +} + +@test "security: RSYNC_DRY_FLAG ist leer bei --execute" { + # Verifiziere dass bei --execute kein --dry-run an rsync uebergeben wird + run_with_stubs env SSH_STUB_FAIL=0 bash "$SYNC_SCRIPT" --execute + [ "$status" -eq 0 ] + # Backup muss existieren (nur bei echtem Sync) + [ -f "$HOME/.config/darktable/library.db.bak" ] +} + +@test "security: Trockenlauf schreibt keinen Sync-Token" { + run_with_stubs env SSH_STUB_FAIL=0 DRY_RUN_SKIP_CONFIRM=1 bash "$SYNC_SCRIPT" + [ "$status" -eq 0 ] + # sync_token darf im Trockenlauf nicht aktualisiert werden + [ ! -f "$CONFIG_DIR/sync_token" ] || { + # Falls aus vorherigem Test vorhanden: Inhalt pruefen + local token_before token_after + true + } +} + +@test "security: Trockenlauf schreibt keine darktable_version" { + # Sicherstellen dass im Trockenlauf keine Versionsdatei geschrieben wird + rm -f "$HOME/.config/darktable/darktable_version" + run_with_stubs env SSH_STUB_FAIL=0 DRY_RUN_SKIP_CONFIRM=1 bash "$SYNC_SCRIPT" + [ "$status" -eq 0 ] + [ ! -f "$HOME/.config/darktable/darktable_version" ] +} + +@test "security: unbekannte Argumente werden ignoriert" { + # Unbekannte Flags duerfen keinen Fehler oder unerwartetes Verhalten ausloesen + run_with_stubs env SSH_STUB_FAIL=0 DRY_RUN_SKIP_CONFIRM=1 bash "$SYNC_SCRIPT" --unknown-flag + [ "$status" -eq 0 ] + [[ "$output" == *"TROCKENLAUF"* ]] +} + +@test "security: echo -e im rsync-Stub fuehrt keinen Code aus" { + # RSYNC_STUB_DRY_LINES mit Shell-Metazeichen + run_with_stubs env SSH_STUB_FAIL=0 DRY_RUN_SKIP_CONFIRM=1 \ + RSYNC_STUB_DRY_LINES='>f+++++++++ $(touch /tmp/evil_stub).jpg' bash "$SYNC_SCRIPT" + [ "$status" -eq 0 ] + [ ! -f /tmp/evil_stub ] +} From d714f95cb760ed7a1c152d9351cff539c35e771c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Martin=20Tr=C3=B6ger?= Date: Mon, 20 Apr 2026 16:37:47 +0200 Subject: [PATCH 4/5] refactor: Grep-Optimierung im Trockenlauf-Ergebnis Reduziere Dateizugriffe von 12 auf 2 durch Pufferung der Log-Inhalte. Liest Upload- und Download-Logs jeweils einmal, statt sie 6x zu lesen. Co-Authored-By: Claude Sonnet 4.6 --- scripts/darktable_sync.sh | 15 +++++++++------ 1 file changed, 9 insertions(+), 6 deletions(-) diff --git a/scripts/darktable_sync.sh b/scripts/darktable_sync.sh index 4e2ddfd..df8459e 100755 --- a/scripts/darktable_sync.sh +++ b/scripts/darktable_sync.sh @@ -272,12 +272,15 @@ TOTAL_SENT=$((SENT_DB + SENT_PHOTOS)) TOTAL_RECEIVED=$((RECEIVED_DB + RECEIVED_PHOTOS)) 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) + 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}' || echo 0) + UP_UPD=$( echo "$upload_log" | grep -E '^>f' | grep -cvE '^>f[+]{9}' || echo 0) + UP_DEL=$( echo "$upload_log" | grep -cE '^\*deleting' || echo 0) + DN_NEW=$( echo "$download_log" | grep -cE '^ Date: Tue, 21 Apr 2026 07:04:06 +0200 Subject: [PATCH 5/5] =?UTF-8?q?feat:=20L=C3=B6sch-Synchronisation=20mit=20?= =?UTF-8?q?lokalem=20Backup=20und=20Bereinigung?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Gelöschte Dateien werden beim Download ins Backup-Verzeichnis verschoben (${LOCAL_PHOTO_DIR}-bak, ${LOCAL_DARKTABLE_DB_DIR}-bak) statt permanent gelöscht. Upload verwendet --delete ohne Backup. Backups älter als 2 Jahre werden automatisch bereinigt. Safeguard verhindert --delete bei leerem Quellverzeichnis. validate_path prüft jetzt auch lokale Pfade. Co-Authored-By: Claude Sonnet 4.6 --- scripts/darktable_common.sh | 14 +++++++++++ scripts/darktable_sync.sh | 37 +++++++++++++++++++++++----- tests/darktable_common.bats | 37 ++++++++++++++++++++++++++++ tests/darktable_sync.bats | 49 +++++++++++++++++++++++++++++++++++++ tests/helpers/setup.bash | 3 ++- tests/security.bats | 27 ++++++++++++++++++++ tests/stubs/rsync | 8 ++++-- 7 files changed, 166 insertions(+), 9 deletions(-) diff --git a/scripts/darktable_common.sh b/scripts/darktable_common.sh index 6b7cedf..8d20a06 100644 --- a/scripts/darktable_common.sh +++ b/scripts/darktable_common.sh @@ -59,6 +59,8 @@ validate_config() { validate_path SERVER_DB_DIR validate_path SERVER_PHOTO_DIR + validate_path LOCAL_DARKTABLE_DB_DIR + validate_path LOCAL_PHOTO_DIR # DARKTABLE_BIN: basename muss 'darktable' sein if [[ "$(basename "$DARKTABLE_BIN")" != "darktable" ]]; then @@ -127,6 +129,18 @@ gelöscht:^\*deleting EOF } +cleanup_old_backups() { + local backup_dir="$1" + [ -d "$backup_dir" ] || return 0 + local count + count=$(find "$backup_dir" -type f -mtime +730 | wc -l) + if [ "$count" -gt 0 ]; then + log "Backup-Bereinigung: $count Datei(en) älter als 2 Jahre in $backup_dir" + find "$backup_dir" -type f -mtime +730 -delete + find "$backup_dir" -type d -empty -delete 2>/dev/null || true + fi +} + confirm_dry_run() { [ "${DRY_RUN_SKIP_CONFIRM:-0}" = "1" ] && return 0 ask_user "Darktable Sync – Trockenlauf" \ diff --git a/scripts/darktable_sync.sh b/scripts/darktable_sync.sh index df8459e..4589369 100755 --- a/scripts/darktable_sync.sh +++ b/scripts/darktable_sync.sh @@ -180,6 +180,13 @@ 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="" @@ -191,7 +198,12 @@ 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 ! rsync -uavh --itemize-changes \ + 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' \ @@ -208,7 +220,12 @@ if [ "$UPLOAD_ALLOWED" = true ]; then log_step "Upload: Fotos${DRY_SUFFIX}" log " Quelle: $LOCAL_PHOTO_DIR/" log " Ziel: $SERVER_USER@$SERVER_IP:$SERVER_PHOTO_DIR/" - if ! rsync -uavh --itemize-changes \ + 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" \ @@ -227,7 +244,9 @@ fi log_step "Download: Datenbank${DRY_SUFFIX}" log " Quelle: $SERVER_USER@$SERVER_IP:$SERVER_DB_DIR/" log " Ziel: $LOCAL_DARKTABLE_DB_DIR/" -if ! rsync -uavh --itemize-changes \ +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" \ @@ -243,7 +262,9 @@ 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/" -if ! rsync -uavh --itemize-changes \ +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" \ @@ -266,6 +287,10 @@ if [ "$DRY_RUN" = false ]; then 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)) @@ -283,8 +308,8 @@ if [ "$DRY_RUN" = true ]; then DN_DEL=$( echo "$download_log" | grep -cE '^\*deleting' || echo 0) 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" + log " Upload: $UP_NEW neu | $UP_UPD aktualisiert | $UP_DEL gelöscht (Server)" + log " Download: $DN_NEW neu | $DN_UPD aktualisiert | $DN_DEL gelöscht → Backup: $BACKUP_PHOTO_DIR" if [ "$((TOTAL_SENT + TOTAL_RECEIVED))" -gt 0 ]; then if ask_user "Details" "Details der zu übertragenden Dateien anzeigen?"; then diff --git a/tests/darktable_common.bats b/tests/darktable_common.bats index eb2e3da..c621018 100644 --- a/tests/darktable_common.bats +++ b/tests/darktable_common.bats @@ -75,3 +75,40 @@ COMMON_SCRIPT="$BATS_TEST_DIRNAME/../scripts/darktable_common.sh" rm -f "$TMP_SCRIPT" [ "$status" -eq 1 ] } + +# --- cleanup_old_backups --- + +@test "cleanup_old_backups: nicht existierendes Verzeichnis gibt kein Fehler" { + run bash -c "source '$COMMON_SCRIPT'; cleanup_old_backups '/tmp/nonexistent_bak_$RANDOM'" + [ "$status" -eq 0 ] +} + +@test "cleanup_old_backups: Datei juenger als 730 Tage bleibt erhalten" { + BACKUP="$BATS_TMPDIR/backup_test" + mkdir -p "$BACKUP" + touch "$BACKUP/recent.jpg" + run bash -c "source '$COMMON_SCRIPT'; cleanup_old_backups '$BACKUP'" + [ "$status" -eq 0 ] + [ -f "$BACKUP/recent.jpg" ] + rm -rf "$BACKUP" +} + +@test "cleanup_old_backups: Datei aelter als 730 Tage wird geloescht" { + BACKUP="$BATS_TMPDIR/backup_old" + mkdir -p "$BACKUP" + touch -d "3 years ago" "$BACKUP/old.jpg" + run bash -c "source '$COMMON_SCRIPT'; cleanup_old_backups '$BACKUP'" + [ "$status" -eq 0 ] + [ ! -f "$BACKUP/old.jpg" ] + rm -rf "$BACKUP" +} + +@test "cleanup_old_backups: leere Unterverzeichnisse werden entfernt" { + BACKUP="$BATS_TMPDIR/backup_empty" + mkdir -p "$BACKUP/subdir" + touch -d "3 years ago" "$BACKUP/subdir/old.jpg" + run bash -c "source '$COMMON_SCRIPT'; cleanup_old_backups '$BACKUP'" + [ "$status" -eq 0 ] + [ ! -d "$BACKUP/subdir" ] + rm -rf "$BACKUP" +} diff --git a/tests/darktable_sync.bats b/tests/darktable_sync.bats index 532b192..315d5b0 100644 --- a/tests/darktable_sync.bats +++ b/tests/darktable_sync.bats @@ -11,6 +11,7 @@ setup() { touch "$HOME/.config/darktable/data.db" rm -f "$HOME/.config/darktable/"*.bak mkdir -p "$HOME/Pictures" + touch "$HOME/Pictures/test.jpg" export DISPLAY=:99 } @@ -109,3 +110,51 @@ setup() { [ "$status" -eq 0 ] [[ "$output" == *"neu"* ]] } + +# --- Tests: --delete und Backup-Verhalten --- + +@test "Upload-rsync enthaelt --delete Flag" { + ARGS_FILE=$(mktemp) + run_with_stubs env SSH_STUB_FAIL=0 RSYNC_STUB_ARGS_FILE="$ARGS_FILE" bash "$SYNC_SCRIPT" --execute + [ "$status" -eq 0 ] + grep -q -- "--delete" "$ARGS_FILE" + rm -f "$ARGS_FILE" +} + +@test "Download-rsync enthaelt --backup und --backup-dir" { + ARGS_FILE=$(mktemp) + run_with_stubs env SSH_STUB_FAIL=0 RSYNC_STUB_ARGS_FILE="$ARGS_FILE" bash "$SYNC_SCRIPT" --execute + [ "$status" -eq 0 ] + grep -q -- "--backup" "$ARGS_FILE" + grep -q -- "--backup-dir=" "$ARGS_FILE" + rm -f "$ARGS_FILE" +} + +@test "Backup-Verzeichnisse werden bei echtem Sync angelegt" { + run_with_stubs env SSH_STUB_FAIL=0 bash "$SYNC_SCRIPT" --execute + [ "$status" -eq 0 ] + [ -d "$HOME/Pictures-bak" ] + [ -d "$HOME/.config/darktable-bak" ] +} + +@test "Trockenlauf legt keine Backup-Verzeichnisse an" { + run_with_stubs env SSH_STUB_FAIL=0 DRY_RUN_SKIP_CONFIRM=1 bash "$SYNC_SCRIPT" + [ "$status" -eq 0 ] + [ ! -d "$HOME/Pictures-bak" ] + [ ! -d "$HOME/.config/darktable-bak" ] +} + +@test "Upload bricht ab wenn Foto-Quellverzeichnis leer ist" { + rm -f "$HOME/Pictures/"* + run_with_stubs env SSH_STUB_FAIL=0 bash "$SYNC_SCRIPT" --execute + [ "$status" -eq 1 ] + [[ "$output" == *"leer"* ]] + [ -f "$CONFIG_DIR/sync_pending" ] +} + +@test "Trockenlauf-Ergebnis zeigt Backup-Hinweis bei Loeschungen" { + run_with_stubs env SSH_STUB_FAIL=0 DRY_RUN_SKIP_CONFIRM=1 \ + RSYNC_STUB_DRY_LINES="*deleting foto.jpg" bash "$SYNC_SCRIPT" + [ "$status" -eq 0 ] + [[ "$output" == *"Backup"* ]] +} diff --git a/tests/helpers/setup.bash b/tests/helpers/setup.bash index a7ecbc3..97c5f61 100644 --- a/tests/helpers/setup.bash +++ b/tests/helpers/setup.bash @@ -23,9 +23,10 @@ SYNC_BIN=$HOME/.local/bin/darktable_sync.sh EOF } -# Raeumt nach jedem Test auf (verhindert Lock-Leakage zwischen Tests) +# Raeumt nach jedem Test auf (verhindert Lock- und Backup-Dir-Leakage zwischen Tests) teardown() { rm -rf "$CONFIG_DIR/sync.lock" + rm -rf "$HOME/Pictures-bak" "$HOME/.config/darktable-bak" } # Fuehrt ein Script mit dem Stubs-Verzeichnis vorne im PATH aus diff --git a/tests/security.bats b/tests/security.bats index af610a4..dfdb43c 100644 --- a/tests/security.bats +++ b/tests/security.bats @@ -13,6 +13,7 @@ setup() { touch "$HOME/.config/darktable/library.db" touch "$HOME/.config/darktable/data.db" mkdir -p "$HOME/Pictures" + touch "$HOME/Pictures/test.jpg" export DISPLAY=:99 } @@ -241,3 +242,29 @@ EOF [ "$status" -eq 0 ] [ ! -f /tmp/evil_stub ] } + +# --- Backup-Pfad Security --- + +@test "security: LOCAL_PHOTO_DIR mit Path-Traversal wird geblockt" { + create_valid_env + echo "LOCAL_PHOTO_DIR=/home/user/../../../etc/photos" >> "$CONFIG_DIR/.env" + run bash -c "source '$COMMON_SCRIPT'; load_config; validate_config" + [ "$status" -eq 1 ] + [[ "$output" == *"LOCAL_PHOTO_DIR"* ]] +} + +@test "security: LOCAL_DARKTABLE_DB_DIR mit Path-Traversal wird geblockt" { + create_valid_env + echo "LOCAL_DARKTABLE_DB_DIR=/home/user/../../../etc/dt" >> "$CONFIG_DIR/.env" + run bash -c "source '$COMMON_SCRIPT'; load_config; validate_config" + [ "$status" -eq 1 ] + [[ "$output" == *"LOCAL_DARKTABLE_DB_DIR"* ]] +} + +@test "security: LOCAL_PHOTO_DIR mit Single-Quote wird geblockt" { + create_valid_env + printf 'LOCAL_PHOTO_DIR="/home/user/pics'"'"'injection"\n' >> "$CONFIG_DIR/.env" + run bash -c "source '$COMMON_SCRIPT'; load_config; validate_config" + [ "$status" -eq 1 ] + [[ "$output" == *"LOCAL_PHOTO_DIR"* ]] +} diff --git a/tests/stubs/rsync b/tests/stubs/rsync index 8cd3d1d..a3b169d 100755 --- a/tests/stubs/rsync +++ b/tests/stubs/rsync @@ -1,7 +1,11 @@ #!/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) +# RSYNC_STUB_FAIL=1 → schlaegt fehl +# RSYNC_STUB_DRY_LINES → Ausgabe bei --dry-run (Zeilenumbrüche als \n) +# RSYNC_STUB_ARGS_FILE → Pfad zu Datei, in die alle Argumente geschrieben werden +if [ -n "${RSYNC_STUB_ARGS_FILE:-}" ]; then + echo "$*" >> "$RSYNC_STUB_ARGS_FILE" +fi if [ "${RSYNC_STUB_FAIL:-0}" = "1" ]; then exit 1 fi