infra/scripts/anca-elements-sync.sh
Viktor Barzin 34f8c0f537
Some checks failed
ci/woodpecker/push/build-cli Pipeline failed
ci/woodpecker/push/default Pipeline was successful
docs+scripts: lock in nextcloud-as-PVE-NFS-browser surface
- docs/architecture/storage.md: new "Nextcloud as PVE-NFS browser"
  section documenting mount-per-archive + applicable_users model,
  why mount-level ACL beats Files Access Control on NC 30/31, the
  manifest shape (with current applicableUsers + enableSharing
  fields), and the trade-off
- docs/runbooks/nextcloud-add-archive.md: 5-step runbook to surface
  a new directory under /srv/nfs/* to specific NC users via the
  bootstrap Job
- scripts/anca-elements-sync.sh: deployed at
  /usr/local/bin/anca-elements-sync.sh on the PVE host; fpsync from
  Synology Anca/Elements to /srv/nfs/anca-elements (idempotent +
  resumable). The PVE replica is what the NC /anca-elements mount
  serves; the offsite-sync pipeline excludes this path (committed
  earlier this session) so we don't write it back to Synology

NC usernames are admin/anca/emo (not display names — admin is
Viktor). Stale "viktor" references in the manifest example dropped.
2026-05-24 11:45:01 +00:00

112 lines
4.2 KiB
Bash
Executable file

#!/usr/bin/env bash
# anca-elements-sync.sh — copy Anca's WD-Elements backup from Synology to PVE NFS
#
# Usage:
# /usr/local/bin/anca-elements-sync.sh
#
# Idempotent: re-running after a successful sync is a no-op (only the dry-run
# verification runs, which reports "sync verified clean" immediately).
#
# Resumable: if fpsync was interrupted, resume with:
# fpsync -r /var/tmp/fpsync \
# -n 4 -s 4G \
# -o "-lptgoD -H --no-perms --no-owner --no-group --exclude=@eaDir/ --exclude=*@synoeastream --exclude=.DS_Store --exclude=Thumbs.db" \
# /mnt/synology-backup/Anca/Elements/ /srv/nfs/anca-elements/
#
# NOTE: fpsync -o = rsync options override (what we want)
# fpsync -O = fpart partition options override (NOT rsync)
# NOTE: Do NOT use -a or -r in fpsync rsync options — fpsync handles
# recursion via fpart; -r causes fpsync to warn and skip the slab.
#
# Log: /var/log/anca-elements-sync.log
set -euo pipefail
LOG=/var/log/anca-elements-sync.log
SRC_HOST=192.168.1.13
SRC_EXPORT=/volume1/Backup
SRC_SUBPATH=Anca/Elements
MOUNT_POINT=/mnt/synology-backup
DEST=/srv/nfs/anca-elements
log() {
echo "[$(date -u '+%Y-%m-%dT%H:%M:%SZ')] $*" | tee -a "$LOG"
}
# ── 1. Ensure destination + mount-point directories exist ────────────────────
log "Step 1: ensuring directories"
mkdir -p "$DEST" "$MOUNT_POINT"
# ── 2. NFS-mount Synology read-only (skip if already mounted) ───────────────
MOUNTED_HERE=0
if mountpoint -q "$MOUNT_POINT"; then
log "Step 2: $MOUNT_POINT already mounted — skipping"
else
log "Step 2: mounting ${SRC_HOST}:${SRC_EXPORT} at $MOUNT_POINT (read-only)"
mount -t nfs \
-o ro,vers=4,nolock,soft,timeo=300,retrans=2 \
"${SRC_HOST}:${SRC_EXPORT}" \
"$MOUNT_POINT"
MOUNTED_HERE=1
log "Step 2: mount successful"
fi
# ── 3. Ensure fpsync (from fpart package) is available ──────────────────────
log "Step 3: checking for fpsync"
if ! command -v fpsync >/dev/null 2>&1; then
log "Step 3: fpsync not found — installing fpart"
apt-get install -y fpart
log "Step 3: fpart installed"
else
log "Step 3: fpsync already available"
fi
# ── 4. Run fpsync (4-way parallel, no compression — source is already-compressed media) ──
log "Step 4: starting fpsync"
log " source : ${MOUNT_POINT}/${SRC_SUBPATH}/"
log " dest : ${DEST}/"
log " workers: 4, slab: 4G"
fpsync \
-n 4 \
-s 4G \
-o "-lptgoD -H --no-perms --no-owner --no-group --exclude=@eaDir/ --exclude=*@synoeastream --exclude=.DS_Store --exclude=Thumbs.db" \
"${MOUNT_POINT}/${SRC_SUBPATH}/" \
"${DEST}/" \
2>&1 | tee -a "$LOG"
log "Step 4: fpsync completed"
# ── 5. Verification dry-run ──────────────────────────────────────────────────
log "Step 5: running dry-run verification rsync"
VERIFY_OUT=$(rsync \
-rlptgoD -H --no-perms --no-owner --no-group \
--exclude='@eaDir/' --exclude='*@synoeastream' \
--exclude='.DS_Store' --exclude='Thumbs.db' \
-n --delete \
--info=progress2 \
--out-format='%o %f' \
"${MOUNT_POINT}/${SRC_SUBPATH}/" \
"${DEST}/" \
2>&1 || true)
# Count lines that represent actual file changes (send / del. operations)
CHANGE_COUNT=$(echo "$VERIFY_OUT" | grep -cE '^(send|del\.)' || true)
if [ "$CHANGE_COUNT" -eq 0 ]; then
log "Step 5: sync verified clean — no pending changes"
else
log "Step 5: WARNING — verification found ${CHANGE_COUNT} pending change(s). First 50 lines:"
# Use printf to avoid SIGPIPE from head closing the pipe early (set -o pipefail)
{ echo "$VERIFY_OUT" | head -50; } >> "$LOG" 2>&1 || true
fi
# ── 6. Unmount (only if we mounted it) ──────────────────────────────────────
if [ "$MOUNTED_HERE" -eq 1 ]; then
log "Step 6: unmounting $MOUNT_POINT"
umount "$MOUNT_POINT"
rmdir "$MOUNT_POINT"
log "Step 6: unmounted"
else
log "Step 6: mount was pre-existing — leaving in place"
fi
log "Done. Final size: $(du -sh "${DEST}" | cut -f1)"