refactor: retire the images tarball backup
CI and images / lint (push) Successful in 2s
CI and images / extension-version (push) Successful in 2s
CI and images / extension-test (push) Successful in 20s
CI and images / frontend-build (push) Successful in 25s
CI and images / backend-lint-and-test (push) Successful in 50s
CI and images / integration (push) Successful in 2m38s
CI and images / build-agent (push) Successful in 5s
CI and images / sign-extension (push) Successful in 8s
CI and images / build-web (push) Successful in 4m10s
CI and images / smoke-web (push) Successful in 1m3s
CI and images / promote (push) Successful in 1s
CI and images / refresh-dev (push) Skipped
CI and images / unattended-failure (push) Skipped
CI and images / lint (push) Successful in 2s
CI and images / extension-version (push) Successful in 2s
CI and images / extension-test (push) Successful in 20s
CI and images / frontend-build (push) Successful in 25s
CI and images / backend-lint-and-test (push) Successful in 50s
CI and images / integration (push) Successful in 2m38s
CI and images / build-agent (push) Successful in 5s
CI and images / sign-extension (push) Successful in 8s
CI and images / build-web (push) Successful in 4m10s
CI and images / smoke-web (push) Successful in 1m3s
CI and images / promote (push) Successful in 1s
CI and images / refresh-dev (push) Skipped
CI and images / unattended-failure (push) Skipped
Images and attachments are backed up off-site, one object per file, so the tar+zstd of /images goes: its service and restore, both tasks, the trigger and restore routes, the Keep last setting, the screen's Images section, and the tests. Migration 0122 drops backup_run.tar_path and import_settings.backup_images_keep_last_n and deletes any images runs. The local database dump stays for an install with no off-site destination, and the Backups card now says plainly that images and attachments are backed up only off-site. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
@@ -1,5 +1,5 @@
|
||||
"""FC-3h: /api/system/backup — create/list/restore/delete/tag for
|
||||
DB + image backups.
|
||||
"""/api/system/backup: database dumps, the off-site destination and its media
|
||||
backup, restores, test restores, and the run history.
|
||||
|
||||
Read endpoints are public on FC (operator-facing internal API; same
|
||||
posture as /api/system/activity). Write endpoints take a typed
|
||||
@@ -20,13 +20,12 @@ system_backup_bp = Blueprint(
|
||||
"system_backup", __name__, url_prefix="/api/system/backup",
|
||||
)
|
||||
|
||||
_KINDS = frozenset({"db", "images", "media", "restore", "reconcile", "test"})
|
||||
_KINDS = frozenset({"db", "media", "restore", "reconcile", "test"})
|
||||
_TAG_MAX_LEN = 64
|
||||
_BACKUP_SETTINGS_FIELDS = (
|
||||
"backup_db_nightly_enabled",
|
||||
"backup_db_nightly_hour_utc",
|
||||
"backup_db_keep_last_n",
|
||||
"backup_images_keep_last_n",
|
||||
)
|
||||
|
||||
|
||||
@@ -44,7 +43,6 @@ def _row_to_dict(r: BackupRun) -> dict:
|
||||
if r.finished_at and r.started_at else None
|
||||
),
|
||||
"sql_path": r.sql_path,
|
||||
"tar_path": r.tar_path,
|
||||
"size_bytes": r.size_bytes,
|
||||
"error": r.error,
|
||||
"restored_from_id": r.restored_from_id,
|
||||
@@ -81,10 +79,6 @@ def _validate_backup_settings_patch(body: dict):
|
||||
v = body["backup_db_keep_last_n"]
|
||||
if not isinstance(v, int) or isinstance(v, bool) or not (1 <= v <= 365):
|
||||
return _bad("invalid_value", detail="backup_db_keep_last_n must be 1..365")
|
||||
if "backup_images_keep_last_n" in body:
|
||||
v = body["backup_images_keep_last_n"]
|
||||
if not isinstance(v, int) or isinstance(v, bool) or not (1 <= v <= 100):
|
||||
return _bad("invalid_value", detail="backup_images_keep_last_n must be 1..100")
|
||||
return None
|
||||
|
||||
|
||||
@@ -99,17 +93,6 @@ async def trigger_db_backup():
|
||||
return jsonify({"status": "dispatched"}), 202
|
||||
|
||||
|
||||
@system_backup_bp.route("/images", methods=["POST"])
|
||||
async def trigger_images_backup():
|
||||
body = await request.get_json(silent=True) or {}
|
||||
tag = _validate_tag(body.get("tag"))
|
||||
if isinstance(tag, tuple):
|
||||
return tag
|
||||
from ..tasks.backup import backup_images_task
|
||||
backup_images_task.delay(tag=tag, triggered_by="manual")
|
||||
return jsonify({"status": "dispatched"}), 202
|
||||
|
||||
|
||||
@system_backup_bp.route("/media", methods=["POST"])
|
||||
async def trigger_media_backup():
|
||||
"""Send the images and attachments not yet in the off-site bucket. Refused
|
||||
@@ -348,17 +331,17 @@ async def trigger_restore(run_id: int):
|
||||
row = await session.get(BackupRun, run_id)
|
||||
if row is None:
|
||||
return _bad("not_found", status=404)
|
||||
if row.status != "ok":
|
||||
return _bad(
|
||||
"not_restorable",
|
||||
detail=f"source backup status={row.status!r}; only 'ok' rows are restorable",
|
||||
)
|
||||
if (row.manifest or {}).get("offsite"):
|
||||
return _bad(
|
||||
"not_restorable",
|
||||
detail=("this dump is kept in the off-site bucket, not on this server, "
|
||||
"so it can't be restored from here"),
|
||||
)
|
||||
if row.kind != "db" or row.status != "ok" or not row.sql_path:
|
||||
return _bad(
|
||||
"not_restorable",
|
||||
detail="only a finished database dump kept on this server can be restored here",
|
||||
)
|
||||
expected = f"restore-{row.kind}-{row.id}"
|
||||
if supplied != expected:
|
||||
return _bad(
|
||||
@@ -366,15 +349,10 @@ async def trigger_restore(run_id: int):
|
||||
detail=f"confirm must equal {expected!r}",
|
||||
expected=expected,
|
||||
)
|
||||
kind = row.kind
|
||||
|
||||
if kind == "db":
|
||||
from ..tasks.backup import restore_db_task
|
||||
restore_db_task.delay(source_backup_run_id=run_id)
|
||||
else: # 'images' (the only other value _KINDS allows via the trigger path)
|
||||
from ..tasks.backup import restore_images_task
|
||||
restore_images_task.delay(source_backup_run_id=run_id)
|
||||
return jsonify({"status": "dispatched", "kind": kind}), 202
|
||||
from ..tasks.backup import restore_db_task
|
||||
restore_db_task.delay(source_backup_run_id=run_id)
|
||||
return jsonify({"status": "dispatched", "kind": "db"}), 202
|
||||
|
||||
|
||||
@system_backup_bp.route("/runs/<int:run_id>/test", methods=["POST"])
|
||||
@@ -414,7 +392,7 @@ async def delete_run(run_id: int):
|
||||
)
|
||||
from ..services import backup_service
|
||||
backup_service.unlink_artifact_files(
|
||||
sql_path=row.sql_path, tar_path=row.tar_path,
|
||||
sql_path=row.sql_path,
|
||||
manifest_path=(row.manifest or {}).get("manifest_path"),
|
||||
)
|
||||
await session.delete(row)
|
||||
@@ -430,7 +408,6 @@ async def get_settings():
|
||||
"backup_db_nightly_enabled": row.backup_db_nightly_enabled,
|
||||
"backup_db_nightly_hour_utc": row.backup_db_nightly_hour_utc,
|
||||
"backup_db_keep_last_n": row.backup_db_keep_last_n,
|
||||
"backup_images_keep_last_n": row.backup_images_keep_last_n,
|
||||
})
|
||||
|
||||
|
||||
|
||||
Reference in New Issue
Block a user