311 lines
16 KiB
Bash
Executable File
311 lines
16 KiB
Bash
Executable File
#!/bin/bash
|
|
# ==============================================================================================
|
|
# ============================== DATA LAYOUT MIGRATION =========================================
|
|
# ==============================================================================================
|
|
#
|
|
# PURPOSE
|
|
# ─────────────────────────────────────────────────────────────────────────────
|
|
# One-time move of everything Varaverk persists into a single rooted tree under DATA_DIR.
|
|
#
|
|
# State_Files/ → data/state/
|
|
# data/*.db|.count|.tsv|.list → data/db/
|
|
# data/ai_* → data/ai/
|
|
# data/*_tracked_cache.json → data/cache/arr/
|
|
# data/*.log → data/logs/
|
|
# SCRIPTS_DIR/.cache/vv/d/ → data/cache/conf/
|
|
#
|
|
# ==============================================================================================
|
|
# WHY THIS EXISTS SEPARATELY FROM conf_upgrade
|
|
# ==============================================================================================
|
|
#
|
|
# conf_upgrade adds keys the template has and the installation does not; it never rewrites a
|
|
# value the operator already has, which is exactly the behaviour you want from it and exactly
|
|
# why it cannot perform this migration. The paths being moved are existing keys — STATE_DIR,
|
|
# BANDWIDTH_LOG, AI_INDEX_DB and two dozen more — so their values would keep pointing at the old
|
|
# layout forever while the new directory variables sat beside them unused.
|
|
#
|
|
# So this rewrites those values, then moves the files to match. Both halves, or neither: a conf
|
|
# pointing at a directory the data is not in is worse than not having started.
|
|
#
|
|
# ==============================================================================================
|
|
# OPERATIONAL MODEL
|
|
# ==============================================================================================
|
|
#
|
|
# Two halves, in order: move the files, then rewrite the conf keys that point at them. Doing it
|
|
# the other way round would leave every path variable naming a location nothing had reached yet,
|
|
# and any script that ran in between would create the old layout again underneath the new one.
|
|
#
|
|
# Idempotent. A path already under DATA_DIR is left alone, so a re-run after a partial migration
|
|
# finishes the job rather than moving things twice or failing on what is already done.
|
|
#
|
|
# One-time by intent, not by a marker file. There is no "already migrated" flag — the check is
|
|
# whether each individual path is already where it belongs, which is also what makes an
|
|
# interrupted run safe to repeat.
|
|
#
|
|
# ==============================================================================================
|
|
# DESIGN PRINCIPLES
|
|
# ==============================================================================================
|
|
#
|
|
# Existing keys are rewritten, which is why conf_upgrade cannot do this.
|
|
# conf_upgrade adds keys the template has and the installation does not, and never rewrites a
|
|
# value the operator already holds — correct for it, and exactly why it is the wrong tool here.
|
|
# STATE_DIR, BANDWIDTH_LOG, AI_INDEX_DB and two dozen more are existing keys whose values must
|
|
# change, or they would go on naming the old layout forever while the new directory variables
|
|
# sat beside them unused.
|
|
#
|
|
# Move, never copy-and-hope.
|
|
# The data being relocated is the only copy — statistics, histories, the AI index, arr caches.
|
|
# Everything is moved and the source is gone afterwards, so there is no second location that
|
|
# might still be written to by something that missed the change.
|
|
#
|
|
# The conf rewrite is the last thing, and the riskiest thing.
|
|
# Until it happens the installation still works from the old layout. That ordering means an
|
|
# abort partway through leaves a system that runs, rather than one whose paths point at
|
|
# nothing.
|
|
#
|
|
# ==============================================================================================
|
|
# OPERATIONAL SAFEGUARDS
|
|
# ==============================================================================================
|
|
#
|
|
# Idempotent
|
|
# Every step tests before acting. A second run reports "already migrated" and changes nothing,
|
|
# which matters because the natural instinct after a partial failure is to run it again.
|
|
#
|
|
# Moves, never copies-and-deletes
|
|
# mv within one filesystem is atomic per file, so a reader either sees the file at the old
|
|
# path or the new one — never a half-written copy at both. Nothing is deleted; if a file
|
|
# cannot be moved it is reported and left exactly where it is.
|
|
#
|
|
# Conf is backed up before it is rewritten
|
|
# master.conf.bak-<stamp>, next to the original, same convention conf_upgrade uses.
|
|
#
|
|
# Refuses to run while the orchestrators might be writing
|
|
# A watchdog that sourced conf before the rewrite and writes state after the move would put a
|
|
# file back at the old path. The window is seconds and the damage is one stale file, but the
|
|
# check costs nothing and the failure is silent otherwise.
|
|
#
|
|
# ==============================================================================================
|
|
# CONFIGURATION
|
|
# ==============================================================================================
|
|
#
|
|
# This script reads conf to find the old locations and rewrites conf to record the new ones. It
|
|
# is the one script here whose purpose is to change these values rather than obey them.
|
|
#
|
|
# Read to locate what moves
|
|
# STATE_DIR, BANDWIDTH_LOG, AI_INDEX_DB, AI_MEMORY_FILE, AI_TOKEN_DB, ARR_CLEANUP_STATS,
|
|
# ARR_SYNC_BLOCKLIST, CORRUPTION_SCAN_STATE_FILE, LIDARR_CACHE_FILE, ZFS_REPORT_LOG and the
|
|
# rest of the per-script path keys — roughly two dozen in total.
|
|
#
|
|
# Written as the new roots
|
|
# DATA_DIR and the directories beneath it: DB_DIR, STATE_DIR, AI_DATA_DIR,
|
|
# CACHE_BACKUP_DIR, ARR_CACHE_BACKUP_DIR, CONF_CACHE_BACKUP_DIR, LOG_ARCHIVE_DIR.
|
|
#
|
|
# Every rewritten value is expressed as ${DB_DIR}/… rather than an absolute path, so a later
|
|
# storage-mode migration moves them again by changing one variable.
|
|
#
|
|
# ==============================================================================================
|
|
# RUNTIME MODES
|
|
# ==============================================================================================
|
|
#
|
|
# migrate_data_layout.sh --dry-run Show what would move. Changes nothing. Do this first.
|
|
# migrate_data_layout.sh Perform the migration.
|
|
# migrate_data_layout.sh --force Skip the running-orchestrator check.
|
|
#
|
|
# ==============================================================================================
|
|
|
|
set -uo pipefail
|
|
|
|
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
|
ROOT="$(cd "$SCRIPT_DIR/.." && pwd)"
|
|
CONF="$ROOT/Configurations/master.conf"
|
|
|
|
DRY_RUN=false
|
|
FORCE=false
|
|
for a in "$@"; do
|
|
case "$a" in
|
|
--dry-run) DRY_RUN=true ;;
|
|
--force) FORCE=true ;;
|
|
*) echo "Unknown argument: $a" >&2; exit 2 ;;
|
|
esac
|
|
done
|
|
|
|
[[ -f "$CONF" ]] || { echo "[FATAL] master.conf not found at $CONF" >&2; exit 1; }
|
|
|
|
# Resolve the roots the same way load_config.sh will after this runs.
|
|
SCRIPTS_DIR="$ROOT"
|
|
DATA_DIR="$(grep -m1 -E '^\s*DATA_DIR=' "$CONF" | cut -d'"' -f2)"
|
|
DATA_DIR="${DATA_DIR//\$\{SCRIPTS_DIR\}/$SCRIPTS_DIR}"
|
|
DATA_DIR="${DATA_DIR:-$ROOT/data}"
|
|
|
|
OLD_STATE="$ROOT/State_Files"
|
|
OLD_CONFCACHE="$ROOT/.cache/vv/d"
|
|
|
|
DB_DIR="$DATA_DIR/db"
|
|
STATE_DIR="$DATA_DIR/state"
|
|
AI_DATA_DIR="$DATA_DIR/ai"
|
|
CACHE_BACKUP_DIR="$DATA_DIR/cache"
|
|
ARR_CACHE_BACKUP_DIR="$CACHE_BACKUP_DIR/arr"
|
|
CONF_CACHE_BACKUP_DIR="$CACHE_BACKUP_DIR/conf"
|
|
LOG_ARCHIVE_DIR="$DATA_DIR/logs"
|
|
|
|
moved=0; skipped=0; failed=0
|
|
|
|
say() { printf ' %s\n' "$*"; }
|
|
step() { printf '\n━━━ %s ━━━\n' "$*"; }
|
|
|
|
# ── Guard: orchestrators mid-run ──────────────────────────────────────────────
|
|
# Scoped to THIS installation's path, not to the script names. pgrep is system-wide, and a box
|
|
# running both a production checkout and a development clone will always have one of them busy —
|
|
# matching on "Orchestrators/" alone made a dev migration abort because prod was mid-cycle, which
|
|
# is a process that cannot touch this tree's data at all. The full path is what distinguishes
|
|
# them, so that is what is matched.
|
|
if [[ "$FORCE" == false && "$DRY_RUN" == false ]]; then
|
|
running=$(pgrep -fa "$ROOT/(Orchestrators|Watchdogs|Media|Rsync)/" 2>/dev/null \
|
|
| grep -v "$$" | grep -v migrate_data_layout || true)
|
|
if [[ -n "$running" ]]; then
|
|
echo "[ABORT] A job from this installation is running — it may rewrite state mid-move:" >&2
|
|
echo "$running" >&2
|
|
echo "Wait for it to finish, or re-run with --force if you are sure." >&2
|
|
exit 1
|
|
fi
|
|
fi
|
|
|
|
# ── Move one path ─────────────────────────────────────────────────────────────
|
|
move() {
|
|
local src="$1" dstdir="$2" base
|
|
base="$(basename "$src")"
|
|
[[ -e "$src" ]] || return 0
|
|
if [[ -e "$dstdir/$base" ]]; then
|
|
say "skip $base — already at ${dstdir#$DATA_DIR/}/"
|
|
((skipped++)); return 0
|
|
fi
|
|
if [[ "$DRY_RUN" == true ]]; then
|
|
say "would $base → ${dstdir#$DATA_DIR/}/"
|
|
((moved++)); return 0
|
|
fi
|
|
mkdir -p "$dstdir" 2>/dev/null
|
|
if mv "$src" "$dstdir/$base" 2>/dev/null; then
|
|
say "moved $base → ${dstdir#$DATA_DIR/}/"
|
|
((moved++))
|
|
else
|
|
say "FAILED $base — left in place"
|
|
((failed++))
|
|
fi
|
|
}
|
|
|
|
# ── 1. Rewrite the conf values conf_upgrade cannot ────────────────────────────
|
|
step "Step 1: master.conf path values"
|
|
if grep -q 'STATE_DIR="\${DATA_DIR}/state"' "$CONF"; then
|
|
say "already migrated — no conf changes needed"
|
|
else
|
|
if [[ "$DRY_RUN" == false ]]; then
|
|
cp "$CONF" "${CONF}.bak-$(date +%Y%m%d%H%M%S)"
|
|
sed -i -E \
|
|
-e 's|^(\s*STATE_DIR=)".*"|\1"${DATA_DIR}/state"|' \
|
|
-e 's|^(\s*PERSISTENT_CONF_CACHE=)".*"|\1"${CACHE_BACKUP_DIR}/conf"|' \
|
|
"$CONF"
|
|
for v in ARR_SYNC_BLOCKLIST DOCKER_UPDATE_REBUILT_DAILY_FILE DOCKER_UPDATE_REBUILT_WEEKLY_FILE \
|
|
WATCHDOG_CONTAINER_RESTART_LOG TUNING_MONITOR_LOG LIDARR_TRACKED_COUNT_FILE \
|
|
LIDARR_RESCAN_DURATION_DB LIDARR_ART_MISS_CACHE LIDARR_DISCOVERY_HISTORY \
|
|
SONARR_DISCOVERY_HISTORY RADARR_DISCOVERY_HISTORY SONARR_TRACKED_COUNT_FILE \
|
|
CORRUPTION_SCAN_STATE_FILE CORRUPTION_SCAN_STRIKES_FILE RADARR_TRACKED_COUNT_FILE \
|
|
TRANSCODE_DAILY_LOG BANDWIDTH_LOG ARR_CLEANUP_STATS ARR_RECOVERY_STATS \
|
|
ARR_RECOVERY_FAILURE_COUNTS; do
|
|
sed -i -E "s|^(\s*${v}=\")\\\$\{?DATA_DIR\}?/|\1\${DB_DIR}/|" "$CONF"
|
|
done
|
|
for v in AI_INDEX_DB AI_MEMORY_FILE AI_TOKEN_DB; do
|
|
sed -i -E "s|^(\s*${v}=\")\\\$\{?DATA_DIR\}?/|\1\${AI_DATA_DIR}/|" "$CONF"
|
|
done
|
|
sed -i -E 's|^(\s*LIDARR_CACHE_FILE=")\$\{?DATA_DIR\}?/|\1${ARR_CACHE_BACKUP_DIR}/|' "$CONF"
|
|
sed -i -E 's|^(\s*ZFS_REPORT_LOG=")\$\{?DATA_DIR\}?/|\1${LOG_ARCHIVE_DIR}/|' "$CONF"
|
|
say "rewritten — backup kept beside it"
|
|
else
|
|
say "would rewrite STATE_DIR, PERSISTENT_CONF_CACHE and 25 file paths"
|
|
fi
|
|
fi
|
|
|
|
# ── 2. Build the tree ─────────────────────────────────────────────────────────
|
|
step "Step 2: directory tree"
|
|
for d in "$DB_DIR" "$STATE_DIR" "$AI_DATA_DIR" "$ARR_CACHE_BACKUP_DIR" "$LOG_ARCHIVE_DIR"; do
|
|
if [[ -d "$d" ]]; then say "exists ${d#$DATA_DIR/}"
|
|
elif [[ "$DRY_RUN" == true ]]; then say "would create ${d#$DATA_DIR/}"
|
|
else mkdir -p "$d" && say "created ${d#$DATA_DIR/}"
|
|
fi
|
|
done
|
|
# The conf cache carries partner credentials and keeps its restrictive mode.
|
|
if [[ "$DRY_RUN" == false ]]; then
|
|
mkdir -p "$CONF_CACHE_BACKUP_DIR" && chmod 700 "$CONF_CACHE_BACKUP_DIR"
|
|
say "created cache/conf (0700)"
|
|
fi
|
|
|
|
# ── 3. State files ────────────────────────────────────────────────────────────
|
|
step "Step 3: State_Files → data/state"
|
|
if [[ -d "$OLD_STATE" ]]; then
|
|
shopt -s nullglob dotglob
|
|
for f in "$OLD_STATE"/*; do move "$f" "$STATE_DIR"; done
|
|
shopt -u nullglob dotglob
|
|
if [[ "$DRY_RUN" == false && -d "$OLD_STATE" ]]; then
|
|
rmdir "$OLD_STATE" 2>/dev/null && say "removed empty State_Files/" \
|
|
|| say "State_Files/ not empty — left in place, inspect it"
|
|
fi
|
|
else
|
|
say "no State_Files/ — nothing to do"
|
|
fi
|
|
|
|
# ── 4. Sort the data root ─────────────────────────────────────────────────────
|
|
step "Step 4: sort data/ into subfolders"
|
|
classify() {
|
|
local f="$1" base; base="$(basename "$f")"
|
|
case "$base" in
|
|
ai_*) move "$f" "$AI_DATA_DIR" ;;
|
|
*_tracked_cache.json) move "$f" "$ARR_CACHE_BACKUP_DIR" ;;
|
|
*.log) move "$f" "$LOG_ARCHIVE_DIR" ;;
|
|
*.db|*.count|*.tsv|*.list|*.json) move "$f" "$DB_DIR" ;;
|
|
*) say "leave $base — unclassified, left in data/" ;;
|
|
esac
|
|
}
|
|
|
|
# Two passes, sidecars first. A SQLite database is three files, and the -wal holds committed
|
|
# transactions that have not been checkpointed into the .db yet. Move the .db first and any
|
|
# process that opens it during the gap sees a database with no write-ahead log, creates a fresh
|
|
# one at the old path, and everything still in the old -wal is lost when it is moved over the
|
|
# top. Sidecars ahead of their base closes that ordering: the worst case becomes a database
|
|
# opened without its log still sitting beside it, which SQLite handles.
|
|
shopt -s nullglob
|
|
for f in "$DATA_DIR"/*-wal "$DATA_DIR"/*-shm; do
|
|
[[ -d "$f" ]] && continue
|
|
classify "$f"
|
|
done
|
|
for f in "$DATA_DIR"/*; do
|
|
# Skip the destinations by name, not everything that happens to be a directory. ai_bugs/ and
|
|
# ai_chats/ are directories that belong under ai/, and an earlier version of this skipped
|
|
# every directory outright — which moved neither, silently, while the PHP layer had already
|
|
# been repointed at the new location. They were empty at the time; that was luck, not design.
|
|
case "$(basename "$f")" in
|
|
db|state|ai|cache|logs) continue ;;
|
|
*-wal|*-shm) continue ;;
|
|
esac
|
|
classify "$f"
|
|
done
|
|
shopt -u nullglob
|
|
|
|
# ── 5. Conf cache backup ──────────────────────────────────────────────────────
|
|
step "Step 5: conf cache backup"
|
|
if [[ -d "$OLD_CONFCACHE" ]]; then
|
|
shopt -s nullglob dotglob
|
|
for f in "$OLD_CONFCACHE"/*; do move "$f" "$CONF_CACHE_BACKUP_DIR"; done
|
|
shopt -u nullglob dotglob
|
|
[[ "$DRY_RUN" == false ]] && rmdir "$OLD_CONFCACHE" "$ROOT/.cache/vv" "$ROOT/.cache" 2>/dev/null
|
|
else
|
|
say "no $OLD_CONFCACHE — nothing to do"
|
|
fi
|
|
|
|
# ── Summary ───────────────────────────────────────────────────────────────────
|
|
printf '\n━━━━━ SUMMARY ━━━━━\n'
|
|
printf ' %-10s %s\n' "moved:" "$moved"
|
|
printf ' %-10s %s\n' "skipped:" "$skipped"
|
|
printf ' %-10s %s\n' "failed:" "$failed"
|
|
[[ "$DRY_RUN" == true ]] && printf '\n DRY RUN — nothing was changed.\n'
|
|
[[ "$failed" -gt 0 ]] && exit 1
|
|
exit 0
|