.
This commit is contained in:
206
unRAID Userscript - Music sync/README.MD
Normal file
206
unRAID Userscript - Music sync/README.MD
Normal file
@@ -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
|
||||||
|
```
|
||||||
200
unRAID Userscript - Music sync/music_sync.sh
Normal file
200
unRAID Userscript - Music sync/music_sync.sh
Normal file
@@ -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 "========================================"
|
||||||
Reference in New Issue
Block a user