#5818 "First-run setup: Restore from a backup, by upload": beside creating the first admin, setup offers "Or restore from a backup". Only while no user exists; the upload is checked (a newer backup or one whose key can't open its secrets is refused and not kept), then restored with no safety backup (nothing to keep), and Steward restarts into the backup's users. #5819 "Docs and CI": backups.md leads with restoring from Settings → Backups and from setup; `steward restore --in-place` does the same-schema restore from the shell, and CI's image check now uses it with a real pg_restore: a changed setting comes back and the audit log stays. Also exempts errors/restoring.html from the breadcrumb guard, as with the other whole-app error pages: it replaces every page while a restore runs, so there is nowhere to trail back to. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
@@ -53,6 +53,7 @@ def rotate() -> None:
|
||||
UPLOAD_LIMITS: dict[str, int] = {
|
||||
"/settings/backups/check/file/": 512 * 1024 * 1024,
|
||||
"/settings/backups/restore/file/": 512 * 1024 * 1024,
|
||||
"/setup/restore": 512 * 1024 * 1024,
|
||||
}
|
||||
|
||||
|
||||
|
||||
@@ -295,6 +295,50 @@ async def setup_post():
|
||||
return redirect(url_for("dashboard.index"))
|
||||
|
||||
|
||||
|
||||
@auth_bp.post("/setup/restore")
|
||||
@public_route("first-run setup's restore (refuses once a user exists)")
|
||||
async def setup_restore():
|
||||
"""Bring a new Steward up from a backup instead of an empty start (#5818):
|
||||
only while no user exists, so there is nothing here to lose and no
|
||||
safety backup to take. The upload's size limit is raised in
|
||||
auth/csrf.py UPLOAD_LIMITS. The restore then runs as from Settings →
|
||||
Backups (backup/restoring.py) and Steward restarts into the backup's
|
||||
users."""
|
||||
import shutil
|
||||
from pathlib import Path
|
||||
|
||||
from quart import current_app
|
||||
from steward.backup import job, restoring
|
||||
from steward.backup.dump import BackupError
|
||||
if await get_user_count(current_app) > 0:
|
||||
return redirect(url_for("auth.login"))
|
||||
|
||||
async def refused(error: str):
|
||||
return await render_template("auth/setup.html", default_url=request.host_url.rstrip("/"),
|
||||
restore_error=error), 400
|
||||
|
||||
upload = (await request.files).get("file")
|
||||
if upload is None or not upload.filename:
|
||||
return await refused("Choose a backup file to restore.")
|
||||
name = Path(upload.filename).name
|
||||
held = restoring.stage()
|
||||
await upload.save(held.path)
|
||||
try:
|
||||
report = await job.check_file(current_app._get_current_object(), held.path, name)
|
||||
except BackupError as exc:
|
||||
shutil.rmtree(held.dir, ignore_errors=True)
|
||||
return await refused(str(exc))
|
||||
if report.schema == "newer" or not report.ok:
|
||||
shutil.rmtree(held.dir, ignore_errors=True)
|
||||
return await refused("This Steward is older than the one that made the backup; update Steward, "
|
||||
"then restore it." if report.schema == "newer" else report.summary())
|
||||
if not restoring.start_restore(current_app._get_current_object(), held.path, name,
|
||||
username="first-run setup", replace_all=report.schema == "older",
|
||||
owned_dir=held.dir, safety=False):
|
||||
return await refused("A restore is already under way.")
|
||||
return redirect(url_for("auth.setup"))
|
||||
|
||||
# ── Invites (milestone 496) ──────────────────────────────────────────────────
|
||||
|
||||
_INVITE_GONE = ("This invite link has expired, been used or been withdrawn. "
|
||||
|
||||
Reference in New Issue
Block a user