#!/bin/bash # ============================================================================================== # ================================= Storage Migration ========================================== # ============================================================================================== # # PURPOSE # ───────────────────────────────────────────────────────────────────────────── # Migrates Varaverk between internal NVMe and USB flash storage modes. # # Internal mode: SCRIPTS_DIR = /boot/config/plugins/varaverk # All scripts, conf, state, and git repo live on fast internal storage. # Direct git pull/push. Zero write-wear concern. # # Flash mode: SCRIPTS_DIR = /mnt/user/appdata/Varaverk # All scripts, conf, state, and git repo live in appdata. # Preserves USB flash lifetime. Array must be started for Varaverk to function. # git_pull_execute.sh syncs Plugin/ back to /boot/ after each pull so the # Unraid webUI always serves current PHP files. # # What this script updates: # varaverk.cfg SCRIPTS_DIR # master.conf TARGET_DIR # host*.conf HOST*_STORAGE_MODE_INTERNAL # varaverk.cron rebuilt via PHP (job paths regenerated for new SCRIPTS_DIR) # # ============================================================================================== # OPERATIONAL MODEL # ============================================================================================== # # 1. Determine current mode from varaverk.cfg SCRIPTS_DIR, and the requested target mode # Already in the target mode → exit cleanly, nothing to do # 2. rsync -a --delete SRC → DST, then carry .git across so history survives the move # 3. Rewrite the pointers, in this order: # varaverk.cfg SCRIPTS_DIR # master.conf TARGET_DIR # host*.conf HOST*_STORAGE_MODE_INTERNAL # 4. Rebuild varaverk.cron via PHP so every job path points at the new SCRIPTS_DIR # 5. Flash mode only: sync Plugin/ back to /boot so the webUI keeps serving current PHP # 6. Remove the old location once the new one is confirmed in place # # ============================================================================================== # DESIGN PRINCIPLES # ============================================================================================== # # Atomic Path Transition # All four files (varaverk.cfg, master.conf, host*.conf, varaverk.cron) are # updated in a single pass. A partial migration would leave cron entries # pointing at the wrong SCRIPTS_DIR — all or nothing. # # PHP Rebuilds Cron # Job paths in varaverk.cron are derived from SCRIPTS_DIR. Rather than # text-substituting the cron file, the script regenerates it via PHP using # the new SCRIPTS_DIR as the source of truth. # # ============================================================================================== # OPERATIONAL SAFEGUARDS # ============================================================================================== # # acquire_lock — prevents concurrent migration attempts # dry-run mode — shows all changes without touching any file # --status mode — reports current mode without requiring a target # --to= required — refuses to run without an explicit target mode # # ============================================================================================== # CONFIGURATION # ============================================================================================== # # This script WRITES these rather than reading them — they are the migration's output: # # varaverk.cfg # SCRIPTS_DIR the authoritative install path. Everything else in the # ecosystem derives from it, which is why it is written # first and the cron is rebuilt from it afterwards. # # master.conf # TARGET_DIR kept in step with SCRIPTS_DIR # # host*.conf # HOST*_STORAGE_MODE_INTERNAL true = /boot/config/plugins/varaverk # false = /mnt/user/appdata/Varaverk # # Load-bearing: STATE_DIR, DATA_DIR, PERSISTENT_CONF_CACHE and the orchestrator job paths are # all built from SCRIPTS_DIR. Changing storage mode moves every one of them, which is why the # cron is regenerated rather than edited. # # In flash mode the array must be started before Varaverk can function at all — appdata is # not mounted before that. # # ============================================================================================== # RUNTIME MODES # ============================================================================================== # # storage_migrate.sh --to=internal # Migrate to /boot/config/plugins/varaverk # # storage_migrate.sh --to=flash # Migrate to /mnt/user/appdata/Varaverk # # storage_migrate.sh --dry-run --to= # Show what would happen — no changes made # # storage_migrate.sh --status # Show current mode, paths, and boot device info # # ============================================================================================== SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" source "$SCRIPT_DIR/../../../load_config.sh" parse_args "$@" # Relocates the entire Varaverk installation with rsync --delete and rm -rf, and rewrites # varaverk.cfg, master.conf and host*.conf. Everything here needs root. [[ "$EUID" -ne 0 ]] && { error "Must be run as root"; exit 1; } acquire_lock detect_hosts VV_CFG="/boot/config/plugins/varaverk/varaverk.cfg" INTERNAL_DIR="/boot/config/plugins/varaverk" FLASH_DIR="/mnt/user/appdata/Varaverk" CONF_FILE="$CONF_DIR/${MY_ID,,}.conf" # ────────────────────────────────────────────────────────────────────────────── # Parse --to= from raw args (parse_args doesn't handle this flag) TO_MODE="" for arg in "$@"; do case "$arg" in --to=internal) TO_MODE="internal" ;; --to=flash) TO_MODE="flash" ;; esac done # ────────────────────────────────────────────────────────────────────────────── # Boot device detection detect_boot_storage() { local boot_part boot_disk transport boot_part=$(findmnt -n -o SOURCE /boot 2>/dev/null) boot_disk=$(lsblk -no pkname "$boot_part" 2>/dev/null) transport=$(lsblk -dno TRAN "/dev/$boot_disk" 2>/dev/null | tr '[:upper:]' '[:lower:]') echo "${transport:-unknown}" } # ────────────────────────────────────────────────────────────────────────────── # Status if [[ "$SHOW_STATUS" == true ]]; then TRANSPORT=$(detect_boot_storage) DETECTED=$([[ "$TRANSPORT" == "usb" ]] && echo "flash" || echo "internal") echo "" echo "━━━━━ $ICON_SUMMARY STORAGE STATUS ━━━━━" echo "$ICON_GEAR SCRIPTS_DIR: $SCRIPTS_DIR" echo "$ICON_GEAR varaverk.cfg: $VV_CFG" echo "$ICON_HOST Boot device: transport=$TRANSPORT → detected=$DETECTED" echo "$ICON_GEAR Target dirs:" echo " internal: $INTERNAL_DIR" echo " flash: $FLASH_DIR" CONF_MODE=$(grep -m1 "${MY_ID}_STORAGE_MODE_INTERNAL" "$CONF_FILE" 2>/dev/null | cut -d= -f2 | tr -d '"' | tr -d '[:space:]') echo "$ICON_GEAR conf setting: ${MY_ID}_STORAGE_MODE_INTERNAL=${CONF_MODE:-not set}" if [[ "$SCRIPTS_DIR" == "$INTERNAL_DIR" ]]; then echo "$ICON_DONE Current mode: INTERNAL ✅" elif [[ "$SCRIPTS_DIR" == "$FLASH_DIR" ]]; then echo "$ICON_DONE Current mode: FLASH ✅" else echo "$ICON_WARN Current mode: CUSTOM ($SCRIPTS_DIR)" fi echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━" exit 0 fi # ────────────────────────────────────────────────────────────────────────────── if [[ -z "$TO_MODE" ]]; then error "Usage: storage_migrate.sh --to=internal|flash [--dry-run] [--log]" exit 1 fi SRC="$SCRIPTS_DIR" DST=$([[ "$TO_MODE" == "internal" ]] && echo "$INTERNAL_DIR" || echo "$FLASH_DIR") NEW_INTERNAL=$([[ "$TO_MODE" == "internal" ]] && echo "true" || echo "false") log "$ICON_GEAR Config: to=${TO_MODE} src=${SRC} dst=${DST} dry-run=${DRY_RUN}" echo "" echo "━━━━━ $ICON_SYNC Storage Migration ━━━━━" echo "$ICON_GEAR From: $SRC" echo "$ICON_GEAR To: $DST" echo "$ICON_GEAR Mode: $TO_MODE" [[ "$DRY_RUN" == true ]] && warn "DRY RUN — no changes will be made" echo "" # ────────────────────────────────────────────────────────────────────────────── # Pre-flight checks if [[ "$SRC" == "$DST" ]]; then info "Already in $TO_MODE mode — nothing to do" exit 0 fi if [[ "$TO_MODE" == "flash" ]]; then if ! mountpoint -q /mnt/user 2>/dev/null; then error "Array not started — /mnt/user is not mounted. Start the array before migrating to flash." exit 1 fi fi if [[ ! -f "$SRC/load_config.sh" ]]; then error "Source directory looks invalid: $SRC (load_config.sh not found)" exit 1 fi # ────────────────────────────────────────────────────────────────────────────── # Step 1: Git push — ensure remote has everything before we touch the local repo echo "━━━ $ICON_SYNC Step 1: Git push ━━━" if [[ "$DRY_RUN" == false ]]; then if [[ -d "$SRC/.git" ]]; then log "Pushing to Gitea before migration..." if GIT_SSH_COMMAND="ssh -i $GITEA_SSH_KEY -p $SSH_PORT" \ git -C "$SRC" push origin main 2>&1 | while IFS= read -r line; do echo " $line"; done; then echo " Git push complete ✅" else warn "Git push failed — continuing (data safe locally, push manually after migration)" fi else warn "No .git directory in $SRC — skipping push" fi else warn "DRY RUN — would push $SRC to Gitea" fi # ────────────────────────────────────────────────────────────────────────────── # Step 2: Rsync content to destination echo "" echo "━━━ $ICON_SYNC Step 2: Copy files ━━━" if [[ "$DRY_RUN" == false ]]; then mkdir -p "$DST" echo " rsync: $SRC/ → $DST/" if rsync -av --delete \ --exclude='.git' \ "$SRC/" "$DST/" 2>&1 | \ grep -v "/$" | \ while IFS= read -r line; do log "$line"; done; then echo " Files copied ✅" else error "rsync failed — aborting migration" exit 1 fi # Copy .git separately (rsync --exclude='.git' above skipped it) echo " Copying .git..." if cp -a "$SRC/.git" "$DST/.git" 2>/dev/null || \ rsync -a "$SRC/.git/" "$DST/.git/" 2>/dev/null; then echo " .git copied ✅" else error ".git copy failed — aborting" exit 1 fi # Mark git safe directory git config --global --add safe.directory "$DST" 2>/dev/null else warn "DRY RUN — would rsync $SRC/ → $DST/ (including .git)" fi # ────────────────────────────────────────────────────────────────────────────── # Step 3: Update varaverk.cfg echo "" echo "━━━ $ICON_GEAR Step 3: Update varaverk.cfg ━━━" if [[ "$DRY_RUN" == false ]]; then if grep -q '^SCRIPTS_DIR' "$VV_CFG"; then sed -i "s|^SCRIPTS_DIR=.*|SCRIPTS_DIR=\"$DST\"|" "$VV_CFG" else echo "SCRIPTS_DIR=\"$DST\"" >> "$VV_CFG" fi echo " SCRIPTS_DIR → $DST ✅" else warn "DRY RUN — would set SCRIPTS_DIR=\"$DST\" in $VV_CFG" fi # ────────────────────────────────────────────────────────────────────────────── # Step 4: Update TARGET_DIR, DATA_DIR, STATE_DIR in master.conf (new location) echo "" echo "━━━ $ICON_GEAR Step 4: Update master.conf paths ━━━" NEW_MASTER="$DST/Configurations/master.conf" if [[ "$DRY_RUN" == false ]]; then if [[ -f "$NEW_MASTER" ]]; then # Only rewrite a path that is a literal. These three are derived from SCRIPTS_DIR in # current confs, which load_config.sh exports from its own location — so after the move # they already point at the new install and hardcoding them here would turn a # self-correcting value back into one that has to be maintained. Older confs still carry # literals, so the substitution stays for them. _mig_path() { # $1=key $2=new value if grep -qE "^\s*$1\s*=\s*\"[^\"]*\\\$\{?(SCRIPTS_DIR|DATA_DIR)" "$NEW_MASTER"; then echo " $1 — already derived, left alone ✅" else sed -i "s|^\(\s*$1\s*=\s*\)\"[^\"]*\"|\1\"$2\"|" "$NEW_MASTER" echo " $1 → $2 ✅" fi } _mig_path TARGET_DIR "$DST" _mig_path DATA_DIR "$DST/data" _mig_path STATE_DIR "$DST/data/state" unset -f _mig_path else error "master.conf not found at $NEW_MASTER" exit 1 fi else warn "DRY RUN — would update TARGET_DIR, DATA_DIR, STATE_DIR in master.conf" fi # ────────────────────────────────────────────────────────────────────────────── # Step 5: Update STORAGE_MODE_INTERNAL in host*.conf (new location) echo "" echo "━━━ $ICON_GEAR Step 5: Update STORAGE_MODE_INTERNAL ━━━" NEW_CONF="$DST/Configurations/${MY_ID,,}.conf" if [[ "$DRY_RUN" == false ]]; then if [[ -f "$NEW_CONF" ]]; then if grep -q "${MY_ID}_STORAGE_MODE_INTERNAL" "$NEW_CONF"; then sed -i "s|^\(\s*${MY_ID}_STORAGE_MODE_INTERNAL\s*=\s*\).*|\1${NEW_INTERNAL}|" "$NEW_CONF" else sed -i "/# ━━━ Storage mode/a\\ ${MY_ID}_STORAGE_MODE_INTERNAL=${NEW_INTERNAL}" "$NEW_CONF" fi echo " ${MY_ID}_STORAGE_MODE_INTERNAL → $NEW_INTERNAL ✅" else warn "${MY_ID,,}.conf not found at $NEW_CONF — skipping conf update" fi else warn "DRY RUN — would set ${MY_ID}_STORAGE_MODE_INTERNAL=$NEW_INTERNAL" fi # ────────────────────────────────────────────────────────────────────────────── # Step 6: Rebuild cron (paths must reference new SCRIPTS_DIR) echo "" echo "━━━ $ICON_GEAR Step 6: Rebuild cron ━━━" if [[ "$DRY_RUN" == false ]]; then RESULT=$(php -r " define('PLUGIN_CFG', '/boot/config/plugins/varaverk/varaverk.cfg'); \$_c = @parse_ini_file(PLUGIN_CFG) ?: []; define('SCRIPTS_DIR', \$_c['SCRIPTS_DIR'] ?? '/boot/config/plugins/varaverk'); define('CONF_DIR', SCRIPTS_DIR . '/Configurations'); define('DATA_DIR', SCRIPTS_DIR . '/data'); define('STATE_DIR', SCRIPTS_DIR . '/data/state'); define('LOG_DIR', '/var/log/varaverk'); define('VV_SETUP_STATE_FILE', STATE_DIR . '/varaverk_setup.db'); require_once '/usr/local/emhttp/plugins/varaverk/include/confform.php'; require_once '/usr/local/emhttp/plugins/varaverk/include/scheduler.php'; \$ok = vv_cron_rebuild(vv_schedule_load()); echo \$ok ? 'ok' : 'fail'; " 2>/dev/null) if [[ "$RESULT" == "ok" ]]; then echo " Cron rebuilt ✅" else warn "Cron rebuild failed — run Settings → Scheduler → Save to regenerate" fi else warn "DRY RUN — would rebuild cron with new SCRIPTS_DIR paths" fi # ────────────────────────────────────────────────────────────────────────────── # Step 7: Flash mode — sync Plugin/ to /boot/ so webUI is current if [[ "$TO_MODE" == "flash" && "$DRY_RUN" == false ]]; then echo "" echo "━━━ $ICON_SYNC Step 7: Sync Plugin/ → /boot/ ━━━" if rsync -a --delete "$DST/Plugin/" "$INTERNAL_DIR/Plugin/" 2>/dev/null; then echo " Plugin/ synced to /boot/ ✅" else warn "Plugin/ sync to /boot/ failed — webUI may be stale" fi fi # ────────────────────────────────────────────────────────────────────────────── # Step 8: Delete old location echo "" echo "━━━ $ICON_GEAR Step $([[ "$TO_MODE" == "flash" ]] && echo 8 || echo 7): Clean up old location ━━━" if [[ "$DRY_RUN" == false ]]; then if [[ "$SRC" == "$INTERNAL_DIR" ]]; then # Migrating internal→flash: keep varaverk.cfg and Plugin/ in /boot/, remove everything else echo " Removing scripts/conf/state from /boot/ (keeping Plugin/ and varaverk.cfg)..." find "$SRC" -mindepth 1 -maxdepth 1 \ ! -name 'Plugin' \ ! -name 'varaverk.cfg' \ ! -name '*.plg' \ ! -name '*.txz' \ -exec rm -rf {} + 2>/dev/null echo " /boot/ cleaned ✅ (Plugin/ and varaverk.cfg preserved)" else # Migrating flash→internal: remove appdata copy entirely echo " Removing $SRC..." rm -rf "$SRC" echo " $SRC removed ✅" fi else if [[ "$SRC" == "$INTERNAL_DIR" ]]; then warn "DRY RUN — would remove scripts/conf/state from /boot/ (keeping Plugin/ and varaverk.cfg)" else warn "DRY RUN — would remove $SRC" fi fi # ── Repoint the WebGUI at the new location ──────────────────────────────────── # The .plg rebuilds this symlink on every boot and now follows SCRIPTS_DIR, but a migration is # not a boot. Without this the WebGUI keeps serving the old copy until the next restart, so a # migration appears to work while every page, endpoint and pull-target is still the old tree. WEB_DIR="/usr/local/emhttp/plugins/varaverk" if [[ "$DRY_RUN" == true ]]; then warn "DRY RUN — would repoint $WEB_DIR → $DST/Plugin/unraid" elif [[ -d "$DST/Plugin/unraid" ]]; then ln -sfn "$DST/Plugin/unraid" "$WEB_DIR" if [[ "$(readlink -f "$WEB_DIR")" == "$(readlink -f "$DST/Plugin/unraid")" ]]; then echo "WebGUI repointed → $DST/Plugin/unraid ✅" else warn "Could not repoint $WEB_DIR — the WebGUI will keep serving the old tree until reboot" fi else warn "$DST/Plugin/unraid missing — WebGUI left pointing at the old tree" fi # ────────────────────────────────────────────────────────────────────────────── echo "" echo "━━━━━ $ICON_DONE Migration complete ━━━━━" echo "$ICON_GEAR Mode: $TO_MODE" echo "$ICON_GEAR SCRIPTS_DIR: $DST" if [[ "$TO_MODE" == "flash" ]]; then echo "" warn "IMPORTANT: Varaverk requires the array to be started to function in flash mode." warn "The webUI plugin tab will load normally at all times (Plugin/ stays in /boot/)." fi echo "" echo " Reload the Varaverk plugin tab to apply changes." echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"