#!/bin/bash # Apply a cross-host migrate. Wraps restoreAppStart with the migrate-specific # scaffolding (pre-backup of destination, URL rewrite of host-bound CFG_* # fields, structured progress) so the WebUI can drive it cleanly. # # Two entry points: # migrateApplyApp [loc_idx] [opts...] # migrateApplySystem [loc_idx] [opts...] # # Opts (any order, all defaults match the WebUI defaults): # --no-pre-backup Skip the safety snapshot of destination's # --keep-urls Don't rewrite host-bound CFG__* fields # --json-progress Emit one JSON record per stage on fd 3 / stdout # (sets MIGRATE_JSON_PROGRESS=1 for nested helpers) # # Exit: 0 on success, non-zero if the migrate failed at any stage. # Strip our --foo opts out of the positional args and set MIGRATE_OPT_* flags. # Mutates the caller's $1.. via printf-and-reread; returns the cleaned list # via the global _MIGRATE_REMAINING_ARGS array. _migrateParseOpts() { MIGRATE_OPT_PRE_BACKUP=1 MIGRATE_OPT_KEEP_URLS=0 MIGRATE_OPT_JSON_PROGRESS=0 _MIGRATE_REMAINING_ARGS=() local a for a in "$@"; do case "$a" in --no-pre-backup) MIGRATE_OPT_PRE_BACKUP=0 ;; --keep-urls) MIGRATE_OPT_KEEP_URLS=1 ;; --json-progress) MIGRATE_OPT_JSON_PROGRESS=1; export MIGRATE_JSON_PROGRESS=1 ;; --) : ;; # arg separator, ignore *) _MIGRATE_REMAINING_ARGS+=("$a") ;; esac done } migrateApplyApp() { _migrateParseOpts "$@" local source_host="${_MIGRATE_REMAINING_ARGS[0]}" local app="${_MIGRATE_REMAINING_ARGS[1]}" local idx="${_MIGRATE_REMAINING_ARGS[2]}" if [[ -z "$source_host" || -z "$app" ]]; then isError "migrateApplyApp: source_host and app required" return 1 fi [[ -z "$idx" ]] && idx=$(resticEnabledLocations | head -1) if [[ -z "$idx" ]]; then isError "No backup locations enabled — cannot migrate" return 1 fi local started_at started_at=$(date +%s) isHeader "Migrate $app from host=$source_host → this host" migrateEmit phase=start status=running app="$app" source_host="$source_host" loc_idx="$idx" # ---- 1. Preflight (informational; we already trust the caller) ------------ migrateEmit phase=preflight status=running local pf pf=$(migratePreflight "$source_host" "$app" "$idx") local pf_rc=$? if (( pf_rc != 0 )); then isError "Preflight failed: $pf" migrateEmit phase=preflight status=failed detail="$pf" return 1 fi migrateEmit phase=preflight status=complete # ---- 2. Pre-migrate backup of destination (default ON) -------------------- if (( MIGRATE_OPT_PRE_BACKUP )); then migratePreBackupDestination "$app" "$idx" else isNotice "Pre-migrate backup skipped (--no-pre-backup)" migrateEmit phase=pre-backup status=skipped reason=user-opt-out app="$app" fi # ---- 3. The actual restore (reuses existing restoreAppStart) -------------- # restoreAppStart already wipes the app folder, restores the snapshot, # re-runs the install-time tag pipeline, and starts the container. The # host_filter arg makes it pull the SOURCE's snapshot, not this host's. migrateEmit phase=restore status=running app="$app" source_host="$source_host" if ! restoreAppStart "$app" "latest" "$idx" "$source_host"; then isError "Restore stage failed for $app" migrateEmit phase=restore status=failed app="$app" return 1 fi migrateEmit phase=restore status=complete app="$app" # ---- 4. URL rewrite (default ON) ------------------------------------------ if (( MIGRATE_OPT_KEEP_URLS )); then isNotice "URL rewrite skipped (--keep-urls)" migrateEmit phase=url-rewrite status=skipped reason=user-opt-out app="$app" else migrateEmit phase=url-rewrite status=running app="$app" migrateApplyUrlRewrite "$app" # restoreAppStart already restarted the app once; if URL rewrite # changed anything, a quick re-deploy of the compose picks them up. # dockerComposeUpdateAndStartApp is idempotent (per the project's # idempotent-setup preference) so re-running is cheap. if declare -f dockerComposeUpdateAndStartApp >/dev/null 2>&1; then dockerComposeUpdateAndStartApp "$app" install >/dev/null 2>&1 || true fi fi local finished_at finished_at=$(date +%s) local duration=$((finished_at - started_at)) isSuccessful "Migrate of $app from $source_host complete in ${duration}s" migrateEmit phase=done status=complete app="$app" duration_seconds="$duration" return 0 } migrateApplySystem() { _migrateParseOpts "$@" local source_host="${_MIGRATE_REMAINING_ARGS[0]}" local idx="${_MIGRATE_REMAINING_ARGS[1]}" if [[ -z "$source_host" ]]; then isError "migrateApplySystem: source_host required" return 1 fi [[ -z "$idx" ]] && idx=$(resticEnabledLocations | head -1) if [[ -z "$idx" ]]; then isError "No backup locations enabled — cannot migrate" return 1 fi isHeader "Migrate every app from host=$source_host → this host" migrateEmit phase=system-start status=running source_host="$source_host" loc_idx="$idx" local apps=() while IFS= read -r a; do [[ -n "$a" ]] && apps+=("$a") done < <(migrateDiscoverApps "$source_host" "$idx") if (( ${#apps[@]} == 0 )); then isError "No apps found for host=$source_host in $(resticLocationName "$idx")" migrateEmit phase=system-start status=failed reason=no-apps return 1 fi isNotice "Apps to migrate: ${apps[*]}" migrateEmit phase=system-plan status=info apps_count="${#apps[@]}" # Forward the same opts to each per-app run. Note we re-pass the parsed # flags as literal --foo so each migrateApplyApp re-parses cleanly. local forwarded=() (( MIGRATE_OPT_PRE_BACKUP == 0 )) && forwarded+=(--no-pre-backup) (( MIGRATE_OPT_KEEP_URLS == 1 )) && forwarded+=(--keep-urls) (( MIGRATE_OPT_JSON_PROGRESS == 1 )) && forwarded+=(--json-progress) local ok=0 fail=0 local a for a in "${apps[@]}"; do if migrateApplyApp "${forwarded[@]}" "$source_host" "$a" "$idx"; then ((ok++)) else ((fail++)) fi done isSuccessful "System migrate finished: $ok ok / $fail failed" migrateEmit phase=system-done status=complete ok="$ok" failed="$fail" (( fail == 0 )) } # ---- Backwards-compat shims --------------------------------------------------- # Pre-existing callers (cli_restore_commands.sh, restoreFirstRunBulk, etc.) # used the old names with the old arg order. Forward to the new functions. migrateApp() { # Old signature: migrateApp [idx] local app="$1"; local source_host="$2"; local idx="$3" migrateApplyApp "$source_host" "$app" "$idx" } migrateSystem() { # Old signature: migrateSystem [idx] migrateApplySystem "$@" }