Files
siftlode/scripts/restore.sh
T
peter 9f28f46839 fix(e12): unbreak the gate, and stop printing a command that can strand the restore
Round five. 47 -> 34 -> 15 -> 7 -> 6 raw findings, and this round's sharpest was the
loop's own blind spot: **the branch did not pass the gate.** `prettier --check` is one of
its eight lanes and I had been running the other seven, so an unformatted `onError` from
round one sat on the branch for four rounds — `siftlode publish` would have refused it,
with no --skip flag to get past.

**The restart hint could still strand the restored data.** Round four said the scripts no
longer print a command they cannot resolve; they did. The COMPOSE_FILE fallback only fires
when `docker-compose.yml` is ABSENT, so in a source checkout it never fires and the hint
expanded to a bare, pasteable `docker compose up -d` — which reads the other file, whose
volumes are `pgdata`/`downloads` rather than `siftlode_pgdata`/`siftlode_downloads`, and
recreates the database on an empty one. When the file is unresolved the scripts now print
`-f <the compose file you started with>`: not runnable, which is the point.

**A comment naming the wrong owner.** `formats.py` still said a zero budget is "a refusal
for enqueue to make" — the premise round one disproved, three files away from the two
docstrings that now say the opposite. Anyone acting on it would delete the worker's refusal
and re-open the original inversion, so it now says where the refusal actually lives.

Also: the reset-flag rule was rewritten in three consecutive rounds and pinned by nothing —
two tests now cover a fragment losing its token, both by a pasted verify link and by
navigating away entirely; `backup.ps1` deletes a partially-copied dump before throwing, the
guard round four gave the docs but not the script it mirrors; and one comment that read as
if the asset were untouched by a line four below it.

Gate green, all eight lanes: ruff, ruff format, 3x tsc, eslint, prettier, knip,
vitest 687, pytest 950.
2026-09-05 04:41:21 +02:00

63 lines
3.5 KiB
Bash

#!/bin/sh
# Restores a Siftlode Postgres dump created by backup.sh into the running db container.
# Usage: ./scripts/restore.sh backups/siftlode-YYYYMMDD-HHMMSS.dump
#
# Restoring the database does NOT restore the secrets that decrypt it. If this dump is from an
# instance with a different TOKEN_ENCRYPTION_KEY than the current .env, the encrypted sysconfig
# values and refresh tokens will not be readable. See docs/self-hosting.md § Backups.
set -e
# See backup.sh: the documented self-host layout ships only docker-compose.selfhost.yml, so plain
# `docker compose` has no configuration file to find there.
if [ -z "${COMPOSE_FILE:-}" ] && [ ! -f docker-compose.yml ] && [ -f docker-compose.selfhost.yml ]; then
COMPOSE_FILE=docker-compose.selfhost.yml
export COMPOSE_FILE
fi
FILE="$1"
if [ -z "$FILE" ]; then echo "Usage: restore.sh <dumpfile>"; exit 1; fi
if [ ! -f "$FILE" ]; then echo "Dump file not found: $FILE"; exit 1; fi
USER_NAME="${POSTGRES_USER:-siftlode}"
DB_NAME="${POSTGRES_DB:-siftlode}"
# Stop the app before dropping its tables. `--clean` DROPs each object in its own transaction, and a
# running api (with its scheduler) holds locks that make those DROPs queue behind it — abort the wait
# and the database is left half-dropped. It also keeps the app from writing into a schema that is
# only partly back.
#
# It is NOT restarted automatically, on purpose. Every start runs `alembic upgrade head`, so on the
# documented rollback path (restore a pre-upgrade dump, THEN pin the older IMAGE_TAG) an automatic
# restart would re-apply the very migrations the restore just undid. On a FAILED restore it would
# put the app back on a half-restored schema, which is the second thing the stop exists to prevent.
# The operator starts it when they are ready — with `up -d`, not `start`: `start` relaunches the
# EXISTING container, so it ignores a newly pinned IMAGE_TAG and comes back on the image being
# rolled back FROM.
if ! docker compose stop api worker; then
echo "Could not stop api/worker — refusing to restore while the app may still be writing." >&2
exit 1
fi
# pg_restore exits non-zero for ignorable warnings too (--clean --if-exists on a fresh database
# reports every missing object it tried to drop), so a non-zero code is reported as "check this",
# not swallowed and not dressed up as success.
if docker compose exec -T db pg_restore -U "$USER_NAME" -d "$DB_NAME" --clean --if-exists < "$FILE"; then
echo "Restored $FILE"
echo "api and worker are stopped. Start them again with:"
if [ -n "${COMPOSE_FILE:-}" ]; then
echo " docker compose -f $COMPOSE_FILE up -d"
else
# Deliberately NOT a runnable line. Both compose files can be present (a source checkout), and
# the two declare DIFFERENT volume names — so a pasted bare 'docker compose up -d' would recreate
# the database on an empty volume and strand what was just restored.
echo " docker compose -f <the compose file you started with> up -d"
fi
echo "It must be the SAME file you started with: 'up -d' recreates from whatever file it reads,"
echo "and a different one points at different volumes. Rolling a release back? Pin the older"
echo "IMAGE_TAG in .env FIRST: every start re-runs the migrations, and 'start' would relaunch the"
echo "existing container on the image you are rolling back FROM."
else
echo "pg_restore reported errors while restoring $FILE — read the output above before treating this instance as restored." >&2
echo "api and worker are left STOPPED: the database may be half-restored. Fix the dump and re-run before starting them." >&2
exit 1
fi