faa65dde2f
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>
340 lines
12 KiB
Bash
Executable File
340 lines
12 KiB
Bash
Executable File
#!/usr/bin/env bash
|
||
set -euo pipefail
|
||
|
||
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||
# shellcheck source=darktable_common.sh
|
||
source "$SCRIPT_DIR/darktable_common.sh"
|
||
|
||
log_step "Darktable Sync gestartet (PID $$, Argumente: ${*:-keine})"
|
||
|
||
check_dependency rsync
|
||
check_dependency ssh openssh-client
|
||
check_dependency notify-send libnotify-bin
|
||
check_dependency darktable
|
||
log "Alle Abhängigkeiten vorhanden."
|
||
|
||
load_config
|
||
validate_config
|
||
log "Konfiguration geladen: Server=$SERVER_USER@$SERVER_IP:$SERVER_SSH_PORT"
|
||
log " DB lokal: $LOCAL_DARKTABLE_DB_DIR"
|
||
log " DB Server: $SERVER_DB_DIR"
|
||
log " Fotos lokal: $LOCAL_PHOTO_DIR"
|
||
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
|
||
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=()
|
||
|
||
log "Lock anfordern: $LOCKDIR"
|
||
if ! mkdir "$LOCKDIR" 2>/dev/null; then
|
||
EXISTING_PID=$(cat "$LOCKPID" 2>/dev/null || true)
|
||
if [ -n "$EXISTING_PID" ] && ! kill -0 "$EXISTING_PID" 2>/dev/null; then
|
||
log "Verwaisten Lock gefunden (PID $EXISTING_PID läuft nicht mehr) – wird entfernt."
|
||
rm -f "$LOCKPID"
|
||
rmdir "$LOCKDIR" 2>/dev/null || true
|
||
mkdir "$LOCKDIR"
|
||
else
|
||
log_error "Sync läuft bereits (PID ${EXISTING_PID:-unbekannt}). Lock: $LOCKDIR"
|
||
echo " rmdir $LOCKDIR" >&2
|
||
exit 1
|
||
fi
|
||
fi
|
||
echo "$$" > "$LOCKPID"
|
||
log "Lock erworben (PID $$)."
|
||
trap 'rm -f "${TMPFILES[@]}" "$LOCKPID"; rmdir "$LOCKDIR" 2>/dev/null || true; log "Lock freigegeben."' EXIT
|
||
|
||
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..."
|
||
if pgrep -x darktable > /dev/null 2>&1; then
|
||
log "Darktable läuft (PID: $(pgrep -x darktable | tr '\n' ' ')) – Sync übersprungen."
|
||
notify-send "Darktable Sync – Abbruch" \
|
||
"Darktable ist gerade geöffnet. Sync erst nach dem Beenden möglich." \
|
||
-u normal -t 8000
|
||
exit 0
|
||
fi
|
||
log "Darktable läuft nicht – Sync kann fortfahren."
|
||
|
||
if [ -f "$CONFIG_DIR/sync_pending" ]; then
|
||
log "Ausstehender Sync aus vorherigem Lauf wird jetzt nachgeholt."
|
||
notify-send "Darktable Sync" "Ausstehender Sync wird jetzt ausgeführt..." -t 3000
|
||
fi
|
||
|
||
log "Serververbindung prüfen ($SERVER_USER@$SERVER_IP Port $SERVER_SSH_PORT)..."
|
||
if ! server_reachable; then
|
||
log "Server nicht erreichbar – Sync übersprungen, sync_pending gesetzt."
|
||
touch "$CONFIG_DIR/sync_pending"
|
||
exit 0
|
||
fi
|
||
log "Server erreichbar."
|
||
|
||
if [ "$SHOW_NOTIFY_START_STOP" = true ]; then
|
||
notify-send "Darktable Sync" "Sync gestartet..." -t 3000
|
||
fi
|
||
|
||
log "Active-Marker auf Server prüfen..."
|
||
ACTIVE=$(ssh_server "cat '$SERVER_DB_DIR/darktable.active' 2>/dev/null || true")
|
||
if [ -n "$ACTIVE" ]; then
|
||
log "WARNUNG: Active-Marker vorhanden: $ACTIVE"
|
||
notify-send "Darktable Sync – Warnung" \
|
||
"Darktable läuft möglicherweise auf: $ACTIVE" -u normal -t 10000
|
||
else
|
||
log "Kein Active-Marker – kein anderer Client aktiv."
|
||
fi
|
||
|
||
log "Darktable-Versionen prüfen..."
|
||
SERVER_VERSION=$(ssh_server "cat '$SERVER_DB_DIR/darktable_version' 2>/dev/null || true")
|
||
LOCAL_VERSION=$(darktable --version 2>&1 | head -1 || true)
|
||
log " Lokal: ${LOCAL_VERSION:-unbekannt}"
|
||
log " Server: ${SERVER_VERSION:-noch nicht gespeichert}"
|
||
|
||
if [ -n "$SERVER_VERSION" ]; then
|
||
LOCAL_MM=$(echo "$LOCAL_VERSION" | grep -oP '\d+\.\d+' | head -1 || true)
|
||
SERVER_MM=$(echo "$SERVER_VERSION" | grep -oP '\d+\.\d+' | head -1 || true)
|
||
if [ -n "$SERVER_MM" ] && [ "$LOCAL_MM" != "$SERVER_MM" ]; then
|
||
log_error "Versionskonflikt: lokal=$LOCAL_MM, server=$SERVER_MM"
|
||
log_error "Bitte beide Rechner auf gleichen Stand bringen."
|
||
notify-send "Darktable Sync – Versionskonflikt" \
|
||
"Lokal: $LOCAL_MM Server: $SERVER_MM\nBitte angleichen!" \
|
||
-u critical
|
||
touch "$CONFIG_DIR/sync_pending"
|
||
exit 1
|
||
fi
|
||
log "Versionen übereinstimmend ($LOCAL_MM)."
|
||
fi
|
||
|
||
log "Sync-Token prüfen..."
|
||
SAVED_TOKEN=$(read_sync_token)
|
||
SERVER_TOKEN=$(server_db_mtime)
|
||
log " Gespeicherter Token: ${SAVED_TOKEN:-keiner (erster Sync)}"
|
||
log " Aktueller Server-Token: $SERVER_TOKEN"
|
||
|
||
UPLOAD_ALLOWED=true
|
||
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)
|
||
log "Benutzerentscheidung: $RESOLUTION"
|
||
case "$RESOLUTION" in
|
||
upload)
|
||
log "Upload erzwungen – lokale Version überschreibt Server."
|
||
;;
|
||
abort)
|
||
log "Sync abgebrochen durch Benutzer."
|
||
exit 0
|
||
;;
|
||
*)
|
||
log "Nur Download – Server-Stand wird übernommen."
|
||
UPLOAD_ALLOWED=false
|
||
;;
|
||
esac
|
||
else
|
||
log "Token stimmt überein – Upload erlaubt."
|
||
fi
|
||
|
||
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${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)"
|
||
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)"
|
||
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."
|
||
|
||
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"
|
||
|
||
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))
|
||
|
||
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)
|
||
|
||
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 '^<f[+]{9}' || echo 0)
|
||
DN_UPD=$( echo "$download_log" | grep -E '^<f' | grep -cvE '^<f[+]{9}' || echo 0)
|
||
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 (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
|
||
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_step "Sync abgeschlossen"
|
||
log " Hochgeladen: $TOTAL_SENT ($SENT_DB DB + $SENT_PHOTOS Fotos)"
|
||
log " Heruntergeladen: $TOTAL_RECEIVED ($RECEIVED_DB DB + $RECEIVED_PHOTOS Fotos)"
|
||
|
||
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
|