feat(523): the backup job - an hourly tick that runs at the chosen hour, Back up now, and the last 30 runs
CI & Build / Python lint (push) Successful in 3s
CI & Build / Plugin hooks (push) Successful in 13s
CI & Build / TypeScript typecheck (push) Successful in 53s
CI & Build / integration (push) Successful in 1m9s
CI & Build / Python tests (push) Successful in 1m54s
CI & Build / Build & push image (push) Successful in 23s
CI & Build / Python lint (push) Successful in 3s
CI & Build / Plugin hooks (push) Successful in 13s
CI & Build / TypeScript typecheck (push) Successful in 53s
CI & Build / integration (push) Successful in 1m9s
CI & Build / Python tests (push) Successful in 1m54s
CI & Build / Build & push image (push) Successful in 23s
services/backup_job.py reads its config when it fires, so startup reads nothing (#4181) and a changed hour needs no reschedule. A run is held to a 60-minute deadline (a hung NFS mount would otherwise hold the lock and skip every later backup), never runs twice at once, and keeps its outcome - file, size, duration, or the reason in words - in the bounded backup_runs setting, which no backup carries. summarise() gives the page its verdict: overdue when on and no success for 26h. Admin API: GET/PUT /api/admin/backups, POST /backups/check (tests unsaved form values, 45s deadline), POST /backups/run (202, background). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
@@ -108,6 +108,32 @@ class BackupConfig:
|
||||
return out
|
||||
|
||||
|
||||
def apply_changes(current: BackupConfig, data: dict) -> BackupConfig:
|
||||
"""`current` with the fields an admin sent changed, checked strictly.
|
||||
|
||||
`from_dict` forgives a stored value it can't read; a value a person just
|
||||
typed is refused instead, with the reason, so a typo in the hour does not
|
||||
silently become 02:00. Keys that aren't config fields (the secret, say)
|
||||
are ignored here.
|
||||
"""
|
||||
names = {f.name for f in fields(BackupConfig)}
|
||||
changes = {k: v for k, v in (data or {}).items() if k in names}
|
||||
if "hour" in changes:
|
||||
try:
|
||||
hour = int(changes["hour"])
|
||||
except (TypeError, ValueError):
|
||||
raise ValueError("The hour must be a whole number from 0 to 23.") from None
|
||||
if not 0 <= hour <= 23:
|
||||
raise ValueError("The hour must be from 0 to 23 (UTC).")
|
||||
changes["hour"] = hour
|
||||
if "destination" in changes and changes["destination"] not in DESTINATION_KINDS:
|
||||
raise ValueError("The destination must be a folder or an S3 bucket.")
|
||||
for name in ("enabled", "s3_verify_tls"):
|
||||
if name in changes and not isinstance(changes[name], bool):
|
||||
raise ValueError(f"{name} must be true or false.")
|
||||
return BackupConfig.from_dict({**current.to_dict(), **changes})
|
||||
|
||||
|
||||
async def load_config() -> BackupConfig:
|
||||
raw = await get_admin_setting(CONFIG_SETTING, "")
|
||||
try:
|
||||
|
||||
Reference in New Issue
Block a user