9f401e48e9
- 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>
270 lines
9.1 KiB
Bash
270 lines
9.1 KiB
Bash
#!/usr/bin/env bash
|
||
# Gemeinsame Hilfsfunktionen fuer darktable-sync Scripts.
|
||
# Dieses Script wird per `source` eingebunden, nicht direkt ausgefuehrt.
|
||
|
||
CONFIG_DIR="$HOME/.config/darktable-sync"
|
||
|
||
load_config() {
|
||
local env_file="$CONFIG_DIR/.env"
|
||
if [ ! -f "$env_file" ]; then
|
||
echo "Fehler: Konfiguration nicht gefunden: $env_file" >&2
|
||
echo "Vorlage kopieren mit: cp .env.example $env_file" >&2
|
||
exit 1
|
||
fi
|
||
|
||
# Berechtigungen pruefen: .env darf nicht world-readable sein
|
||
local perms
|
||
perms=$(stat -c '%a' "$env_file" 2>/dev/null || stat -f '%A' "$env_file" 2>/dev/null)
|
||
if [[ "${perms: -1}" != "0" ]]; then
|
||
echo "Warnung: $env_file ist world-readable. Empfehlung: chmod 600 $env_file" >&2
|
||
fi
|
||
|
||
# Zeilen mit Shell-Operatoren abweisen (Kommentare und Leerzeilen ignorieren)
|
||
if grep -vE '^\s*#|^\s*$' "$env_file" | grep -qE '[;|&`]'; then
|
||
echo "Fehler: $env_file enthaelt unerlaubte Zeichen (; | & \`). Bitte pruefen." >&2
|
||
exit 1
|
||
fi
|
||
|
||
# shellcheck source=/dev/null
|
||
. "$env_file"
|
||
}
|
||
|
||
require_var() {
|
||
local var_name="$1"
|
||
if [ -z "${!var_name:-}" ]; then
|
||
echo "Fehler: Variable '$var_name' ist nicht gesetzt in $CONFIG_DIR/.env" >&2
|
||
exit 1
|
||
fi
|
||
}
|
||
|
||
validate_path() {
|
||
local var_name="$1" value="${!1:-}"
|
||
# Pfade duerfen keine Shell-Sonderzeichen oder Path-Traversal enthalten
|
||
if echo "$value" | grep -qE "['\";|&\`\$()\\\\]" || [[ "$value" == *".."* ]]; then
|
||
echo "Fehler: '$var_name' enthaelt unerlaubte Zeichen: $value" >&2
|
||
exit 1
|
||
fi
|
||
}
|
||
|
||
validate_config() {
|
||
require_var SERVER_IP
|
||
require_var SERVER_USER
|
||
require_var SERVER_SSH_PORT
|
||
require_var SERVER_DB_DIR
|
||
require_var SERVER_PHOTO_DIR
|
||
require_var LOCAL_DARKTABLE_DB_DIR
|
||
require_var LOCAL_PHOTO_DIR
|
||
require_var SYNC_BIN
|
||
require_var DARKTABLE_BIN
|
||
|
||
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
|
||
echo "Fehler: DARKTABLE_BIN muss auf 'darktable' zeigen, nicht auf '$(basename "$DARKTABLE_BIN")'." >&2
|
||
exit 1
|
||
fi
|
||
}
|
||
|
||
check_dependency() {
|
||
local cmd="$1" pkg="${2:-$1}"
|
||
if ! command -v "$cmd" &>/dev/null; then
|
||
echo "Fehler: '$cmd' ist nicht installiert." >&2
|
||
echo "Installieren mit: sudo apt install $pkg" >&2
|
||
exit 1
|
||
fi
|
||
}
|
||
|
||
log() {
|
||
echo "$*"
|
||
}
|
||
|
||
log_step() {
|
||
echo ""
|
||
echo "=== $* ==="
|
||
}
|
||
|
||
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=">f"
|
||
|
||
local label pattern files
|
||
while IFS=: read -r label pattern; do
|
||
files=$(grep -E "$pattern" "$log_file" 2>/dev/null \
|
||
| sed 's/^[^ ]* *//' | sort) || true
|
||
[ -n "$files" ] || continue
|
||
|
||
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 <<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() {
|
||
[ "${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" "$@"
|
||
}
|
||
|
||
# Liefert den Unix-Timestamp (mtime) von library.db auf dem Server, oder "0" wenn nicht vorhanden.
|
||
server_db_mtime() {
|
||
ssh_server "stat -c '%Y' '$SERVER_DB_DIR/library.db' 2>/dev/null || echo 0"
|
||
}
|
||
|
||
save_sync_token() {
|
||
echo "$1" > "$CONFIG_DIR/sync_token"
|
||
}
|
||
|
||
read_sync_token() {
|
||
cat "$CONFIG_DIR/sync_token" 2>/dev/null || echo ""
|
||
}
|
||
|
||
server_reachable() {
|
||
ssh_server true 2>/dev/null
|
||
}
|
||
|
||
ask_user() {
|
||
local title="$1" text="$2" ans
|
||
if [ "${DARKTABLE_SYNC_MODE:-}" = "gui" ] && command -v zenity &>/dev/null; then
|
||
zenity --question --title="$title" --text="$text" 2>/dev/null
|
||
return $?
|
||
elif [ "${DARKTABLE_SYNC_MODE:-}" = "gui" ] && command -v kdialog &>/dev/null; then
|
||
kdialog --title "$title" --yesno "$text" 2>/dev/null
|
||
return $?
|
||
else
|
||
printf '%b\n' "$text"
|
||
read -r -p "[j/N] " ans || true
|
||
[[ "$ans" =~ ^[jJyY] ]]
|
||
return $?
|
||
fi
|
||
}
|
||
|
||
# Fragt den User wie mit einem Sync-Token-Konflikt umgegangen werden soll.
|
||
# Gibt "download", "upload" oder "abort" aus.
|
||
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 [ "${DARKTABLE_SYNC_MODE:-}" = "gui" ] && command -v zenity &>/dev/null; then
|
||
local choice
|
||
choice=$(zenity --list \
|
||
--title="$TITLE" \
|
||
--text="$EXPLAIN" \
|
||
--radiolist \
|
||
--column="" --column="Aktion" --column="Beschreibung" \
|
||
TRUE "Herunterladen" "Server-Stand übernehmen (empfohlen)" \
|
||
FALSE "Hochladen erzwingen" "Lokale Version auf Server schreiben – Server-Änderungen gehen verloren!" \
|
||
FALSE "Abbrechen" "Nichts tun – Sync wird übersprungen" \
|
||
--width=520 --height=260 2>/dev/null) || true
|
||
case "$choice" in
|
||
"Hochladen erzwingen") echo "upload" ;;
|
||
"Abbrechen") echo "abort" ;;
|
||
*) echo "download" ;;
|
||
esac
|
||
|
||
elif [ "${DARKTABLE_SYNC_MODE:-}" = "gui" ] && command -v kdialog &>/dev/null; then
|
||
local btn
|
||
btn=$(kdialog --title "$TITLE" \
|
||
--menu "$EXPLAIN" \
|
||
download "Herunterladen (empfohlen)" \
|
||
upload "Hochladen erzwingen (Server-Änderungen gehen verloren!)" \
|
||
abort "Abbrechen" 2>/dev/null) || true
|
||
case "$btn" in
|
||
upload|abort) echo "$btn" ;;
|
||
*) echo "download" ;;
|
||
esac
|
||
|
||
else
|
||
echo ""
|
||
echo "=== $TITLE ==="
|
||
echo "Ein anderer Rechner hat die Datenbank seit deinem letzten Sync verändert."
|
||
echo "Deine lokalen Änderungen wurden noch NICHT auf den Server übertragen."
|
||
echo ""
|
||
echo " 1) Herunterladen (empfohlen) – Server-Stand übernehmen"
|
||
echo " 2) Hochladen erzwingen – lokale Version gewinnt, Server-Änderungen gehen verloren"
|
||
echo " 3) Abbrechen"
|
||
local ans
|
||
read -r -p "Auswahl [1/2/3, Standard: 1]: " ans || true
|
||
case "$ans" in
|
||
2) echo "upload" ;;
|
||
3) echo "abort" ;;
|
||
*) echo "download" ;;
|
||
esac
|
||
fi
|
||
}
|