feat: add single-database restore mode to restore.sh
This commit is contained in:
+55
@@ -86,6 +86,61 @@ cmd_file() {
|
||||
log "Restored file available at: $final"
|
||||
}
|
||||
|
||||
detect_client() {
|
||||
if docker exec "$DB_CONTAINER" sh -c 'command -v mariadb' >/dev/null 2>&1; then
|
||||
CLIENT_BIN="mariadb"
|
||||
else
|
||||
CLIENT_BIN="mysql"
|
||||
fi
|
||||
}
|
||||
|
||||
get_root_creds() {
|
||||
if [[ -n "${MYSQL_ROOT_PASSWORD:-}" ]]; then
|
||||
DB_PASS="$MYSQL_ROOT_PASSWORD"
|
||||
elif [[ -r "$ROOT_PASSWORD_FILE" ]]; then
|
||||
DB_PASS="$(< "$ROOT_PASSWORD_FILE")"
|
||||
else
|
||||
die "no root DB password: set MYSQL_ROOT_PASSWORD or create $ROOT_PASSWORD_FILE (chmod 600)"
|
||||
fi
|
||||
}
|
||||
|
||||
confirm_or_abort() {
|
||||
local dbname="$1" typed
|
||||
[[ "$YES" == true ]] && return 0
|
||||
echo "This will DROP/overwrite database '$dbname'. Type the database name to confirm:"
|
||||
read -r typed
|
||||
[[ "$typed" == "$dbname" ]] || die "confirmation did not match '$dbname' - aborting"
|
||||
}
|
||||
|
||||
restore_single_db() {
|
||||
local sqlfile="$1" dbname="$2"
|
||||
[[ -s "$sqlfile" ]] || die "dump file missing or empty: $sqlfile"
|
||||
run_cmd docker exec -e MYSQL_PWD="$DB_PASS" "$DB_CONTAINER" \
|
||||
"$CLIENT_BIN" -u root -e "CREATE DATABASE IF NOT EXISTS \`$dbname\`;"
|
||||
log "[RUN] docker exec -i ... $CLIENT_BIN -u root $dbname < $sqlfile"
|
||||
docker exec -i -e MYSQL_PWD="$DB_PASS" "$DB_CONTAINER" \
|
||||
"$CLIENT_BIN" -u root "$dbname" < "$sqlfile"
|
||||
}
|
||||
|
||||
cmd_db() {
|
||||
local dbname="$1" dumpfile dest
|
||||
resolve_archive
|
||||
step "Restoring database '$dbname' from archive $RESOLVED_ARCHIVE"
|
||||
if [[ "$DRY_RUN" == true ]]; then
|
||||
log "[DRY-RUN] would extract ${ARCHIVE_TARGET_PATH}/${DUMP_SUBDIR}/${dbname}.sql from ${REPO}::${RESOLVED_ARCHIVE}"
|
||||
log "[DRY-RUN] would DROP/recreate database '$dbname' and import the dump using root credentials"
|
||||
return 0
|
||||
fi
|
||||
confirm_or_abort "$dbname"
|
||||
dest="/tmp/restore-db-$$"
|
||||
dumpfile="$(extract_path "${DUMP_SUBDIR}/${dbname}.sql" "$dest" "$RESOLVED_ARCHIVE" | tail -n1)"
|
||||
detect_client
|
||||
get_root_creds
|
||||
restore_single_db "$dumpfile" "$dbname"
|
||||
log "Database '$dbname' restored from $dumpfile"
|
||||
rm -rf "$dest"
|
||||
}
|
||||
|
||||
usage() {
|
||||
cat <<'EOF'
|
||||
Usage:
|
||||
|
||||
@@ -91,12 +91,70 @@ test_file_restore_dry_run_makes_no_borg_call() {
|
||||
rm -rf "$mockdir" "$dest"
|
||||
}
|
||||
|
||||
test_db_restore_with_yes_runs_full_sequence() {
|
||||
local mockdir out bashenv
|
||||
mockdir="$(mktemp -d)"
|
||||
setup_mock_bin "$mockdir"
|
||||
echo "rootpass" > "$mockdir/rootpw"
|
||||
# This host may have a real `docker` CLI in one of the system dirs that
|
||||
# restore.sh's hardened PATH prepends (e.g. /usr/local/bin), which would
|
||||
# shadow the mock and make a real (unreachable) docker daemon get called
|
||||
# instead. A BASH_ENV-sourced function takes precedence over PATH lookup
|
||||
# regardless of PATH ordering, so force `docker` to the mock that way.
|
||||
bashenv="$mockdir/bash_env.sh"
|
||||
cat > "$bashenv" <<EOF
|
||||
docker() { "$mockdir/docker" "\$@"; }
|
||||
EOF
|
||||
out="$(PATH="$mockdir:$PATH" ROOT_PASSWORD_FILE="$mockdir/rootpw" BASH_ENV="$bashenv" bash "$RESTORE" db shopdb --yes </dev/null)"
|
||||
assert_contains "$out" "Database 'shopdb' restored from" "db mode reports success"
|
||||
if grep -q "CREATE DATABASE IF NOT EXISTS" "$mockdir/mock.log"; then
|
||||
echo "PASS: db mode issued CREATE DATABASE"
|
||||
else
|
||||
echo "FAIL: db mode did not issue CREATE DATABASE"
|
||||
FAILURES=$((FAILURES + 1))
|
||||
fi
|
||||
rm -rf "$mockdir"
|
||||
}
|
||||
|
||||
test_db_restore_dry_run_skips_confirmation_and_calls() {
|
||||
local mockdir out
|
||||
mockdir="$(mktemp -d)"
|
||||
setup_mock_bin "$mockdir"
|
||||
out="$(PATH="$mockdir:$PATH" bash "$RESTORE" db shopdb --dry-run </dev/null)"
|
||||
assert_contains "$out" "DRY-RUN" "db dry-run prints DRY-RUN plan"
|
||||
# cmd_db calls resolve_archive() (a borg list call) before checking DRY_RUN,
|
||||
# same as cmd_file does, so mock.log legitimately gets a "borg list" entry.
|
||||
# What must NOT happen in dry-run is any docker/mysql/mariadb interaction.
|
||||
if grep -qE "^(docker|mysql|mariadb)" "$mockdir/mock.log"; then
|
||||
echo "FAIL: dry-run invoked a docker/db-client mock binary"
|
||||
FAILURES=$((FAILURES + 1))
|
||||
else
|
||||
echo "PASS: dry-run made no docker/db-client calls"
|
||||
fi
|
||||
rm -rf "$mockdir"
|
||||
}
|
||||
|
||||
test_db_restore_aborts_on_wrong_confirmation() {
|
||||
local mockdir rc
|
||||
mockdir="$(mktemp -d)"
|
||||
setup_mock_bin "$mockdir"
|
||||
set +e
|
||||
echo "wrongname" | PATH="$mockdir:$PATH" bash "$RESTORE" db shopdb >/dev/null 2>&1
|
||||
rc=$?
|
||||
set -e
|
||||
assert_eq "1" "$rc" "db mode aborts on mismatched confirmation"
|
||||
rm -rf "$mockdir"
|
||||
}
|
||||
|
||||
test_help_exits_zero
|
||||
test_no_args_exits_one
|
||||
test_unknown_command_exits_one
|
||||
test_list_archives
|
||||
test_file_restore_extracts_to_dest
|
||||
test_file_restore_dry_run_makes_no_borg_call
|
||||
test_db_restore_with_yes_runs_full_sequence
|
||||
test_db_restore_dry_run_skips_confirmation_and_calls
|
||||
test_db_restore_aborts_on_wrong_confirmation
|
||||
|
||||
echo "-----"
|
||||
if [[ "$FAILURES" -gt 0 ]]; then
|
||||
|
||||
Reference in New Issue
Block a user