Files

237 lines
9.5 KiB
Bash
Executable File

#!/bin/sh
#
# Shared helpers for relocate-stack-paths (SQLite + PostgreSQL variants source this).
# Contains the pure re-anchor rule and the interactive/dry-run presentation, so the
# only difference between the two DB variants is how they read/write the database.
#
# The DB variant must define, before sourcing this file:
# db_list_stack_paths -> prints one row per (compose|env) path as:
# <id>\t<field>\t<label>\t<stored_path>
# (field = "compose" or "env"; label = human name)
# db_list_external_paths -> prints one line per external_stack_paths entry:
# <index>\t<stored_path>
# db_update_stack_path <id> <field> <new_path> -> UPDATE that one column
# db_update_external_path <index> <new_path> -> replace that array entry
#
# It reads: OLD_DIR, NEW_DIR, MODE (dry|apply), APPLY_ALL (0|1).
# --- pure re-anchor rule: rewrite a stored path from the old DATA_DIR prefix to the
# new one, matching only on a full path segment (so /app/data never matches
# /app/database), with a single trailing slash stripped from each dir. ---
# strip a single trailing slash
_strip_trailing_slash() { printf '%s' "$1" | sed 's:/*$::'; }
# rewrite_under_data_dir <stored> <oldDir> <newDir>
# Prints the rewritten path, or nothing when <stored> is not under <oldDir> (segment
# boundary, so /app/data never matches /app/database) or the rewrite is a no-op.
rewrite_under_data_dir() {
stored="$1"; old="$(_strip_trailing_slash "$2")"; new="$(_strip_trailing_slash "$3")"
[ -z "$stored" ] && return 0
[ -z "$old" ] && return 0
[ "$old" = "$new" ] && return 0
if [ "$stored" = "$old" ]; then printf '%s' "$new"; return 0; fi
case "$stored" in
"$old"/*) printf '%s%s' "$new" "${stored#"$old"}" ;;
*) return 0 ;; # not under old dir - leave it
esac
}
# classify_path <stored> : echoes "STATUS\tNEWPATH\tREASON"
# WILL_UPDATE <new> "" - old missing AND new exists
# SKIP "" outside - not under the old DATA_DIR (NEWPATH is empty)
# SKIP <new> oldexists - old file still present (adopted/not moved)
# SKIP <new> newmissing - rewritten target not on disk yet
classify_path() {
stored="$1"
new="$(rewrite_under_data_dir "$stored" "$OLD_DIR" "$NEW_DIR")"
if [ -z "$new" ]; then
printf 'SKIP\t\toutside'
return 0
fi
if [ -e "$stored" ]; then
printf 'SKIP\t%s\toldexists' "$new"
return 0
fi
if [ ! -e "$new" ]; then
printf 'SKIP\t%s\tnewmissing' "$new"
return 0
fi
printf 'WILL_UPDATE\t%s\t' "$new"
}
_reason_text() {
case "$1" in
outside) printf 'outside the old DATA_DIR - left as-is' ;;
oldexists) printf 'old file still exists - not stale, left as-is' ;;
newmissing) printf 'new file not found (copy the files across first)' ;;
*) printf '%s' "$1" ;;
esac
}
# --- header ---
print_header() {
echo "========================================================================"
echo " Dockhand - Relocate stack paths ($DB_LABEL)"
echo "========================================================================"
echo ""
echo " Updates where Dockhand LOOKS FOR each stack's compose/env files in its"
echo " database, after you changed DATA_DIR."
echo ""
echo " >> It does NOT move, copy, or delete any files. Your files are untouched."
echo " It only rewrites the stored path in the database. A pointer is changed"
echo " only when the old file is gone AND the new file already exists on disk."
echo ""
echo " Old DATA_DIR: $OLD_DIR"
echo " New DATA_DIR: $NEW_DIR"
[ -n "$DB_DISPLAY" ] && echo " Database: $DB_DISPLAY"
echo ""
if [ "$MODE" = "apply" ]; then
echo " >> Make a database backup before proceeding if you want a safety net."
echo ""
fi
}
# --- main pass: build a work list, present it, and (in apply mode) apply it ---
run_relocation() {
will=0; applied=0; declined=0
# Collect WILL_UPDATE items into a temp file: <kind>\t<id-or-index>\t<field>\t<label>\t<old>\t<new>
worklist="$(mktemp)"
print_header
echo "------------------------------------------------------------------------"
# stack_sources rows
db_list_stack_paths | while IFS=' ' read -r id field label stored; do
[ -z "$stored" ] && continue
res="$(classify_path "$stored")"
status="$(printf '%s' "$res" | cut -f1)"
new="$(printf '%s' "$res" | cut -f2)"
reason="$(printf '%s' "$res" | cut -f3)"
if [ "$status" = "WILL_UPDATE" ]; then
printf 'stack\t%s\t%s\t%s\t%s\t%s\n' "$id" "$field" "$label" "$stored" "$new" >> "$worklist"
printf ' %-22s %-8s WILL UPDATE\n' "$label" "$field"
printf ' %s\n -> %s\n' "$stored" "$new"
else
printf ' %-22s %-8s SKIP (%s)\n' "$label" "$field" "$(_reason_text "$reason")"
printf ' %s\n' "$stored"
fi
done
# external_stack_paths entries
db_list_external_paths | while IFS=' ' read -r idx stored; do
[ -z "$stored" ] && continue
res="$(classify_path "$stored")"
status="$(printf '%s' "$res" | cut -f1)"
new="$(printf '%s' "$res" | cut -f2)"
reason="$(printf '%s' "$res" | cut -f3)"
if [ "$status" = "WILL_UPDATE" ]; then
# field is '-' (a placeholder) because a tab-IFS `read` collapses an EMPTY
# field and would shift every later column.
printf 'external\t%s\t-\texternal_stack_paths[%s]\t%s\t%s\n' "$idx" "$idx" "$stored" "$new" >> "$worklist"
printf ' %-22s %-8s WILL UPDATE\n' "external_stack_paths[$idx]" ""
printf ' %s\n -> %s\n' "$stored" "$new"
else
printf ' %-22s %-8s SKIP (%s)\n' "external_stack_paths[$idx]" "" "$(_reason_text "$reason")"
printf ' %s\n' "$stored"
fi
done
echo "------------------------------------------------------------------------"
will="$(wc -l < "$worklist" | tr -d ' ')"
if [ "$MODE" = "dry" ]; then
echo " Summary: $will will update."
echo ""
echo " This was a DRY RUN. Nothing was changed."
echo " To apply, re-run with --apply (review each change), or --apply --all."
echo "========================================================================"
rm -f "$worklist"
return 0
fi
if [ "$will" -eq 0 ]; then
echo " Nothing to update."
echo "========================================================================"
rm -f "$worklist"
return 0
fi
# Interactive review needs a terminal. Without one (e.g. `docker exec` with no
# -it), tell the user how to proceed instead of erroring on each prompt.
if [ "$APPLY_ALL" -ne 1 ] && [ ! -r /dev/tty ]; then
echo " No terminal available for interactive review."
echo " Re-run with 'docker exec -it ...' to confirm each change, or add --all"
echo " to apply all $will change(s) without prompting."
echo "========================================================================"
rm -f "$worklist"
return 1
fi
# apply mode. Read the work list on FD 3, not stdin, so a db_update_* helper that
# happens to touch stdin (e.g. a client that reads it) can't swallow the list.
i=0
while IFS=' ' read -r kind key field label old new <&3; do
i=$((i + 1))
show_field="$field"; [ "$show_field" = "-" ] && show_field=""
printf '\n [%s/%s] %s %s\n' "$i" "$will" "$label" "$show_field"
printf ' %s\n -> %s\n' "$old" "$new"
if [ "$APPLY_ALL" -eq 1 ]; then
ans="y"
else
printf ' Apply this change? [y/n/q] '
read -r ans </dev/tty
fi
case "$ans" in
y|Y)
if [ "$kind" = "stack" ]; then
db_update_stack_path "$key" "$field" "$new"
else
db_update_external_path "$key" "$new"
fi
applied=$((applied + 1))
echo " ... updated."
;;
q|Q)
echo " ... quit."
break
;;
*)
declined=$((declined + 1))
echo " ... skipped."
;;
esac
done 3< "$worklist"
echo ""
echo " Done. $applied applied, $declined skipped by you."
echo " Restart Dockhand to pick up the changes."
echo "========================================================================"
rm -f "$worklist"
}
# --- argument parsing shared by both variants ---
parse_relocate_args() {
OLD_DIR=""; NEW_DIR=""; MODE="dry"; APPLY_ALL=0
for arg in "$@"; do
case "$arg" in
--apply) MODE="apply" ;;
--all) APPLY_ALL=1 ;;
--*) echo "Unknown option: $arg"; exit 2 ;;
*)
if [ -z "$OLD_DIR" ]; then OLD_DIR="$arg"
elif [ -z "$NEW_DIR" ]; then NEW_DIR="$arg"
else echo "Unexpected argument: $arg"; exit 2
fi
;;
esac
done
if [ -z "$OLD_DIR" ] || [ -z "$NEW_DIR" ]; then
echo "Usage: relocate-stack-paths.sh <old-data-dir> <new-data-dir> [--apply] [--all]"
exit 2
fi
case "$OLD_DIR" in /*) ;; *) echo "Error: old-data-dir must be an absolute path"; exit 2 ;; esac
case "$NEW_DIR" in /*) ;; *) echo "Error: new-data-dir must be an absolute path"; exit 2 ;; esac
}