From a2c347cd092893b0c786b89902510d865c34d2ba Mon Sep 17 00:00:00 2001 From: Thorsten Date: Wed, 25 Mar 2026 18:56:07 +0100 Subject: [PATCH] . --- unRAID Userscript - Music sync/README.MD | 206 +++++++++++++++++++ unRAID Userscript - Music sync/music_sync.sh | 200 ++++++++++++++++++ 2 files changed, 406 insertions(+) create mode 100644 unRAID Userscript - Music sync/README.MD create mode 100644 unRAID Userscript - Music sync/music_sync.sh diff --git a/unRAID Userscript - Music sync/README.MD b/unRAID Userscript - Music sync/README.MD new file mode 100644 index 0000000..287ad56 --- /dev/null +++ b/unRAID Userscript - Music sync/README.MD @@ -0,0 +1,206 @@ +# Music Sync Script (Unraid) + +## Zweck +Synchronisiert neue Musikdateien aus `incoming` in zwei Zielordner. +Dateien werden nur verarbeitet, wenn sie vollständig geschrieben sind. + +--- + +## Ablauf +incoming → ready → DEST1 + DEST2 → delete + +--- + +## Warum incoming → ready? + +Das Script verwendet zwei Verzeichnisse, um einen sauberen und sicheren Verarbeitungszustand zu gewährleisten. + +### Problem ohne Trennung + +Wenn direkt aus `incoming` synchronisiert wird: + +- Dateien können noch geschrieben werden (z. B. durch Picard) +- `rsync` kopiert ggf. unvollständige Dateien +- bei mehreren Durchläufen entstehen Inkonsistenzen zwischen den Zielen + +--- + +### Lösung: Staging mit `ready` + +Der Ablauf ist bewusst zweistufig: + +incoming (unsicher) → ready (stabil) → Sync + +#### incoming +- hier schreibt Picard +- Dateien können noch im Zugriff sein +- kein Sync von hier + +#### ready +- Dateien werden erst verschoben, wenn sie alt genug sind +- `mv` innerhalb desselben Filesystems ist atomar +- Datei ist danach garantiert vollständig + +👉 Ergebnis: +Das Script arbeitet nur mit fertigen, stabilen Dateien. + +--- + +## Wichtiger Hinweis zu "atomar" + +Ein `mv` ist nur dann atomar, wenn sich Quelle und Ziel im selben Filesystem befinden. + +In diesem Setup: + +``` +INCOMING="/mnt/user/Cache/Picard/incoming" +READY="/mnt/user/Cache/Picard/ready" +``` + +liegen beide Pfade im selben Share → gleicher Storage → atomarer Move. + +Wichtig: +- Es ist egal, wo Picard läuft (Docker, anderer Host, etc.) +- Entscheidend ist nur, wohin geschrieben wird + +Nicht atomar wäre z. B.: + +``` +/mnt/user/... → /mnt/diskX/... +``` + +Dann würde `mv` intern kopieren + löschen. + +--- + +## Lockfile + +``` +/mnt/user/Cache/Picard/sync.lock +``` + +### Zweck +- verhindert parallele Ausführung +- schützt vor doppelten Operationen + +### Problemfall +Wenn das Script abbricht, bleibt das Lockfile bestehen und blockiert weitere Runs. + +### Lösung + +``` +rm /mnt/user/Cache/Picard/sync.lock +``` + +Nur ausführen, wenn sicher kein Script mehr läuft. + +--- + +## CHOWN (Dateirechte / CIFS Problem) + +Nach dem Kopieren werden Dateien und Ordner explizit gesetzt auf: + +``` +CHOWN_USER="thorsten" +CHOWN_GROUP="users" +``` + +### Warum ist das nötig? + +Unraid + CIFS/Samba verhält sich so: + +- Dateien werden oft als `root` oder `nobody` angelegt +- Netzwerkzugriff über CIFS hat dann **keine Löschrechte** +- selbst wenn Lesen funktioniert + +👉 Typisches Symptom: +Datei kann geöffnet, aber nicht gelöscht werden + +--- + +### Lösung im Script + +Nach erfolgreichem Sync: + +- wird jede Datei gechowned +- zugehörige Ordner werden ebenfalls angepasst +- Ordner aber nur **einmal pro Lauf** (Performance) + +👉 Ergebnis: +- Dateien gehören deinem User +- CIFS Zugriff funktioniert korrekt +- kein manuelles `chown -R` mehr nötig + +--- + +## Eigenschaften + +- verarbeitet nur Dateien älter als 1 Minute +- atomarer Move (incoming → ready) +- Sync zu zwei Zielen per rsync +- Löschen nur bei Erfolg beider Syncs +- fehlgeschlagene Dateien bleiben erhalten +- Lockfile verhindert parallele Ausführung +- Dry-Run Modus vorhanden +- Logging + Konsolenausgabe +- Pfade tolerant gegenüber trailing `/` +- Zielpfade müssen existieren (sonst Abbruch) +- Ownership wird gesetzt (`chown`) +- Ordner werden nur einmal pro Lauf angepasst + +--- + +## Konfiguration + +``` +INCOMING="/mnt/user/Cache/Picard/incoming" +READY="/mnt/user/Cache/Picard/ready" + +DEST1="/mnt/user/Cache/Syncthing/..." +DEST2="/mnt/user/Media/Musik/..." + +LOG="/mnt/user/Cache/Picard/sync.log" +LOCKFILE="/mnt/user/Cache/Picard/sync.lock" + +DRY_RUN=false + +CHOWN_USER="thorsten" +CHOWN_GROUP="users" +``` + +--- + +## Dry Run + +``` +DRY_RUN=true +``` + +- keine Änderungen am Dateisystem +- rsync läuft mit --dry-run +- Aktionen werden geloggt + +--- + +## Cron Beispiel + +``` +0 */6 * * * +``` + +--- + +## Hinweise + +- Script beendet sich sofort, wenn keine Dateien vorhanden sind +- `ready/` wird nur bei Bedarf erstellt und kann wieder verschwinden +- Rechteproblem bei CIFS wird durch chown gelöst + +--- + +## Debug + +``` +cat /mnt/user/Cache/Picard/sync.log +ls /mnt/user/Cache/Picard +``` diff --git a/unRAID Userscript - Music sync/music_sync.sh b/unRAID Userscript - Music sync/music_sync.sh new file mode 100644 index 0000000..b26a6af --- /dev/null +++ b/unRAID Userscript - Music sync/music_sync.sh @@ -0,0 +1,200 @@ +#!/bin/bash + +# ---------------------------------------- +# CONFIG +# ---------------------------------------- +INCOMING="/mnt/user/Cache/Picard/incoming" +READY="/mnt/user/Cache/Picard/ready" + +DEST1="/mnt/user/Cache/Syncthing/Julian/Musik/Thorsten -> Julian" +DEST2="/mnt/user/Media/Musik/[0] Navidrome" + +LOG="/mnt/user/Cache/Picard/sync.log" +LOCKFILE="/mnt/user/Cache/Picard/sync.lock" + +LOG_LINES=5000 + +DRY_RUN=false + +CHOWN_USER="thorsten" +CHOWN_GROUP="users" + +# ---------------------------------------- +# NORMALIZE PATHS +# ---------------------------------------- +normalize_path() { + local p="$1" + [[ "$p" != "/" ]] && p="${p%/}" + echo "$p" +} + +INCOMING="$(normalize_path "$INCOMING")" +READY="$(normalize_path "$READY")" +DEST1="$(normalize_path "$DEST1")" +DEST2="$(normalize_path "$DEST2")" + +# ---------------------------------------- +# FUNCTIONS +# ---------------------------------------- +log() { + MSG="$(date '+%Y-%m-%d %H:%M:%S') | $1" + echo "$MSG" + echo "$MSG" >> "$LOG" +} + +rotate_log() { + if [ -f "$LOG" ]; then + tail -n "$LOG_LINES" "$LOG" > "${LOG}.tmp" && mv "${LOG}.tmp" "$LOG" + fi +} + +run_cmd() { + if [ "$DRY_RUN" = true ]; then + log "[DRY-RUN] $*" + else + "$@" + fi +} + +rsync_cmd() { + if [ "$DRY_RUN" = true ]; then + rsync -a --dry-run "$@" + return 0 + else + rsync -a "$@" + return $? + fi +} + +# ---------------------------------------- +# VERIFY DESTINATIONS +# ---------------------------------------- +for DEST in "$DEST1" "$DEST2"; do + if [ ! -d "$DEST" ]; then + echo "$(date '+%Y-%m-%d %H:%M:%S') | ERROR: Destination not found -> $DEST" + exit 1 + fi +done + +# ---------------------------------------- +# LOCKFILE +# ---------------------------------------- +if [ -f "$LOCKFILE" ]; then + echo "Lockfile exists -> $LOCKFILE" + exit 1 +fi + +trap "rm -f '$LOCKFILE'" EXIT +touch "$LOCKFILE" + +# ---------------------------------------- +# QUICK EXIT +# ---------------------------------------- +if ! find "$INCOMING" -type f -mmin +1 -print -quit | grep -q .; then + echo "$(date '+%Y-%m-%d %H:%M:%S') | No files to process -> exit" + exit 0 +fi + +# ---------------------------------------- +# START +# ---------------------------------------- +rotate_log +log "========================================" +log "START RUN (DRY_RUN=$DRY_RUN)" +log "========================================" + +# ---------------------------------------- +# TRACK CHOWNED DIRECTORIES +# ---------------------------------------- +declare -A CHOWN_DONE_DIRS + +# ---------------------------------------- +# 1. INCOMING → READY +# ---------------------------------------- +log "---- MOVE: INCOMING → READY ----" + +find "$INCOMING" -type f -mmin +1 | while read -r FILE; do + + [ -f "$FILE" ] || continue + + REL="${FILE#"$INCOMING"/}" + TARGET="$READY/$REL" + + mkdir -p "$READY/$(dirname "$REL")" + + log "MOVE -> $REL" + run_cmd mv "$FILE" "$TARGET" + +done + +# ---------------------------------------- +# 2. READY → DESTS +# ---------------------------------------- +log "---- SYNC: READY → DESTINATIONS ----" +log "DEST1: $DEST1" +log "DEST2: $DEST2" + +find "$READY" -type f 2>/dev/null | while read -r FILE; do + + [ -f "$FILE" ] || continue + + REL="${FILE#"$READY"/}" + + DEST1_FILE="$DEST1/$REL" + DEST2_FILE="$DEST2/$REL" + + mkdir -p "$(dirname "$DEST1_FILE")" + mkdir -p "$(dirname "$DEST2_FILE")" + + log "SYNC -> $REL" + + rsync_cmd "$FILE" "$DEST1_FILE" + STATUS1=$? + + rsync_cmd "$FILE" "$DEST2_FILE" + STATUS2=$? + + if [[ $STATUS1 -eq 0 && $STATUS2 -eq 0 ]]; then + + log "SET OWNER -> $REL" + + # Datei + run_cmd chown "$CHOWN_USER:$CHOWN_GROUP" "$DEST1_FILE" + run_cmd chown "$CHOWN_USER:$CHOWN_GROUP" "$DEST2_FILE" + + # Ordner nur einmal behandeln + DIR1="$(dirname "$DEST1_FILE")" + DIR2="$(dirname "$DEST2_FILE")" + + if [[ -z "${CHOWN_DONE_DIRS[$DIR1]}" ]]; then + run_cmd chown "$CHOWN_USER:$CHOWN_GROUP" "$DIR1" + CHOWN_DONE_DIRS[$DIR1]=1 + fi + + if [[ -z "${CHOWN_DONE_DIRS[$DIR2]}" ]]; then + run_cmd chown "$CHOWN_USER:$CHOWN_GROUP" "$DIR2" + CHOWN_DONE_DIRS[$DIR2]=1 + fi + + log "OK -> delete $REL" + run_cmd rm "$FILE" + + else + log "ERROR -> $REL (dest1=$STATUS1 dest2=$STATUS2)" + fi + +done + +# ---------------------------------------- +# CLEANUP +# ---------------------------------------- +if [ "$DRY_RUN" != true ]; then + find "$INCOMING" -mindepth 1 -type d -empty -delete + find "$READY" -mindepth 1 -type d -empty -delete 2>/dev/null +else + log "[DRY-RUN] Skipping cleanup" +fi + +log "========================================" +log "END RUN" +log "========================================" \ No newline at end of file