refactor(migrate)!: rewrite kernel — discover/preflight/apply with JSON progress
Phase 0 of the migration-system refresh. Replaces the 77-line
scripts/migrate/ with a properly-shaped kernel that Phase 1 (WebUI) and
Phase 3 (direct peer SSH) can both build on.
New module layout (6 files):
migrate_progress.sh — migrateEmit JSON-per-line helper; opt-in via
MIGRATE_JSON_PROGRESS=1, writes to fd 3 if open
(clean WebUI streaming channel) else stdout.
migrate_discover.sh — migrateDiscoverHosts / migrateDiscoverApps /
migrateDiscoverAppDetail (JSON {snapshots, latest_*}).
Old migrateDiscoverAppsForHost kept as back-compat.
migrate_preflight.sh — migratePreflight emits one JSON object with
snapshot{id,date}, destination{installed,running,
disk_free_kb}, collision{occurs,default_action,
pre_backup_default}, url_rewrite{default_action,
per_app_opt_out}, warnings[], errors[].
Exit 0 on usable preflight, 1 on hard error.
migrate_url_rewrite.sh— Host-bound CFG_<APP>_* fields (URL/HOST/DOMAIN/
DOMAIN_PREFIX/HOSTNAME/PUBLIC_URL) get rewritten
from the destination's install-template after
restore — so a moved app stops claiming the
source's hostnames. Per-app opt-out via
CFG_<APP>_MIGRATE_URL_REWRITE=false. All other
fields (DB passwords, API keys, prefs) carry
over from the source unchanged.
migrate_pre_backup.sh — migratePreBackupDestination takes a snapshot of
the destination's existing <app> (tagged
pre-migrate=<UTC timestamp>) before the wipe.
Default ON; opt-out with --no-pre-backup. Safety
net for the always-replace collision policy.
migrate_apply.sh — migrateApplyApp / migrateApplySystem. Parses
--no-pre-backup / --keep-urls / --json-progress
opts, runs preflight → pre-backup → restoreAppStart
(existing flow) → URL rewrite → re-deploy compose.
migrateApp / migrateSystem kept as shims so the
old CLI surface still works.
CLI dispatcher (cli_restore_commands.sh + cli_restore_header.sh):
Existing 'restore migrate app/system/discover' calls all still work.
New verbs:
restore migrate list <host> [loc_idx]
restore migrate preflight <host> <app> [loc_idx] ← JSON, for the WebUI
Design choices baked in (per the spec):
- Always-replace collision (no multi-install of an app), safety net is the
on-by-default pre-migrate backup.
- URL rewrite by host-bound suffix list, not per-field allowlist — works
out-of-the-box for new apps without extra config.
- migrateEmit fd-3 contract is what Phase 1's WebUI will stream; falls
back to stdout in interactive CLI so dev/debug just works.
- Transport-agnostic: nothing in this kernel knows whether the backup
location is local/SSH/S3/Connect — engineSnapshotsJson + engineBackupApp
do that, so Connect (the future blind-relay) plugs in as 'just another
location kind' with zero kernel changes.
Smoke-tested: all 13 public functions register; JSON emit produces correct
escaping (quoted strings vs bare numerics) and respects MIGRATE_JSON_PROGRESS.
Signed-off-by: librelad <librelad@digitalangels.vip>
This commit is contained in:
parent
b01968ab0d
commit
32b2840d73
@ -44,17 +44,35 @@ cliHandleRestoreCommands()
|
|||||||
migrate)
|
migrate)
|
||||||
case "$action" in
|
case "$action" in
|
||||||
app)
|
app)
|
||||||
[[ -z "$name" ]] && { isNotice "Usage: restore migrate app <app_name> <source_host> [loc_idx]"; return; }
|
# restore migrate app <app_name> <source_host> [loc_idx] [opts...]
|
||||||
[[ -z "$extra" ]] && { isNotice "Usage: restore migrate app <app_name> <source_host> [loc_idx]"; return; }
|
# opts: --no-pre-backup, --keep-urls, --json-progress
|
||||||
migrateApp "$name" "$extra" "$extra2"
|
[[ -z "$name" ]] && { isNotice "Usage: restore migrate app <app_name> <source_host> [loc_idx] [opts]"; return; }
|
||||||
|
[[ -z "$extra" ]] && { isNotice "Usage: restore migrate app <app_name> <source_host> [loc_idx] [opts]"; return; }
|
||||||
|
shift 4 # consume restore_type, action, name, extra
|
||||||
|
local app_idx="$1"; shift 2>/dev/null || true
|
||||||
|
migrateApplyApp "$extra" "$name" "$app_idx" "$@"
|
||||||
;;
|
;;
|
||||||
system)
|
system)
|
||||||
[[ -z "$name" ]] && { isNotice "Usage: restore migrate system <source_host> [loc_idx]"; return; }
|
# restore migrate system <source_host> [loc_idx] [opts...]
|
||||||
migrateSystem "$name" "$extra"
|
[[ -z "$name" ]] && { isNotice "Usage: restore migrate system <source_host> [loc_idx] [opts]"; return; }
|
||||||
|
shift 3
|
||||||
|
migrateApplySystem "$name" "$@"
|
||||||
;;
|
;;
|
||||||
discover)
|
discover)
|
||||||
|
# restore migrate discover [loc_idx]
|
||||||
migrateDiscoverHosts "$name"
|
migrateDiscoverHosts "$name"
|
||||||
;;
|
;;
|
||||||
|
list)
|
||||||
|
# restore migrate list <source_host> [loc_idx]
|
||||||
|
[[ -z "$name" ]] && { isNotice "Usage: restore migrate list <source_host> [loc_idx]"; return; }
|
||||||
|
migrateDiscoverApps "$name" "$extra"
|
||||||
|
;;
|
||||||
|
preflight)
|
||||||
|
# restore migrate preflight <source_host> <app> [loc_idx]
|
||||||
|
# Emits a single JSON object describing the planned migrate.
|
||||||
|
[[ -z "$name" || -z "$extra" ]] && { isNotice "Usage: restore migrate preflight <source_host> <app> [loc_idx]"; return; }
|
||||||
|
migratePreflight "$name" "$extra" "$extra2"
|
||||||
|
;;
|
||||||
*)
|
*)
|
||||||
isNotice "Invalid migrate action: $action"
|
isNotice "Invalid migrate action: $action"
|
||||||
cliShowRestoreHelp
|
cliShowRestoreHelp
|
||||||
|
|||||||
@ -13,15 +13,24 @@ cliShowRestoreHelp()
|
|||||||
echo " Restore the latest system-config snapshot into a staging dir"
|
echo " Restore the latest system-config snapshot into a staging dir"
|
||||||
echo " (review-then-copy; never overwrites live config). Default: first location."
|
echo " (review-then-copy; never overwrites live config). Default: first location."
|
||||||
echo ""
|
echo ""
|
||||||
echo "restore migrate app <app_name> <source_host> [loc_idx]"
|
|
||||||
echo " Restore one app's backup taken on another host (cross-host migrate)."
|
|
||||||
echo ""
|
|
||||||
echo "restore migrate system <source_host> [loc_idx]"
|
|
||||||
echo " Restore every app backed up on a source host."
|
|
||||||
echo ""
|
|
||||||
echo "restore migrate discover [loc_idx]"
|
echo "restore migrate discover [loc_idx]"
|
||||||
echo " List the source hosts that have backups in a location."
|
echo " List the source hosts that have backups in a location."
|
||||||
echo ""
|
echo ""
|
||||||
|
echo "restore migrate list <source_host> [loc_idx]"
|
||||||
|
echo " List the apps that <source_host> has backed up in a location."
|
||||||
|
echo ""
|
||||||
|
echo "restore migrate preflight <source_host> <app> [loc_idx]"
|
||||||
|
echo " Probe what a migrate would do (snapshot id, disk, collision,"
|
||||||
|
echo " URL-rewrite decision). Emits a single JSON object — used by the"
|
||||||
|
echo " WebUI before showing the confirm modal."
|
||||||
|
echo ""
|
||||||
|
echo "restore migrate app <app_name> <source_host> [loc_idx] [opts]"
|
||||||
|
echo " Restore one app's backup taken on another host (cross-host migrate)."
|
||||||
|
echo " Opts: --no-pre-backup --keep-urls --json-progress"
|
||||||
|
echo ""
|
||||||
|
echo "restore migrate system <source_host> [loc_idx] [opts]"
|
||||||
|
echo " Restore every app backed up on a source host. Same opts as above."
|
||||||
|
echo ""
|
||||||
echo "restore first-run discover [loc_idx]"
|
echo "restore first-run discover [loc_idx]"
|
||||||
echo " Discover LibrePortal backups in a location (fresh-install flow)."
|
echo " Discover LibrePortal backups in a location (fresh-install flow)."
|
||||||
echo ""
|
echo ""
|
||||||
|
|||||||
@ -1,18 +0,0 @@
|
|||||||
#!/bin/bash
|
|
||||||
|
|
||||||
migrateApp()
|
|
||||||
{
|
|
||||||
local app_name="$1"
|
|
||||||
local source_host="$2"
|
|
||||||
local idx="$3"
|
|
||||||
|
|
||||||
if [[ -z "$app_name" || -z "$source_host" ]]; then
|
|
||||||
isError "migrateApp requires app_name and source_host"
|
|
||||||
return 1
|
|
||||||
fi
|
|
||||||
|
|
||||||
isHeader "Migrating $app_name from host=$source_host"
|
|
||||||
|
|
||||||
[[ -z "$idx" ]] && idx=$(resticEnabledLocations | head -1)
|
|
||||||
restoreAppStart "$app_name" "latest" "$idx" "$source_host"
|
|
||||||
}
|
|
||||||
189
scripts/migrate/migrate_apply.sh
Normal file
189
scripts/migrate/migrate_apply.sh
Normal file
@ -0,0 +1,189 @@
|
|||||||
|
#!/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 <source_host> <app> [loc_idx] [opts...]
|
||||||
|
# migrateApplySystem <source_host> [loc_idx] [opts...]
|
||||||
|
#
|
||||||
|
# Opts (any order, all defaults match the WebUI defaults):
|
||||||
|
# --no-pre-backup Skip the safety snapshot of destination's <app>
|
||||||
|
# --keep-urls Don't rewrite host-bound CFG_<APP>_* 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 <app_name> <source_host> [idx]
|
||||||
|
local app="$1"; local source_host="$2"; local idx="$3"
|
||||||
|
migrateApplyApp "$source_host" "$app" "$idx"
|
||||||
|
}
|
||||||
|
|
||||||
|
migrateSystem()
|
||||||
|
{
|
||||||
|
# Old signature: migrateSystem <source_host> [idx]
|
||||||
|
migrateApplySystem "$@"
|
||||||
|
}
|
||||||
@ -1,19 +1,87 @@
|
|||||||
#!/bin/bash
|
#!/bin/bash
|
||||||
|
|
||||||
migrateDiscoverHosts()
|
# Discovery helpers for cross-host migrate. All read-only — they query a
|
||||||
|
# backup location's snapshot index, never touch live state.
|
||||||
|
#
|
||||||
|
# Three views, increasing specificity:
|
||||||
|
# migrateDiscoverHosts — which hosts have snapshots in this location?
|
||||||
|
# migrateDiscoverApps — which apps does <host> have in this location?
|
||||||
|
# migrateDiscoverAppDetail — newest snapshot + count for one host/app.
|
||||||
|
|
||||||
|
# Resolve to the first enabled location when idx is unset. Returns "" if
|
||||||
|
# nothing is enabled — callers should treat that as "nowhere to discover from."
|
||||||
|
_migrateResolveLocation()
|
||||||
{
|
{
|
||||||
local idx="$1"
|
local idx="$1"
|
||||||
[[ -z "$idx" ]] && idx=$(resticEnabledLocations | head -1)
|
if [[ -z "$idx" ]]; then
|
||||||
[[ -z "$idx" ]] && return 1
|
idx=$(resticEnabledLocations | head -1)
|
||||||
|
fi
|
||||||
engineSnapshotsJson "$idx" | grep -o '"hostname":"[^"]*"' | sort -u | cut -d'"' -f4
|
echo "$idx"
|
||||||
}
|
}
|
||||||
|
|
||||||
|
# List hostnames with at least one snapshot in the given location. One per line.
|
||||||
|
migrateDiscoverHosts()
|
||||||
|
{
|
||||||
|
local idx
|
||||||
|
idx=$(_migrateResolveLocation "$1")
|
||||||
|
[[ -z "$idx" ]] && return 1
|
||||||
|
|
||||||
|
engineSnapshotsJson "$idx" 2>/dev/null \
|
||||||
|
| grep -o '"hostname":"[^"]*"' \
|
||||||
|
| sort -u \
|
||||||
|
| cut -d'"' -f4
|
||||||
|
}
|
||||||
|
|
||||||
|
# List apps backed up by <host> in this location. One slug per line.
|
||||||
|
migrateDiscoverApps()
|
||||||
|
{
|
||||||
|
local idx
|
||||||
|
idx=$(_migrateResolveLocation "$2")
|
||||||
|
local host="$1"
|
||||||
|
[[ -z "$idx" || -z "$host" ]] && return 1
|
||||||
|
|
||||||
|
engineSnapshotsJson "$idx" "" "$host" 2>/dev/null \
|
||||||
|
| grep -o '"app=[^"]*"' \
|
||||||
|
| sort -u \
|
||||||
|
| sed 's/"app=\(.*\)"/\1/'
|
||||||
|
}
|
||||||
|
|
||||||
|
# JSON detail for one host+app: snapshot count + latest id/date.
|
||||||
|
# Output: {"host":"…","app":"…","snapshots":N,"latest_id":"…","latest_date":"…"}
|
||||||
|
# Missing snapshots → snapshots=0, latest_*=null.
|
||||||
|
migrateDiscoverAppDetail()
|
||||||
|
{
|
||||||
|
local host="$1"
|
||||||
|
local app="$2"
|
||||||
|
local idx
|
||||||
|
idx=$(_migrateResolveLocation "$3")
|
||||||
|
[[ -z "$idx" || -z "$host" || -z "$app" ]] && return 1
|
||||||
|
|
||||||
|
local json
|
||||||
|
json=$(engineSnapshotsJson "$idx" "$app" "$host" 2>/dev/null)
|
||||||
|
|
||||||
|
local count
|
||||||
|
count=$(printf '%s' "$json" | grep -oc '"short_id":"' || echo 0)
|
||||||
|
|
||||||
|
local latest_id="null"
|
||||||
|
local latest_date="null"
|
||||||
|
if (( count > 0 )); then
|
||||||
|
# Restic prints snapshots in creation order — last one is newest.
|
||||||
|
latest_id="\"$(printf '%s' "$json" \
|
||||||
|
| grep -o '"short_id":"[^"]*"' | tail -1 | cut -d'"' -f4)\""
|
||||||
|
latest_date="\"$(printf '%s' "$json" \
|
||||||
|
| grep -o '"time":"[^"]*"' | tail -1 | cut -d'"' -f4)\""
|
||||||
|
fi
|
||||||
|
|
||||||
|
printf '{"host":"%s","app":"%s","loc_idx":%s,"snapshots":%s,"latest_id":%s,"latest_date":%s}\n' \
|
||||||
|
"$host" "$app" "$idx" "$count" "$latest_id" "$latest_date"
|
||||||
|
}
|
||||||
|
|
||||||
|
# Back-compat shim — older callers (and the existing CLI) used this name with
|
||||||
|
# (idx, host) arg order. New code should call migrateDiscoverApps directly.
|
||||||
migrateDiscoverAppsForHost()
|
migrateDiscoverAppsForHost()
|
||||||
{
|
{
|
||||||
local idx="$1"
|
local idx="$1"
|
||||||
local host="$2"
|
local host="$2"
|
||||||
[[ -z "$idx" ]] && idx=$(resticEnabledLocations | head -1)
|
migrateDiscoverApps "$host" "$idx"
|
||||||
|
|
||||||
engineSnapshotsJson "$idx" "" "$host" | grep -o '"app=[^"]*"' | sort -u | sed 's/"app=\(.*\)"/\1/'
|
|
||||||
}
|
}
|
||||||
|
|||||||
57
scripts/migrate/migrate_pre_backup.sh
Normal file
57
scripts/migrate/migrate_pre_backup.sh
Normal file
@ -0,0 +1,57 @@
|
|||||||
|
#!/bin/bash
|
||||||
|
|
||||||
|
# Pre-migrate backup. Before the destination app gets wiped and replaced with
|
||||||
|
# the source's snapshot, take a fresh local backup of the destination's app —
|
||||||
|
# tagged with `pre-migrate=<timestamp>` so it's easy to find for rollback if
|
||||||
|
# the migrate misbehaves. Best-effort: emits a notice and continues if no
|
||||||
|
# backup location is available (the caller already warned the user via
|
||||||
|
# preflight).
|
||||||
|
#
|
||||||
|
# Args: <app_name> <loc_idx> (loc_idx defaults to first enabled)
|
||||||
|
# Returns: 0 always (a missing pre-backup must never block the migrate; the
|
||||||
|
# user already saw and confirmed the preflight that warned about it).
|
||||||
|
|
||||||
|
migratePreBackupDestination()
|
||||||
|
{
|
||||||
|
local app="$1"
|
||||||
|
local idx="$2"
|
||||||
|
|
||||||
|
if [[ -z "$app" ]]; then
|
||||||
|
isError "migratePreBackupDestination: app required"
|
||||||
|
migrateEmit phase=pre-backup status=skipped reason=no-app
|
||||||
|
return 0
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Only meaningful if the destination actually has this app installed.
|
||||||
|
if [[ ! -d "$containers_dir$app" ]]; then
|
||||||
|
isNotice "No existing $app on destination — pre-migrate backup skipped"
|
||||||
|
migrateEmit phase=pre-backup status=skipped reason=no-existing-app app="$app"
|
||||||
|
return 0
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [[ -z "$idx" ]]; then
|
||||||
|
idx=$(resticEnabledLocations | head -1)
|
||||||
|
fi
|
||||||
|
if [[ -z "$idx" ]]; then
|
||||||
|
isNotice "No backup locations enabled — pre-migrate backup skipped"
|
||||||
|
migrateEmit phase=pre-backup status=skipped reason=no-backup-location app="$app"
|
||||||
|
return 0
|
||||||
|
fi
|
||||||
|
|
||||||
|
local stamp
|
||||||
|
stamp=$(date -u +%Y%m%dT%H%M%SZ)
|
||||||
|
isNotice "Pre-migrate backup of destination $app → $(resticLocationName "$idx") (tag pre-migrate=$stamp)"
|
||||||
|
migrateEmit phase=pre-backup status=running app="$app" loc_idx="$idx" stamp="$stamp"
|
||||||
|
|
||||||
|
# engineBackupApp takes (idx, app, [extra_tags...]). The pre-migrate tag
|
||||||
|
# makes the snapshot trivially findable later: restic snapshots --tag pre-migrate
|
||||||
|
if engineBackupApp "$idx" "$app" "pre-migrate=$stamp"; then
|
||||||
|
isSuccessful "Pre-migrate backup of $app complete"
|
||||||
|
migrateEmit phase=pre-backup status=complete app="$app" stamp="$stamp"
|
||||||
|
else
|
||||||
|
isNotice "Pre-migrate backup of $app FAILED — continuing migrate anyway (you confirmed)"
|
||||||
|
migrateEmit phase=pre-backup status=failed app="$app" stamp="$stamp"
|
||||||
|
fi
|
||||||
|
|
||||||
|
return 0
|
||||||
|
}
|
||||||
147
scripts/migrate/migrate_preflight.sh
Normal file
147
scripts/migrate/migrate_preflight.sh
Normal file
@ -0,0 +1,147 @@
|
|||||||
|
#!/bin/bash
|
||||||
|
|
||||||
|
# Preflight for a cross-host migrate. Read-only: probes the destination, asks
|
||||||
|
# the backup location what it knows about the source/app, and produces a JSON
|
||||||
|
# object the WebUI (or a thoughtful CLI user) consumes before pressing "go."
|
||||||
|
#
|
||||||
|
# Args: <source_host> <app> [loc_idx]
|
||||||
|
#
|
||||||
|
# Output (single JSON object, one line):
|
||||||
|
# {
|
||||||
|
# "source_host": "homelab",
|
||||||
|
# "app": "linkding",
|
||||||
|
# "loc_idx": 1,
|
||||||
|
# "snapshot": {"id":"abc12345","date":"2026-05-26T10:30:00Z"} | null,
|
||||||
|
# "destination": {
|
||||||
|
# "app_installed": true,
|
||||||
|
# "app_running": true,
|
||||||
|
# "disk_free_kb": 50000000
|
||||||
|
# },
|
||||||
|
# "collision": {
|
||||||
|
# "occurs": true,
|
||||||
|
# "default_action": "replace",
|
||||||
|
# "pre_backup_default": true
|
||||||
|
# },
|
||||||
|
# "url_rewrite": {
|
||||||
|
# "default_action": "rewrite" | "keep",
|
||||||
|
# "per_app_opt_out": false
|
||||||
|
# },
|
||||||
|
# "warnings": [...],
|
||||||
|
# "errors": [...]
|
||||||
|
# }
|
||||||
|
#
|
||||||
|
# Exit codes: 0 on a usable preflight (even if warnings); 1 if the migrate
|
||||||
|
# can't proceed at all (no location, no snapshot, etc.) — errors[] is filled.
|
||||||
|
|
||||||
|
# Append a string to a JSON-array variable held by name. Used by migratePreflight
|
||||||
|
# to grow its warnings[]/errors[] without dragging in jq. Defined at file scope
|
||||||
|
# because bash doesn't allow `local funcname() {}` nested inside another function.
|
||||||
|
_migratePreflightAppend()
|
||||||
|
{
|
||||||
|
local __varname="$1"
|
||||||
|
local msg="$2"
|
||||||
|
local escaped="${msg//\\/\\\\}"
|
||||||
|
escaped="${escaped//\"/\\\"}"
|
||||||
|
local current="${!__varname}"
|
||||||
|
if [[ "$current" == "[]" ]]; then
|
||||||
|
printf -v "$__varname" '[%s]' "\"$escaped\""
|
||||||
|
else
|
||||||
|
printf -v "$__varname" '%s,%s]' "${current%]}" "\"$escaped\""
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
|
migratePreflight()
|
||||||
|
{
|
||||||
|
local source_host="$1"
|
||||||
|
local app="$2"
|
||||||
|
local idx="$3"
|
||||||
|
|
||||||
|
local warnings='[]'
|
||||||
|
local errors='[]'
|
||||||
|
|
||||||
|
if [[ -z "$source_host" || -z "$app" ]]; then
|
||||||
|
_migratePreflightAppend errors "source_host and app required"
|
||||||
|
printf '{"source_host":null,"app":null,"loc_idx":null,"snapshot":null,"destination":null,"collision":null,"url_rewrite":null,"warnings":%s,"errors":%s}\n' \
|
||||||
|
"$warnings" "$errors"
|
||||||
|
return 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [[ -z "$idx" ]]; then
|
||||||
|
idx=$(resticEnabledLocations | head -1)
|
||||||
|
fi
|
||||||
|
if [[ -z "$idx" ]]; then
|
||||||
|
_migratePreflightAppend errors "No backup locations enabled — nowhere to pull from"
|
||||||
|
printf '{"source_host":"%s","app":"%s","loc_idx":null,"snapshot":null,"destination":null,"collision":null,"url_rewrite":null,"warnings":%s,"errors":%s}\n' \
|
||||||
|
"$source_host" "$app" "$warnings" "$errors"
|
||||||
|
return 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
# ---- Snapshot probe -------------------------------------------------------
|
||||||
|
local snap_id="" snap_date=""
|
||||||
|
local snap_json
|
||||||
|
snap_json=$(engineSnapshotsJson "$idx" "$app" "$source_host" 2>/dev/null)
|
||||||
|
if [[ -n "$snap_json" && "$snap_json" != "[]" ]]; then
|
||||||
|
snap_id=$(printf '%s' "$snap_json" | grep -o '"short_id":"[^"]*"' | tail -1 | cut -d'"' -f4)
|
||||||
|
snap_date=$(printf '%s' "$snap_json" | grep -o '"time":"[^"]*"' | tail -1 | cut -d'"' -f4)
|
||||||
|
fi
|
||||||
|
local snapshot_json
|
||||||
|
if [[ -n "$snap_id" ]]; then
|
||||||
|
snapshot_json="{\"id\":\"$snap_id\",\"date\":\"$snap_date\"}"
|
||||||
|
else
|
||||||
|
snapshot_json="null"
|
||||||
|
_migratePreflightAppend errors "No snapshots found for app=$app on host=$source_host in $(resticLocationName "$idx")"
|
||||||
|
fi
|
||||||
|
|
||||||
|
# ---- Destination probe ----------------------------------------------------
|
||||||
|
local app_installed=false
|
||||||
|
local app_running=false
|
||||||
|
if [[ -d "$containers_dir$app" ]]; then
|
||||||
|
app_installed=true
|
||||||
|
if dockerCommandRun "docker ps --filter name=^${app}\$ --format '{{.Names}}' 2>/dev/null" | grep -q "^$app\$"; then
|
||||||
|
app_running=true
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
|
||||||
|
# df -P on the containers root, fourth col = free 1K-blocks. Best-effort.
|
||||||
|
local disk_free_kb
|
||||||
|
disk_free_kb=$(df -Pk "$containers_dir" 2>/dev/null | awk 'NR==2 {print $4}')
|
||||||
|
[[ -z "$disk_free_kb" ]] && disk_free_kb=0
|
||||||
|
|
||||||
|
local dest_json
|
||||||
|
dest_json="{\"app_installed\":$app_installed,\"app_running\":$app_running,\"disk_free_kb\":$disk_free_kb}"
|
||||||
|
|
||||||
|
# ---- Collision policy (we always replace; pre-backup defaults ON) ---------
|
||||||
|
local pre_backup_default=true
|
||||||
|
[[ "$app_installed" == "false" ]] && pre_backup_default=false # nothing to back up
|
||||||
|
local collision_json
|
||||||
|
collision_json="{\"occurs\":$app_installed,\"default_action\":\"replace\",\"pre_backup_default\":$pre_backup_default}"
|
||||||
|
|
||||||
|
# ---- URL rewrite policy ---------------------------------------------------
|
||||||
|
local url_default="rewrite"
|
||||||
|
local opt_out=false
|
||||||
|
if [[ "$(migrateUrlRewriteEnabled "$app")" == "false" ]]; then
|
||||||
|
url_default="keep"
|
||||||
|
opt_out=true
|
||||||
|
fi
|
||||||
|
local url_json
|
||||||
|
url_json="{\"default_action\":\"$url_default\",\"per_app_opt_out\":$opt_out}"
|
||||||
|
|
||||||
|
# ---- Soft warnings --------------------------------------------------------
|
||||||
|
# Disk free below 2× the destination's existing app folder is concerning.
|
||||||
|
if [[ "$app_installed" == "true" ]]; then
|
||||||
|
local existing_kb
|
||||||
|
existing_kb=$(du -sk "$containers_dir$app" 2>/dev/null | awk '{print $1}')
|
||||||
|
if [[ -n "$existing_kb" && "$disk_free_kb" -lt $((existing_kb * 2)) ]]; then
|
||||||
|
_migratePreflightAppend warnings "Disk free ($((disk_free_kb / 1024)) MB) is less than 2× existing $app size ($((existing_kb / 1024)) MB)"
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
if [[ "$app_running" == "true" ]]; then
|
||||||
|
_migratePreflightAppend warnings "$app is currently running on this host; migrate will stop it"
|
||||||
|
fi
|
||||||
|
|
||||||
|
printf '{"source_host":"%s","app":"%s","loc_idx":%s,"snapshot":%s,"destination":%s,"collision":%s,"url_rewrite":%s,"warnings":%s,"errors":%s}\n' \
|
||||||
|
"$source_host" "$app" "$idx" "$snapshot_json" "$dest_json" "$collision_json" "$url_json" \
|
||||||
|
"$warnings" "$errors"
|
||||||
|
|
||||||
|
[[ "$errors" == "[]" ]] && return 0 || return 1
|
||||||
|
}
|
||||||
52
scripts/migrate/migrate_progress.sh
Normal file
52
scripts/migrate/migrate_progress.sh
Normal file
@ -0,0 +1,52 @@
|
|||||||
|
#!/bin/bash
|
||||||
|
|
||||||
|
# Structured progress emit for migrate flows. Phase 1's WebUI streams these
|
||||||
|
# JSON-per-line records and renders them. When MIGRATE_JSON_PROGRESS=1 the
|
||||||
|
# helper writes machine-readable JSON to fd 3 (or stdout if fd 3 isn't open);
|
||||||
|
# otherwise it's a no-op so the human CLI output stays clean.
|
||||||
|
#
|
||||||
|
# Usage:
|
||||||
|
# migrateEmit phase=preflight status=running
|
||||||
|
# migrateEmit phase=restore status=running pct=50
|
||||||
|
# migrateEmit phase=done status=complete duration_seconds=42
|
||||||
|
#
|
||||||
|
# Keys with spaces or special chars should be passed pre-quoted (a=foo\ bar)
|
||||||
|
# or via the explicit setter form (rare — most callers use the simple kv form).
|
||||||
|
migrateEmit()
|
||||||
|
{
|
||||||
|
[[ "$MIGRATE_JSON_PROGRESS" != "1" ]] && return 0
|
||||||
|
|
||||||
|
local pair
|
||||||
|
local first=1
|
||||||
|
local out='{'
|
||||||
|
for pair in "$@"; do
|
||||||
|
local k="${pair%%=*}"
|
||||||
|
local v="${pair#*=}"
|
||||||
|
# Numeric values stay bare; everything else gets quoted + escaped.
|
||||||
|
local rendered
|
||||||
|
if [[ "$v" =~ ^-?[0-9]+(\.[0-9]+)?$ ]]; then
|
||||||
|
rendered="$v"
|
||||||
|
else
|
||||||
|
local escaped="${v//\\/\\\\}"
|
||||||
|
escaped="${escaped//\"/\\\"}"
|
||||||
|
escaped="${escaped//$'\n'/\\n}"
|
||||||
|
escaped="${escaped//$'\t'/\\t}"
|
||||||
|
rendered="\"$escaped\""
|
||||||
|
fi
|
||||||
|
if (( first )); then
|
||||||
|
out+="\"$k\":$rendered"
|
||||||
|
first=0
|
||||||
|
else
|
||||||
|
out+=",\"$k\":$rendered"
|
||||||
|
fi
|
||||||
|
done
|
||||||
|
out+='}'
|
||||||
|
|
||||||
|
# fd 3 is the structured channel — Phase 1 opens it. If it's not open
|
||||||
|
# (interactive CLI), fall back to stdout so dev/debug runs still see it.
|
||||||
|
if { true >&3; } 2>/dev/null; then
|
||||||
|
printf '%s\n' "$out" >&3
|
||||||
|
else
|
||||||
|
printf '%s\n' "$out"
|
||||||
|
fi
|
||||||
|
}
|
||||||
@ -1,40 +0,0 @@
|
|||||||
#!/bin/bash
|
|
||||||
|
|
||||||
migrateSystem()
|
|
||||||
{
|
|
||||||
local source_host="$1"
|
|
||||||
local idx="$2"
|
|
||||||
|
|
||||||
if [[ -z "$source_host" ]]; then
|
|
||||||
isError "migrateSystem requires source_host"
|
|
||||||
return 1
|
|
||||||
fi
|
|
||||||
|
|
||||||
[[ -z "$idx" ]] && idx=$(resticEnabledLocations | head -1)
|
|
||||||
|
|
||||||
isHeader "Migrating entire system from host=$source_host"
|
|
||||||
|
|
||||||
local apps_json
|
|
||||||
apps_json=$(engineSnapshotsJson "$idx" "" "$source_host")
|
|
||||||
if [[ -z "$apps_json" || "$apps_json" == "[]" ]]; then
|
|
||||||
isError "No snapshots found in $(resticLocationName "$idx") for host=$source_host"
|
|
||||||
return 1
|
|
||||||
fi
|
|
||||||
|
|
||||||
local apps=()
|
|
||||||
while IFS= read -r app; do
|
|
||||||
[[ -n "$app" ]] && apps+=("$app")
|
|
||||||
done < <(echo "$apps_json" | grep -o '"app=[^"]*"' | sort -u | sed 's/"app=\(.*\)"/\1/')
|
|
||||||
|
|
||||||
if [[ ${#apps[@]} -eq 0 ]]; then
|
|
||||||
isError "Could not parse app list from snapshots"
|
|
||||||
return 1
|
|
||||||
fi
|
|
||||||
|
|
||||||
isNotice "Apps to migrate: ${apps[*]}"
|
|
||||||
for app in "${apps[@]}"; do
|
|
||||||
migrateApp "$app" "$source_host" "$idx"
|
|
||||||
done
|
|
||||||
|
|
||||||
isSuccessful "System migration complete — ${#apps[@]} apps"
|
|
||||||
}
|
|
||||||
102
scripts/migrate/migrate_url_rewrite.sh
Normal file
102
scripts/migrate/migrate_url_rewrite.sh
Normal file
@ -0,0 +1,102 @@
|
|||||||
|
#!/bin/bash
|
||||||
|
|
||||||
|
# Cross-host migrate inherits the source's CFG_<APP>_* values via the restored
|
||||||
|
# config file. Most of them (DB passwords, API keys, user preferences) are
|
||||||
|
# data and must survive the move. A small set are host-bound — they describe
|
||||||
|
# where the app lives — and would 404 or break TLS if we kept them pointing
|
||||||
|
# at the source. This module identifies and rewrites those.
|
||||||
|
#
|
||||||
|
# Host-bound fields are detected by suffix (URL, HOST, DOMAIN, DOMAIN_PREFIX),
|
||||||
|
# and the rewrite source-of-truth is the destination's install-template
|
||||||
|
# config (containers/<app>/<app>.config under the install tree) — that's what
|
||||||
|
# a fresh install on this box would produce.
|
||||||
|
#
|
||||||
|
# Per-app opt-out: set CFG_<APP>_MIGRATE_URL_REWRITE="false" in the app's
|
||||||
|
# install-template config to skip the rewrite (e.g. for apps with hardcoded
|
||||||
|
# URLs that mustn't change).
|
||||||
|
|
||||||
|
# Field-name suffixes treated as host-bound. Compared after stripping the
|
||||||
|
# CFG_<APP>_ prefix.
|
||||||
|
_MIGRATE_HOST_BOUND_SUFFIXES=(URL HOST DOMAIN DOMAIN_PREFIX HOSTNAME PUBLIC_URL)
|
||||||
|
|
||||||
|
# Echo "true" or "false" — should we rewrite host-bound URLs for this app?
|
||||||
|
# Defaults true unless the app explicitly opts out.
|
||||||
|
migrateUrlRewriteEnabled()
|
||||||
|
{
|
||||||
|
local app="$1"
|
||||||
|
local template_config="$install_containers_dir$app/$app.config"
|
||||||
|
[[ ! -f "$template_config" ]] && { echo "true"; return; }
|
||||||
|
|
||||||
|
local opt_key="CFG_${app^^}_MIGRATE_URL_REWRITE"
|
||||||
|
local val
|
||||||
|
val=$(grep -E "^${opt_key}=" "$template_config" 2>/dev/null \
|
||||||
|
| head -1 | cut -d'=' -f2- | tr -d '"' | tr -d "'")
|
||||||
|
if [[ "$val" == "false" || "$val" == "no" || "$val" == "0" ]]; then
|
||||||
|
echo "false"
|
||||||
|
else
|
||||||
|
echo "true"
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
|
# Read the destination's install template, find host-bound CFG_<APP>_* keys,
|
||||||
|
# and overwrite the same keys in the deployed (just-restored) config with the
|
||||||
|
# template's values. Anything not in the host-bound list is left alone — the
|
||||||
|
# source's data/preferences survive.
|
||||||
|
#
|
||||||
|
# Args: <app_name>
|
||||||
|
# Returns: count of fields rewritten (emitted as `migrate.url_rewrite.fields`).
|
||||||
|
migrateApplyUrlRewrite()
|
||||||
|
{
|
||||||
|
local app="$1"
|
||||||
|
[[ -z "$app" ]] && { isError "migrateApplyUrlRewrite: app required"; return 1; }
|
||||||
|
|
||||||
|
if [[ "$(migrateUrlRewriteEnabled "$app")" != "true" ]]; then
|
||||||
|
isNotice "URL rewrite disabled by app config — keeping source URLs for $app"
|
||||||
|
migrateEmit phase=url-rewrite status=skipped reason=app-opt-out app="$app"
|
||||||
|
return 0
|
||||||
|
fi
|
||||||
|
|
||||||
|
local template="$install_containers_dir$app/$app.config"
|
||||||
|
local deployed="$containers_dir$app/$app.config"
|
||||||
|
|
||||||
|
if [[ ! -f "$template" || ! -f "$deployed" ]]; then
|
||||||
|
isNotice "URL rewrite: missing template or deployed config for $app — skipping"
|
||||||
|
migrateEmit phase=url-rewrite status=skipped reason=missing-config app="$app"
|
||||||
|
return 0
|
||||||
|
fi
|
||||||
|
|
||||||
|
local rewritten=0
|
||||||
|
local key value bare_suffix
|
||||||
|
while IFS='=' read -r key value; do
|
||||||
|
[[ -z "$key" ]] && continue
|
||||||
|
[[ "$key" =~ ^# ]] && continue
|
||||||
|
[[ ! "$key" =~ ^CFG_ ]] && continue
|
||||||
|
|
||||||
|
bare_suffix="${key##*_}"
|
||||||
|
# Also catch the two-word _DOMAIN_PREFIX / _PUBLIC_URL cases.
|
||||||
|
local two_word="${key%_*}"
|
||||||
|
two_word="${two_word##*_}_${bare_suffix}"
|
||||||
|
|
||||||
|
local is_host_bound=0
|
||||||
|
local suffix
|
||||||
|
for suffix in "${_MIGRATE_HOST_BOUND_SUFFIXES[@]}"; do
|
||||||
|
if [[ "$bare_suffix" == "$suffix" || "$two_word" == "$suffix" ]]; then
|
||||||
|
is_host_bound=1
|
||||||
|
break
|
||||||
|
fi
|
||||||
|
done
|
||||||
|
(( is_host_bound )) || continue
|
||||||
|
|
||||||
|
# Strip surrounding quotes; keep whatever the template specifies, even
|
||||||
|
# if empty (a deliberately-blank URL is a valid state).
|
||||||
|
value="${value%\"}"; value="${value#\"}"
|
||||||
|
value="${value%\'}"; value="${value#\'}"
|
||||||
|
|
||||||
|
updateConfigOption "$key" "$value" "$deployed" >/dev/null 2>&1
|
||||||
|
((rewritten++))
|
||||||
|
migrateEmit phase=url-rewrite status=field app="$app" key="$key" new_value="$value"
|
||||||
|
done < <(grep -E '^CFG_' "$template" 2>/dev/null)
|
||||||
|
|
||||||
|
isSuccessful "URL rewrite: $rewritten host-bound field(s) repointed to this host for $app"
|
||||||
|
migrateEmit phase=url-rewrite status=complete app="$app" fields="$rewritten"
|
||||||
|
}
|
||||||
@ -4,8 +4,11 @@
|
|||||||
# Do not edit manually - run './scripts/source/files/generate_arrays.sh run' to regenerate
|
# Do not edit manually - run './scripts/source/files/generate_arrays.sh run' to regenerate
|
||||||
|
|
||||||
migrate_scripts=(
|
migrate_scripts=(
|
||||||
"migrate/migrate_app.sh"
|
"migrate/migrate_apply.sh"
|
||||||
"migrate/migrate_discover.sh"
|
"migrate/migrate_discover.sh"
|
||||||
"migrate/migrate_system.sh"
|
"migrate/migrate_pre_backup.sh"
|
||||||
|
"migrate/migrate_preflight.sh"
|
||||||
|
"migrate/migrate_progress.sh"
|
||||||
|
"migrate/migrate_url_rewrite.sh"
|
||||||
|
|
||||||
)
|
)
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user