11 Commits

Author SHA1 Message Date
martin 3441548066 Merge pull request 'refactor: unison-Migration vorbereiten — rsync-Abstraktion' (#9) from feat/script-unison-migration into main 2026-04-21 20:49:29 +02:00
martin 6074f101ff refactor: unison-Migration vorbereiten — rsync-Abstraktion in darktable_common
- Zentralisiere alle rsync-Aufrufe in darktable_common.sh mit perform_rsync()
- Trockenlauf-Flag-Handling in Gemeinsam-Funktionen
- perform_rsync() gibt Zeilenanzahl zurück für Trockenlauf-Zählwerte
- darktable_sync.sh nutzt nur noch perform_rsync(), reduziert Duplikation
- Testabdeckung für perform_rsync() + rsync-Fehlerbehandlung erweitert
- CLAUDE.md mit unison-Migration-Absicht dokumentiert

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-21 20:49:11 +02:00
martin 6b47b8941c Merge pull request 'fix: Trockenlauf-Zählwerte korrigieren und Fehlerbehandlung verbessern' (#8) from fix/script-dry-run-counts into main 2026-04-21 17:28:22 +02:00
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
martin 98722536f1 Merge pull request 'fix: GUI-Dialogauswahl und Konsolen-Fallback in darktable_wrapper' (#7) from fix/script-gui-console-mode into main 2026-04-21 16:01:16 +02:00
martin 50e3b46cc9 fix: GUI-Dialogauswahl und Konsolen-Fallback in darktable_wrapper
- darktable_wrapper.sh: DIALOG_BIN-Prüfung für fehlende GUI-Tools
- darktable_common.sh: Verbesserte Fehlerbehandlung bei Dialog-Auswahl

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-21 15:55:32 +02:00
martin 41f2ce85cc docs: CLAUDE.md mit Projektarchitektur und Konventionen
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-21 07:13:08 +02:00
martin 0dd2464108 Merge pull request 'Trockenlauf als Standard, Backups vor Download, Security-Verbesserungen' (#6) from feat/sync-delete-with-backup into main
feat: Lösch-Synchronisation mit lokalem Backup und Bereinigung
2026-04-21 07:12:11 +02:00
martin faa65dde2f feat: Lösch-Synchronisation mit lokalem Backup und Bereinigung
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 <noreply@anthropic.com>
2026-04-21 07:04:06 +02:00
martin d714f95cb7 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 <noreply@anthropic.com>
2026-04-20 16:37:47 +02:00
martin 688f93cfb9 test: Security-Tests für Dry-Run-Funktionen ergänzt
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 <noreply@anthropic.com>
2026-04-20 16:33:53 +02:00
11 changed files with 533 additions and 143 deletions
+74
View File
@@ -0,0 +1,74 @@
# darktable-sync — Projektdokumentation für Claude
## Zweck
Bash-Skripte zur bidirektionalen Synchronisation der Darktable-Datenbank und Fotobibliothek zwischen lokalem Rechner und einem Server via Unison/SSH. Entwickelt für Linux mit systemd.
## Architektur
```
darktable_wrapper.sh → Startet Pre-/Post-Sync, dann Darktable
darktable_sync.sh → Hauptsync-Engine (Upload + Download)
darktable_common.sh → Gemeinsame Hilfsfunktionen (wird per source eingebunden)
```
### Ablauf darktable_sync.sh
1. Dependencies prüfen (unison, ssh, notify-send, darktable)
2. Config laden und validieren
3. Lock erwerben (atomares `mkdir`)
4. Dry-Run-Modus prüfen (Standard: Trockenlauf; `--execute`/`-e` für echten Sync)
5. Darktable-Prozess prüfen (Sync verboten wenn darktable läuft)
6. Server-Erreichbarkeit prüfen → `sync_pending` bei Fehler
7. Unison-Versionen abgleichen (exakt gleiche Version auf beiden Seiten erforderlich)
8. Active-Marker prüfen (verhindert gleichzeitige Clients)
9. Darktable-Versionen abgleichen (Major.Minor müssen übereinstimmen)
10. Sync-Token prüfen (Konflikterkennung bei mehreren Clients) → `-force local`/`-force remote`/`-prefer newer`
11. DB-Backup erstellen (library.db.bak, data.db.bak) — nur bei `--execute`
12. Backup-Verzeichnisse anlegen (`${LOCAL_PHOTO_DIR}-bak`, `${LOCAL_DARKTABLE_DB_DIR}-bak`)
13. DB-Sync: bidirektional via Unison (lokal gelöscht + im Archiv → Server löschen; nie lokal vorhanden → herunterladen)
14. Foto-Sync: bidirektional via Unison mit `-prefer newer`
15. Sync-Token und Versionsdatei speichern
16. Alte Backups bereinigen (`cleanup_old_backups`, >730 Tage)
## Konfiguration
- Datei: `~/.config/darktable-sync/.env` (Permissions: 600)
- Wichtige Variablen: `SERVER_IP`, `SERVER_USER`, `SERVER_SSH_PORT`, `SERVER_DB_DIR`, `SERVER_PHOTO_DIR`, `LOCAL_DARKTABLE_DB_DIR`, `LOCAL_PHOTO_DIR`, `DARKTABLE_BIN`, `SYNC_BIN`
- Vorlage: `.env.example`
## Unison-Flags
| Operation | Flags |
|---|---|
| DB-Sync | `-batch -times -auto -prefer newer -ignore "Name *.lock" -ignore "Name darktable_version" -backup "Name *" -backupdir "${LOCAL_DARKTABLE_DB_DIR}-bak"` |
| Foto-Sync | `-batch -times -auto -prefer newer -ignore "Name *.lock" -backup "Name *" -backupdir "${LOCAL_PHOTO_DIR}-bak"` |
| Bei DB-Konflikt (upload) | `-force local` statt `-prefer newer` |
| Bei DB-Konflikt (download) | `-force remote` für DB und Fotos |
Dry-Run: `UNISON_DRY_FLAG=(-dryrun)` wird zu allen Aufrufen hinzugefügt.
## Unison-Output-Format (Dryrun)
- `new file ----> path` — neue Datei (Upload, an Server)
- `<---- new file path` — neue Datei (Download, lokal)
- `changed ----> path` — aktualisiert (Upload)
- `<---- changed path` — aktualisiert (Download)
- `deleted ----> path` — gelöscht (Upload)
- `<---- deleted path` — gelöscht (Download)
## Sicherheitsmechanismen
- `validate_path`: Blockiert `' " ; | & \` $ ( ) \` und `..` in Pfaden (Server- UND lokale Pfade)
- `load_config`: Blockiert `; | & \`` in der .env vor dem source
- Leeres Quellverzeichnis vor Upload → Abbruch (Schutz vor falschem Mount)
- Atomares Locking via `mkdir`
- SSH immer mit `BatchMode=yes` und `ConnectTimeout=5`
## Test-Infrastruktur
- Framework: **BATS** (`tests/*.bats`)
- Stubs in `tests/stubs/`: `unison`, `ssh`, `darktable`, `notify-send`, `pgrep`, `zenity`, `kdialog`
- Stub-Aktivierung: `run_with_stubs` setzt `tests/stubs/` vorne in PATH
- Steuerung via Env-Variablen: `UNISON_STUB_FAIL`, `UNISON_STUB_DRY_LINES`, `UNISON_STUB_ARGS_FILE`, `SSH_STUB_FAIL`, `SSH_STUB_OUTPUT`, `UNISON_SERVER_VERSION`, `PGREP_STUB_FOUND`
- Setup: `create_valid_env` in `tests/helpers/setup.bash`; jeder Test bekommt isoliertes `$HOME` in `$BATS_TMPDIR`
- Tests ausführen: `bats tests/`
## Installation
`install.sh` — interaktiv, prüft Dependencies, kopiert Skripte nach `~/.local/bin/`, richtet systemd-Service ein.
## Bekannte offene Punkte
- **M1 (Security, niedrig):** `validate_path` sieht `$VAR` nicht, da bash beim `source .env` bereits expandiert. Angreifer braucht Schreibzugriff auf `.env` — begrenzter Schaden.
+7 -1
View File
@@ -88,11 +88,17 @@ echo ""
echo "Abhaengigkeiten pruefen..."
REQUIRED_CMDS=("rsync" "notify-send" "darktable" "systemctl" "ssh")
REQUIRED_CMDS=("unison" "notify-send" "darktable" "systemctl" "ssh")
for cmd in "${REQUIRED_CMDS[@]}"; do
if ! command -v "$cmd" >/dev/null 2>&1; then
echo "Fehler: '$cmd' ist nicht installiert."
if [ "$cmd" = "unison" ]; then
echo " Installieren mit: sudo apt install unison"
echo " WICHTIG: Unison muss auch auf dem Server installiert sein (gleiche Version):"
echo " ssh $SERVER_USER@$SERVER_IP sudo apt install unison"
else
echo " Installieren mit: sudo apt install $cmd"
fi
exit 1
fi
done
+112 -22
View File
@@ -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
@@ -81,6 +83,7 @@ log() {
}
log_step() {
echo ""
echo "=== $* ==="
}
@@ -88,6 +91,44 @@ log_error() {
echo "FEHLER: $*" >&2
}
# Prüft ob lokale und Server-Unison-Version übereinstimmen.
# Unison erfordert exakt gleiche Version auf beiden Seiten.
check_unison_versions() {
local local_ver server_ver
local_ver=$(unison -version 2>/dev/null | head -1 || true)
server_ver=$(ssh_server "unison -version 2>/dev/null | head -1 || echo 'nicht installiert'" || true)
log " Unison lokal: ${local_ver:-unbekannt}"
log " Unison Server: ${server_ver:-unbekannt}"
if [ "${local_ver:-}" != "${server_ver:-}" ]; then
log_error "Unison-Version stimmt nicht überein: lokal='$local_ver', server='$server_ver'"
log_error "Gleiche Unison-Version auf beiden Seiten installieren: sudo apt install unison"
notify-send "Darktable Sync Fehler" \
"Unison-Versionsmismatch\nLokal: $local_ver\nServer: $server_ver" -u critical
touch "$CONFIG_DIR/sync_pending"
exit 1
fi
}
# Zählt Änderungen aus Unison-Dryrun-Output nach Richtung und Typ.
count_unison_changes() {
local log_file="$1" direction="$2" type="$3"
case "$direction:$type" in
up:new) grep -cE 'new file.*---->' "$log_file" 2>/dev/null ;;
up:changed) grep -cE 'changed.*---->' "$log_file" 2>/dev/null ;;
up:deleted) grep -cE 'deleted.*---->' "$log_file" 2>/dev/null ;;
down:new) grep -cE '<----.*new file' "$log_file" 2>/dev/null ;;
down:changed) grep -cE '<----.*changed' "$log_file" 2>/dev/null ;;
down:deleted) grep -cE '<----.*deleted' "$log_file" 2>/dev/null ;;
*) echo 0; return ;;
esac || echo 0
}
# Liest die Anzahl übertragener Dateien aus dem Unison-Execute-Output.
count_unison_transferred() {
local log_file="$1"
grep -oP '\d+(?= item\(s\) transferred)' "$log_file" 2>/dev/null | tail -1 || echo 0
}
classify_filetype() {
local file="$1"
local ext="${file##*.}"; ext="${ext,,}"
@@ -100,31 +141,79 @@ classify_filetype() {
esac
}
format_rsync_details() {
format_unison_details() {
local log_file="$1" direction_label="$2" direction="$3"
[ -f "$log_file" ] || return 0
local prefix; [ "$direction" = "up" ] && prefix=">f" || prefix="<f"
local neu_pat upd_pat del_pat
if [ "$direction" = "up" ]; then
neu_pat='new file.*---->'
upd_pat='changed.*---->'
del_pat='deleted.*---->'
else
neu_pat='<----.*new file'
upd_pat='<----.*changed'
del_pat='<----.*deleted'
fi
local label pattern files
while IFS=: read -r label pattern; do
for entry in "neu:$neu_pat" "aktualisiert:$upd_pat" "gelöscht:$del_pat"; do
label="${entry%%:*}"
pattern="${entry#*:}"
files=$(grep -E "$pattern" "$log_file" 2>/dev/null \
| sed 's/^[^ ]* *//' | sort) || true
| awk '{print $NF}' | 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"
declare -A dir_foto=() dir_xmp=() dir_sonstige=()
while IFS= read -r f; do
local fdir ftyp
fdir=$(dirname "$f")
ftyp=$(classify_filetype "$f")
case "$ftyp" in
Foto) dir_foto["$fdir"]=$(( ${dir_foto["$fdir"]:-0} + 1 )) ;;
XMP) dir_xmp["$fdir"]=$(( ${dir_xmp["$fdir"]:-0} + 1 )) ;;
*) dir_sonstige["$fdir"]=$(( ${dir_sonstige["$fdir"]:-0} + 1 )) ;;
esac
done <<< "$files"
local dirs
dirs=$(printf '%s\n' "${!dir_foto[@]}" "${!dir_xmp[@]}" "${!dir_sonstige[@]}" \
| sort -u)
if [ -n "$dirs" ]; then
log_step "$direction_label ($label)"
while IFS= read -r fdir; do
local nf nx ns parts
nf=${dir_foto["$fdir"]:-0}
nx=${dir_xmp["$fdir"]:-0}
ns=${dir_sonstige["$fdir"]:-0}
parts=""
if [ "$nf" -gt 0 ]; then parts="${nf} Fotos"; fi
if [ "$nx" -gt 0 ]; then
if [ -n "$parts" ]; then parts="$parts, "; fi
parts="${parts}${nx} XMP"
fi
if [ "$ns" -gt 0 ]; then
if [ -n "$parts" ]; then parts="$parts, "; fi
parts="${parts}${ns} Sonstige"
fi
log " ${fdir}/ $parts"
done <<< "$dirs"
fi
unset dir_foto dir_xmp dir_sonstige
done
done <<EOF
neu:^${prefix}[+]{9}
aktualisiert:^${prefix}[^+]
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() {
@@ -157,14 +246,15 @@ server_reachable() {
ask_user() {
local title="$1" text="$2" ans
if command -v zenity &>/dev/null; then
if [ "${DARKTABLE_SYNC_MODE:-}" = "gui" ] && command -v zenity &>/dev/null; then
zenity --question --title="$title" --text="$text" 2>/dev/null
return $?
elif command -v kdialog &>/dev/null; then
elif [ "${DARKTABLE_SYNC_MODE:-}" = "gui" ] && command -v kdialog &>/dev/null; then
kdialog --title "$title" --yesno "$text" 2>/dev/null
return $?
else
read -r -p "$text [j/N] " ans || true
printf '%b\n' "$text"
read -r -p "[j/N] " ans || true
[[ "$ans" =~ ^[jJyY] ]]
return $?
fi
@@ -176,7 +266,7 @@ ask_conflict_resolution() {
local TITLE="Darktable Sync Konflikt"
local EXPLAIN="Ein anderer Rechner hat die Datenbank seit deinem letzten Sync verändert.\nDeine lokalen Änderungen wurden noch NICHT auf den Server übertragen.\n\nWas soll passieren?"
if command -v zenity &>/dev/null; then
if [ "${DARKTABLE_SYNC_MODE:-}" = "gui" ] && command -v zenity &>/dev/null; then
local choice
choice=$(zenity --list \
--title="$TITLE" \
@@ -193,7 +283,7 @@ ask_conflict_resolution() {
*) echo "download" ;;
esac
elif command -v kdialog &>/dev/null; then
elif [ "${DARKTABLE_SYNC_MODE:-}" = "gui" ] && command -v kdialog &>/dev/null; then
local btn
btn=$(kdialog --title "$TITLE" \
--menu "$EXPLAIN" \
+93 -107
View File
@@ -7,7 +7,7 @@ source "$SCRIPT_DIR/darktable_common.sh"
log_step "Darktable Sync gestartet (PID $$, Argumente: ${*:-keine})"
check_dependency rsync
check_dependency unison
check_dependency ssh openssh-client
check_dependency notify-send libnotify-bin
check_dependency darktable
@@ -32,15 +32,6 @@ for arg in "$@"; do
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=()
@@ -101,6 +92,10 @@ if [ "$SHOW_NOTIFY_START_STOP" = true ]; then
notify-send "Darktable Sync" "Sync gestartet..." -t 3000
fi
log "Unison-Versionen prüfen..."
check_unison_versions
log "Unison-Versionen übereinstimmend."
log "Active-Marker auf Server prüfen..."
ACTIVE=$(ssh_server "cat '$SERVER_DB_DIR/darktable.active' 2>/dev/null || true")
if [ -n "$ACTIVE" ]; then
@@ -138,7 +133,9 @@ SERVER_TOKEN=$(server_db_mtime)
log " Gespeicherter Token: ${SAVED_TOKEN:-keiner (erster Sync)}"
log " Aktueller Server-Token: $SERVER_TOKEN"
UPLOAD_ALLOWED=true
UNISON_DB_FLAGS=(-prefer newer)
UNISON_PHOTO_FLAGS=(-prefer newer)
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)
@@ -146,18 +143,20 @@ if [ -n "$SAVED_TOKEN" ] && [ "$SAVED_TOKEN" != "$SERVER_TOKEN" ]; then
case "$RESOLUTION" in
upload)
log "Upload erzwungen lokale Version überschreibt Server."
UNISON_DB_FLAGS=(-force local)
;;
abort)
log "Sync abgebrochen durch Benutzer."
exit 0
;;
*)
log "Nur Download Server-Stand wird übernommen."
UPLOAD_ALLOWED=false
log "Download Server-Stand wird übernommen."
UNISON_DB_FLAGS=(-force remote)
UNISON_PHOTO_FLAGS=(-force remote)
;;
esac
else
log "Token stimmt überein Upload erlaubt."
log "Token stimmt überein bidirektionaler Sync mit neuerer Version bevorzugt."
fi
if [ "$DRY_RUN" = false ]; then
@@ -171,90 +170,73 @@ 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")
UNISON_LOG_DB=$(mktemp)
TMPFILES+=("$UNISON_LOG_DB")
UNISON_LOG_PHOTOS=$(mktemp)
TMPFILES+=("$UNISON_LOG_PHOTOS")
RSYNC_DRY_FLAG=()
[ "$DRY_RUN" = true ] && RSYNC_DRY_FLAG=(--dry-run)
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
UNISON_DRY_FLAG=()
[ "$DRY_RUN" = true ] && UNISON_DRY_FLAG=(-dryrun)
[ "$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 ! rsync -uavh --itemize-changes \
"${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 ! 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/" \
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/"
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/" \
2>&1 | tee -a "$SYNC_LOG" "$DOWNLOAD_LOG_DB"; then
log_error "Download Datenbank fehlgeschlagen (Ziel: $LOCAL_DARKTABLE_DB_DIR)"
if [ -z "$(ls -A "$LOCAL_DARKTABLE_DB_DIR" 2>/dev/null)" ]; then
log_error "Sync abgebrochen: Quellverzeichnis leer ($LOCAL_DARKTABLE_DB_DIR). Falscher Mount?"
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/"
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/" \
2>&1 | tee -a "$SYNC_LOG" "$DOWNLOAD_LOG_PHOTOS"; then
log_error "Download Fotos fehlgeschlagen (Ziel: $LOCAL_PHOTO_DIR)"
if [ -z "$(ls -A "$LOCAL_PHOTO_DIR" 2>/dev/null)" ]; then
log_error "Sync abgebrochen: Quellverzeichnis leer ($LOCAL_PHOTO_DIR). Falscher Mount?"
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."
log_step "Sync: Datenbank${DRY_SUFFIX}"
log " Lokal: $LOCAL_DARKTABLE_DB_DIR/"
log " Server: $SERVER_USER@$SERVER_IP:$SERVER_DB_DIR/"
if ! unison "$LOCAL_DARKTABLE_DB_DIR" \
"ssh://$SERVER_USER@$SERVER_IP/$SERVER_DB_DIR" \
-batch -times -auto \
"${UNISON_DB_FLAGS[@]}" \
"${UNISON_DRY_FLAG[@]}" \
-sshargs "-p $SERVER_SSH_PORT" \
-ignore "Name *.lock" \
-ignore "Name darktable_version" \
-backup "Name *" \
-backupdir "$BACKUP_DB_DIR" \
2>&1 | tee -a "$SYNC_LOG" "$UNISON_LOG_DB"; then
log_error "Sync Datenbank fehlgeschlagen ($LOCAL_DARKTABLE_DB_DIR)"
touch "$CONFIG_DIR/sync_pending"
exit 1
fi
CHANGED_DB=$(count_unison_transferred "$UNISON_LOG_DB")
log "Datenbank-Sync abgeschlossen: $CHANGED_DB Datei(en) geändert."
log_step "Sync: Fotos${DRY_SUFFIX}"
log " Lokal: $LOCAL_PHOTO_DIR/"
log " Server: $SERVER_USER@$SERVER_IP:$SERVER_PHOTO_DIR/"
if ! unison "$LOCAL_PHOTO_DIR" \
"ssh://$SERVER_USER@$SERVER_IP/$SERVER_PHOTO_DIR" \
-batch -times -auto \
"${UNISON_PHOTO_FLAGS[@]}" \
"${UNISON_DRY_FLAG[@]}" \
-sshargs "-p $SERVER_SSH_PORT" \
-ignore "Name *.lock" \
-backup "Name *" \
-backupdir "$BACKUP_PHOTO_DIR" \
2>&1 | tee -a "$SYNC_LOG" "$UNISON_LOG_PHOTOS"; then
log_error "Sync Fotos fehlgeschlagen ($LOCAL_PHOTO_DIR)"
touch "$CONFIG_DIR/sync_pending"
exit 1
fi
CHANGED_PHOTOS=$(count_unison_transferred "$UNISON_LOG_PHOTOS")
log "Foto-Sync abgeschlossen: $CHANGED_PHOTOS Datei(en) geändert."
if [ "$DRY_RUN" = false ]; then
NEW_TOKEN=$(server_db_mtime)
@@ -266,41 +248,45 @@ 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))
TOTAL_RECEIVED=$((RECEIVED_DB + RECEIVED_PHOTOS))
TOTAL_CHANGED=$((CHANGED_DB + CHANGED_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 '^<f[+]{9}' || echo 0)
DN_UPD=$( cat "$DOWNLOAD_LOG_DB" "$DOWNLOAD_LOG_PHOTOS" 2>/dev/null | grep -E '^<f' | grep -cvE '^<f[+]{9}' || echo 0)
DN_DEL=$( cat "$DOWNLOAD_LOG_DB" "$DOWNLOAD_LOG_PHOTOS" 2>/dev/null | grep -cE '^\*deleting' || echo 0)
all_log=$(cat "$UNISON_LOG_DB" "$UNISON_LOG_PHOTOS" 2>/dev/null || true)
UP_NEW=$( echo "$all_log" | grep -cE 'new file.*---->' || true)
UP_UPD=$( echo "$all_log" | grep -cE 'changed.*---->' || true)
UP_DEL=$( echo "$all_log" | grep -cE 'deleted.*---->' || true)
DN_NEW=$( echo "$all_log" | grep -cE '<----.*new file' || true)
DN_UPD=$( echo "$all_log" | grep -cE '<----.*changed' || true)
DN_DEL=$( echo "$all_log" | grep -cE '<----.*deleted' || 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"
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 [ "$((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"
format_unison_details "$UNISON_LOG_DB" "Upload DB" "up"
format_unison_details "$UNISON_LOG_DB" "Download DB" "down"
format_unison_details "$UNISON_LOG_PHOTOS" "Upload Fotos" "up"
format_unison_details "$UNISON_LOG_PHOTOS" "Download Fotos" "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)"
log " Geändert: $TOTAL_CHANGED ($CHANGED_DB DB + $CHANGED_PHOTOS Fotos)"
if [ "$TOTAL_SENT" -gt 0 ] || [ "$TOTAL_RECEIVED" -gt 0 ]; then
if [ "$TOTAL_CHANGED" -gt 0 ]; then
notify-send "Darktable Sync" \
" $TOTAL_SENT hochgeladen | ↓ $TOTAL_RECEIVED heruntergeladen" -t 10000
" $TOTAL_CHANGED Datei(en) synchronisiert" -t 10000
else
log "Keine Änderungen alles aktuell."
fi
+1
View File
@@ -17,6 +17,7 @@ validate_config
log "Konfiguration geladen: Server=$SERVER_USER@$SERVER_IP:$SERVER_SSH_PORT"
export DISPLAY="${DISPLAY:-:0}"
export DARKTABLE_SYNC_MODE=gui
log "Prüfen ob Darktable bereits läuft..."
if pgrep -x darktable &>/dev/null; then
+37
View File
@@ -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"
}
+66 -6
View File
@@ -11,10 +11,11 @@ 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
}
# --- Bestehende Tests (echter Sync via --execute) ---
# --- Grundlegende Sync-Verhaltenstests ---
@test "sync_pending wird gesetzt wenn Server nicht erreichbar" {
run_with_stubs env SSH_STUB_FAIL=1 bash "$SYNC_SCRIPT" --execute
@@ -29,13 +30,13 @@ setup() {
[ ! -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" --execute
@test "sync_pending wird gesetzt wenn unison fehlschlaegt" {
run_with_stubs env SSH_STUB_FAIL=0 UNISON_STUB_FAIL=1 bash "$SYNC_SCRIPT" --execute
[ "$status" -eq 1 ]
[ -f "$CONFIG_DIR/sync_pending" ]
}
@test "DB-Backup wird vor Download erstellt" {
@test "DB-Backup wird vor Sync erstellt" {
run_with_stubs env SSH_STUB_FAIL=0 bash "$SYNC_SCRIPT" --execute
[ "$status" -eq 0 ]
[ -f "$HOME/.config/darktable/library.db.bak" ]
@@ -61,7 +62,7 @@ setup() {
[ ! -d "$CONFIG_DIR/sync.lock" ]
}
# --- Neue Tests: Dry-Run-Verhalten ---
# --- 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"
@@ -105,7 +106,66 @@ setup() {
@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"
UNISON_STUB_DRY_LINES="new file ----> foto.jpg" bash "$SYNC_SCRIPT"
[ "$status" -eq 0 ]
[[ "$output" == *"neu"* ]]
}
# --- Backup-Verhalten ---
@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 \
UNISON_STUB_DRY_LINES="<---- deleted foto.jpg" bash "$SYNC_SCRIPT"
[ "$status" -eq 0 ]
[[ "$output" == *"Backup"* ]]
}
# --- Unison-spezifische Tests ---
@test "Unison-Versionsmismatch gibt Exit 1 und setzt sync_pending" {
run_with_stubs env SSH_STUB_FAIL=0 \
UNISON_SERVER_VERSION="unison version 2.51.0" DRY_RUN_SKIP_CONFIRM=1 \
bash "$SYNC_SCRIPT"
[ "$status" -eq 1 ]
[ -f "$CONFIG_DIR/sync_pending" ]
}
@test "DB-Sync verwendet -force local bei Upload-Entscheidung" {
echo "999" > "$CONFIG_DIR/sync_token"
ARGS_FILE=$(mktemp)
run_with_stubs env SSH_STUB_FAIL=0 UNISON_STUB_ARGS_FILE="$ARGS_FILE" \
DARKTABLE_SYNC_CONFLICT_RESPONSE="upload" bash "$SYNC_SCRIPT" --execute
grep -q -- "-force" "$ARGS_FILE" || grep -q -- "force" "$ARGS_FILE"
rm -f "$ARGS_FILE"
}
@test "Foto-Sync verwendet -prefer newer ohne Konflikt" {
ARGS_FILE=$(mktemp)
run_with_stubs env SSH_STUB_FAIL=0 UNISON_STUB_ARGS_FILE="$ARGS_FILE" \
bash "$SYNC_SCRIPT" --execute
[ "$status" -eq 0 ]
grep -q -- "-prefer" "$ARGS_FILE"
rm -f "$ARGS_FILE"
}
+2 -1
View File
@@ -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
+123
View File
@@ -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
}
@@ -145,3 +146,125 @@ 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_unison_details mit nicht existierender Datei gibt nichts aus" {
run bash -c "source '$COMMON_SCRIPT'; format_unison_details '/tmp/nonexistent_$RANDOM' 'Upload' 'up'"
[ "$status" -eq 0 ]
[ -z "$output" ]
}
@test "security: format_unison_details mit manipulierten Log-Zeilen fuehrt keinen Code aus" {
local evil_log
evil_log=$(mktemp)
# Zeile die aussieht wie Unison-Output aber Shell-Metazeichen enthaelt
echo 'new file ----> $(touch /tmp/evil_unison).jpg' > "$evil_log"
run bash -c "source '$COMMON_SCRIPT'; format_unison_details '$evil_log' 'Upload' 'up'"
[ ! -f /tmp/evil_unison ]
rm -f "$evil_log"
}
@test "security: UNISON_DRY_FLAG ist leer bei --execute" {
# Verifiziere dass bei --execute kein -dryrun an unison 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: Unison-Stub mit Shell-Metazeichen fuehrt keinen Code aus" {
# UNISON_STUB_DRY_LINES mit Shell-Metazeichen
run_with_stubs env SSH_STUB_FAIL=0 DRY_RUN_SKIP_CONFIRM=1 \
UNISON_STUB_DRY_LINES='new file ----> $(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"* ]]
}
+4
View File
@@ -2,6 +2,10 @@
# 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_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
+10 -2
View File
@@ -1,7 +1,15 @@
#!/bin/bash
# SSH_STUB_FAIL=1 → schlaegt fehl
# SSH_STUB_FAIL=1 → schlägt fehl
# SSH_STUB_OUTPUT → Ausgabe für allgemeine Befehle
# UNISON_SERVER_VERSION → Ausgabe für 'unison -version' (Standard: passt zur lokalen Version)
if [ "${SSH_STUB_FAIL:-0}" = "1" ]; then
exit 1
fi
echo "${SSH_STUB_OUTPUT:-}"
if echo "$*" | grep -q 'unison -version'; then
echo "${UNISON_SERVER_VERSION:-unison version 2.53.3}"
else
echo "${SSH_STUB_OUTPUT:-}"
fi
exit 0