improved restic repository operations log (prune, check, repair index etc.)

This commit is contained in:
jarek committed 2026-08-09 10:31:28 +02:00
1 parent 89b260758e
commit 16451d95d7
3 files changed
+224 -85

No files matched your search

@@ -1,16 +1,17 @@
import { json } from '@sveltejs/kit';
import type { RequestHandler } from './$types';
import { authorize } from '$lib/server/authorize';
import { requireBackups } from '$lib/server/backups/route-guards';
import { auditBackupDestination } from '$lib/server/audit';
import { getBackupDestination, getBackupConfigs } from '$lib/server/db';
import { runRepoTask } from '$lib/server/backups';
import { createJobResponse } from '$lib/server/sse';
export const POST: RequestHandler = async (event) => {
const { params, request, cookies } = event;
const auth = await authorize(cookies);
if (auth.authEnabled && !await auth.can('backups', 'manage')) {
return json({ error: 'Permission denied' }, { status: 403 });
}
const denied = await requireBackups(auth, 'manage');
if (denied) return denied;
const destId = parseInt(params.id);
if (isNaN(destId)) return json({ error: 'Invalid destination ID' }, { status: 400 });
@@ -41,20 +42,33 @@ export const POST: RequestHandler = async (event) => {
}
}
try {
// `task` is validated against the allowed list above.
const result = await runRepoTask(destId, task as import('$lib/server/backups').RepoTask);
// 'prune' and 'check' map to canonical audit actions; the rest land on
// 'update' so they're still recoverable from the log.
const action = task === 'prune' ? 'prune' : task === 'check' ? 'verify' : 'update';
// 'prune' and 'check' map to canonical audit actions; the rest land on
// 'update' so they're still recoverable from the log.
const action = task === 'prune' ? 'prune' : task === 'check' ? 'verify' : 'update';
// Stream restic output live into the shared job/log modal (same channel as
// backup/restore). Guards above (auth, env-scope, allowlist) already ran; only
// the restic run is wrapped. `createJobResponse` keeps the backward-compat
// synchronous JSON path for `Accept: application/json` callers (tests/CLI).
return createJobResponse(async (send) => {
let result: Awaited<ReturnType<typeof runRepoTask>>;
try {
// `task` is validated against the allowed list above.
result = await runRepoTask(destId, task as import('$lib/server/backups').RepoTask, {
onProgress: (message) => send('progress', { message })
});
} catch (error) {
// A genuine throw (unexpected). Audit as failed, then rethrow so the job
// ends in 'error' state.
const msg = error instanceof Error ? error.message : String(error);
await auditBackupDestination(event, 'update', destId, dest.name, { task, success: false, error: msg });
throw error instanceof Error ? error : new Error(msg);
}
// runRepoTask can resolve with { success: false, error } WITHOUT throwing
// (e.g. repo unreachable / needs init). Derive the audited success and
// error from the result rather than hardcoding success (audit #53).
// (e.g. repo unreachable / needs init). Derive the audited success/error
// from the result rather than hardcoding success (audit #53).
await auditBackupDestination(event, action, destId, dest.name, { task, success: result.success, error: result.error });
return json(result, { status: result.success ? 200 : 500 });
} catch (error) {
const msg = error instanceof Error ? error.message : String(error);
await auditBackupDestination(event, 'update', destId, dest.name, { task, success: false, error: msg });
return json({ error: msg }, { status: 500 });
}
send('result', result);
if (!result.success) throw new Error(result.error || `${task} failed`);
}, request);
};