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/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/scripts/darktable_common.sh b/scripts/darktable_common.sh index 0c93b8c..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 @@ -88,6 +90,63 @@ 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 </dev/null || true + fi +} + +confirm_dry_run() { + [ "${DRY_RUN_SKIP_CONFIRM:-0}" = "1" ] && return 0 + ask_user "Darktable Sync – Trockenlauf" \ + "Trockenlauf starten?\n\nEs werden keine Dateien verändert oder übertragen." +} + ssh_server() { ssh -o ConnectTimeout=5 -o BatchMode=yes \ -p "$SERVER_SSH_PORT" "$SERVER_USER@$SERVER_IP" "$@" diff --git a/scripts/darktable_sync.sh b/scripts/darktable_sync.sh index 6a77b2d..4589369 100755 --- a/scripts/darktable_sync.sh +++ b/scripts/darktable_sync.sh @@ -23,6 +23,15 @@ 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 @@ -54,9 +63,15 @@ echo "$$" > "$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,51 @@ 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") + +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" + 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 \ + 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" \ @@ -177,12 +217,16 @@ 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 \ + 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/" \ @@ -197,12 +241,13 @@ 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 \ +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/" \ @@ -214,12 +259,13 @@ 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 \ +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/" \ @@ -231,30 +277,63 @@ 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." + + 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)) -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 + 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) -if [ "$TOTAL_SENT" -gt 0 ] || [ "$TOTAL_RECEIVED" -gt 0 ]; then - notify-send "Darktable Sync" \ - "↑ $TOTAL_SENT hochgeladen | ↓ $TOTAL_RECEIVED heruntergeladen" -t 10000 + 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 '^ 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 ] +} + +# --- 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 fc05f94..a3b169d 100755 --- a/tests/stubs/rsync +++ b/tests/stubs/rsync @@ -1,7 +1,18 @@ #!/bin/bash # rsync-Stub: Verhalten per Umgebungsvariable steuerbar -# RSYNC_STUB_FAIL=1 → schlaegt fehl +# 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 +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