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

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:
2026-11-07 22:07:44 -05:00
co-authored by Claude Opus 5.5
parent 794b04a64e
commit 14936b62dc
14 changed files with 92 additions and 393 deletions
+10 -15
View File
@@ -16,21 +16,20 @@ async def _seed_runs(db):
now = datetime.now(UTC)
for i in range(4):
db.add(BackupRun(
kind="db" if i % 2 == 0 else "images",
kind="db" if i % 2 == 0 else "media",
status="ok",
tag=None,
triggered_by="manual",
started_at=now - timedelta(seconds=10 - i),
finished_at=now - timedelta(seconds=9 - i),
sql_path="/tmp/fake.sql" if i % 2 == 0 else None,
tar_path=None if i % 2 == 0 else "/tmp/fake.tar.zst",
size_bytes=100,
manifest={},
))
await db.commit()
# --- POST /db, /images ----------------------------------------------
# --- POST /db ---------------------------------------------------------
@pytest.mark.asyncio
@@ -72,14 +71,10 @@ async def test_post_db_rejects_tag_too_long(client):
@pytest.mark.asyncio
async def test_post_images_dispatches(client, monkeypatch):
dispatched = []
monkeypatch.setattr(
"backend.app.tasks.backup.backup_images_task.delay",
lambda **kw: dispatched.append(kw),
)
resp = await client.post("/api/system/backup/images", json={})
assert resp.status_code == 202
async def test_the_images_tarball_is_gone(client):
assert (await client.post("/api/system/backup/images", json={})).status_code in (404, 405)
resp = await client.get("/api/system/backup/runs?kind=images")
assert resp.status_code == 400
# --- GET /runs -------------------------------------------------------
@@ -103,10 +98,10 @@ async def test_runs_filter_by_kind_db(client, _seed_runs):
@pytest.mark.asyncio
async def test_runs_filter_by_kind_images(client, _seed_runs):
resp = await client.get("/api/system/backup/runs?kind=images")
async def test_runs_filter_by_kind_media(client, _seed_runs):
resp = await client.get("/api/system/backup/runs?kind=media")
body = await resp.get_json()
assert all(r["kind"] == "images" for r in body["runs"])
assert body["runs"] and all(r["kind"] == "media" for r in body["runs"])
@pytest.mark.asyncio
@@ -298,7 +293,7 @@ async def test_get_settings_returns_defaults(client):
assert body["backup_db_nightly_enabled"] is False
assert body["backup_db_nightly_hour_utc"] == 3
assert body["backup_db_keep_last_n"] == 14
assert body["backup_images_keep_last_n"] == 3
assert "backup_images_keep_last_n" not in body
@pytest.mark.asyncio