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>
This commit is contained in:
2026-04-21 20:49:11 +02:00
parent 6b47b8941c
commit 6074f101ff
7 changed files with 227 additions and 195 deletions
+55 -9
View File
@@ -91,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,,}"
@@ -103,15 +141,27 @@ 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
declare -A dir_foto=() dir_xmp=() dir_sonstige=()
@@ -151,11 +201,7 @@ format_rsync_details() {
fi
unset dir_foto dir_xmp dir_sonstige
done <<EOF
neu:^${prefix}[+]{9}
aktualisiert:^${prefix}[^+]
gelöscht:^\*deleting
EOF
done
}
cleanup_old_backups() {
+79 -121
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,14 +170,10 @@ 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")
BACKUP_PHOTO_DIR="${LOCAL_PHOTO_DIR}-bak"
BACKUP_DB_DIR="${LOCAL_DARKTABLE_DB_DIR}-bak"
@@ -187,95 +182,61 @@ 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)
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 [ -z "$(ls -A "$LOCAL_DARKTABLE_DB_DIR" 2>/dev/null)" ]; then
log_error "Upload abgebrochen: Quellverzeichnis leer ($LOCAL_DARKTABLE_DB_DIR). Falscher Mount?"
touch "$CONFIG_DIR/sync_pending"
exit 1
fi
if ! rsync -uavh --itemize-changes --delete \
"${RSYNC_DRY_FLAG[@]}" \
--exclude '*.lock' \
--exclude 'darktable_version' \
-e "ssh -p $SERVER_SSH_PORT" \
"$LOCAL_DARKTABLE_DB_DIR/" "$SERVER_USER@$SERVER_IP:$SERVER_DB_DIR/" \
2>&1 | tee -a "$SYNC_LOG" "$UPLOAD_LOG_DB"; then
log_error "Upload Datenbank fehlgeschlagen (Quelle: $LOCAL_DARKTABLE_DB_DIR)"
touch "$CONFIG_DIR/sync_pending"
exit 1
fi
SENT_DB=$(count_synced_files "$UPLOAD_LOG_DB" "up")
log "Datenbank-Upload abgeschlossen: $SENT_DB Datei(en) übertragen."
log_step "Upload: Fotos${DRY_SUFFIX}"
log " Quelle: $LOCAL_PHOTO_DIR/"
log " Ziel: $SERVER_USER@$SERVER_IP:$SERVER_PHOTO_DIR/"
if [ -z "$(ls -A "$LOCAL_PHOTO_DIR" 2>/dev/null)" ]; then
log_error "Upload abgebrochen: Quellverzeichnis leer ($LOCAL_PHOTO_DIR). Falscher Mount?"
touch "$CONFIG_DIR/sync_pending"
exit 1
fi
if ! rsync -uavh --itemize-changes --delete \
"${RSYNC_DRY_FLAG[@]}" \
--exclude '*.lock' \
-e "ssh -p $SERVER_SSH_PORT" \
"$LOCAL_PHOTO_DIR/" "$SERVER_USER@$SERVER_IP:$SERVER_PHOTO_DIR/" \
2>&1 | tee -a "$SYNC_LOG" "$UPLOAD_LOG_PHOTOS"; then
log_error "Upload Fotos fehlgeschlagen (Quelle: $LOCAL_PHOTO_DIR)"
touch "$CONFIG_DIR/sync_pending"
exit 1
fi
SENT_PHOTOS=$(count_synced_files "$UPLOAD_LOG_PHOTOS" "up")
log "Foto-Upload abgeschlossen: $SENT_PHOTOS Datei(en) übertragen."
else
log "Upload übersprungen (Token-Konflikt)."
fi
log_step "Download: Datenbank${DRY_SUFFIX}"
log " Quelle: $SERVER_USER@$SERVER_IP:$SERVER_DB_DIR/"
log " Ziel: $LOCAL_DARKTABLE_DB_DIR/"
log " Backup: $BACKUP_DB_DIR/"
if ! rsync -uavh --itemize-changes --delete \
--backup --backup-dir="$BACKUP_DB_DIR" \
"${RSYNC_DRY_FLAG[@]}" \
--exclude '*.lock' \
-e "ssh -p $SERVER_SSH_PORT" \
"$SERVER_USER@$SERVER_IP:$SERVER_DB_DIR/" "$LOCAL_DARKTABLE_DB_DIR/" \
2>&1 | tee -a "$SYNC_LOG" "$DOWNLOAD_LOG_DB"; then
log_error "Download Datenbank fehlgeschlagen (Ziel: $LOCAL_DARKTABLE_DB_DIR)"
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/"
log " Backup: $BACKUP_PHOTO_DIR/"
if ! rsync -uavh --itemize-changes --delete \
--backup --backup-dir="$BACKUP_PHOTO_DIR" \
"${RSYNC_DRY_FLAG[@]}" \
--exclude '*.lock' \
-e "ssh -p $SERVER_SSH_PORT" \
"$SERVER_USER@$SERVER_IP:$SERVER_PHOTO_DIR/" "$LOCAL_PHOTO_DIR/" \
2>&1 | tee -a "$SYNC_LOG" "$DOWNLOAD_LOG_PHOTOS"; then
log_error "Download Fotos fehlgeschlagen (Ziel: $LOCAL_PHOTO_DIR)"
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)
@@ -293,19 +254,17 @@ if [ "$DRY_RUN" = false ]; then
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
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)
all_log=$(cat "$UNISON_LOG_DB" "$UNISON_LOG_PHOTOS" 2>/dev/null || true)
UP_NEW=$( echo "$upload_log" | grep -cE '^<f[+]{9}' || true)
UP_UPD=$( echo "$upload_log" | grep -E '^<f' | grep -cvE '^<f[+]{9}' || true)
UP_DEL=$( echo "$upload_log" | grep -cE '^\*deleting' || true)
DN_NEW=$( echo "$download_log" | grep -cE '^>f[+]{9}' || true)
DN_UPD=$( echo "$download_log" | grep -E '^>f' | grep -cvE '^>f[+]{9}' || true)
DN_DEL=$( echo "$download_log" | grep -cE '^\*deleting' || true)
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"
@@ -313,22 +272,21 @@ if [ "$DRY_RUN" = true ]; 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