OpenAPI spec at /api/docs with a Scalar viewer, opt-in via FEAT_API_DOCS

This commit is contained in:
jarek committed 2026-08-17 09:50:34 +02:00
1 parent ba8670af66
commit da26f7f764
251 files changed
+32497 -66

No files matched your search

+24
View File
@@ -11,6 +11,17 @@ import { registerSchedule, isValidCron } from '$lib/server/scheduler';
import { assertStackBackupable } from '$lib/server/backups';
import { validateRetention, retentionToStore } from '$lib/server/backups/helpers';
/**
* GET /api/backup/configs - List backup configurations
*
* @openapi
* summary: List backup configurations, optionally filtered by type, target and environment (enterprise callers only see configs for environments they can access)
* description: Permission denial (403, "backups:view") is produced by the shared requireBackups route guard.
* query: type:string Filter by backup config type (e.g. "container", "stack")
* query: target:string Filter by the backed-up target name (container or stack name)
* query: env:integer Filter by environment id (from GET /api/environments)
* resp-200: Array of backup configuration objects
*/
export const GET: RequestHandler = async ({ url, cookies }) => {
const auth = await authorize(cookies);
const denied = await requireBackups(auth, 'view');
@@ -42,6 +53,19 @@ export const GET: RequestHandler = async ({ url, cookies }) => {
return json(configs);
};
/**
* POST /api/backup/configs - Create a backup configuration
*
* @openapi
* summary: Create a backup configuration for a container or stack, optionally scheduled, and register its cron schedule when enabled
* description: Permission denial (403, "backups:manage") is produced by the shared requireBackups route guard. A requested environmentId the caller can't access also 403s (enterprise). destinationId from GET /api/backup/destinations. environmentId from GET /api/environments.
* body: {destinationId:integer!, targetName:string!, type:string, environmentId:integer, enabled:boolean, allVolumes:boolean, selectedVolumes:array<string>, stopBeforeBackup:boolean, schedule:string, retention:{keepLast:integer, keepDaily:integer, keepWeekly:integer, keepMonthly:integer, keepYearly:integer}, options:{}, tags:array<string>}
* body-example: {"destinationId":3,"targetName":"nextcloud","type":"container","environmentId":1,"enabled":true,"allVolumes":true,"stopBeforeBackup":false,"schedule":"0 3 * * *","retention":{"keepDaily":7,"keepWeekly":4}}
* resp-201: The created backup configuration object
* resp-400: Invalid input — missing destinationId/targetName, invalid targetName, invalid cron expression, invalid retention, or a local repository paired with a remote environment / non-backupable stack
* resp-403: Access denied to the requested environment (enterprise)
* resp-500: Failed to create the backup configuration (persistence error)
*/
export const POST: RequestHandler = async (event) => {
const { request, cookies } = event;
const auth = await authorize(cookies);
@@ -11,6 +11,15 @@ import { isBackupRunning } from '$lib/server/backups';
import { validateRetention, retentionToStore, resolveEnabledOnScheduleChange } from '$lib/server/backups/helpers';
import { requireBackups, loadConfigGateEnv } from '$lib/server/backups/route-guards';
/**
* GET /api/backup/configs/{id} - Get a single backup configuration
*
* @openapi
* summary: Fetch a single backup configuration by id, enforcing environment-scoped access
* description: Permission ("backups:view") and environment-access denials (403) and not-found (404) are produced by the shared route guards.
* path: id:integer! Backup configuration id (from GET /api/backup/configs)
* resp-200: The backup configuration object
*/
export const GET: RequestHandler = async ({ params, cookies }) => {
const auth = await authorize(cookies);
const denied = await requireBackups(auth, 'view');
@@ -24,6 +33,20 @@ export const GET: RequestHandler = async ({ params, cookies }) => {
return json(gated.config);
};
/**
* PUT /api/backup/configs/{id} - Update a backup configuration
*
* @openapi
* summary: Update a backup configuration and re-register or remove its cron schedule accordingly
* description: Permission ("backups:manage") and environment-access denials (403) and not-found (404) are produced by the shared route guards. The environment is fixed at creation and cannot be changed here. destinationId from GET /api/backup/destinations.
* path: id:integer! Backup configuration id (from GET /api/backup/configs)
* body: {destinationId:integer, enabled:boolean, allVolumes:boolean, selectedVolumes:array<string>, stopBeforeBackup:boolean, schedule:string, retention:{keepLast:integer, keepDaily:integer, keepWeekly:integer, keepMonthly:integer, keepYearly:integer}, options:{}, tags:array<string>}
* body-example: {"schedule":"0 4 * * *","enabled":true,"stopBeforeBackup":true,"retention":{"keepDaily":14}}
* resp-200: The updated backup configuration object
* resp-400: Invalid input — invalid cron expression, invalid retention, or a local repository paired with a remote environment
* resp-409: Cannot change the destination while a backup is running for this config
* resp-500: Update failed (persistence error)
*/
export const PUT: RequestHandler = async (event) => {
const { params, request, cookies } = event;
const auth = await authorize(cookies);
@@ -101,6 +124,17 @@ export const PUT: RequestHandler = async (event) => {
}
};
/**
* DELETE /api/backup/configs/{id} - Delete a backup configuration
*
* @openapi
* summary: Delete a backup configuration and unregister its schedule
* description: Permission ("backups:manage") and environment-access denials (403) and not-found (404) are produced by the shared route guards.
* path: id:integer! Backup configuration id (from GET /api/backup/configs)
* resp-200: Returns { success: true } once the configuration is deleted
* resp-200-example: {"success":true}
* resp-409: A backup is currently running for this config — stop it before deleting
*/
export const DELETE: RequestHandler = async (event) => {
const { params, cookies } = event;
const auth = await authorize(cookies);
@@ -6,6 +6,15 @@ import { createJobResponse } from '$lib/server/sse';
import { auditBackup } from '$lib/server/audit';
import { requireBackups, loadConfigGateEnv } from '$lib/server/backups/route-guards';
/**
* POST /api/backup/configs/{id}/run - Run a backup now (streamed job)
*
* @openapi
* summary: Trigger a manual backup run for a configuration, streaming progress as a Server-Sent Events job
* description: Returns a text/event-stream that emits `progress` events during the run and a final `result` event. Permission ("backups:manage") and environment-access denials (403) and not-found (404) are produced by the shared route guards.
* path: id:integer! Backup configuration id (from GET /api/backup/configs)
* resp-200: Server-Sent Events stream of progress and a final result event (status "success", "warning", "skipped" or "error")
*/
export const POST: RequestHandler = async (event) => {
const { params, request, cookies } = event;
const auth = await authorize(cookies);
@@ -5,6 +5,17 @@ import { auditBackup } from '$lib/server/audit';
import { cancelBackup } from '$lib/server/backups';
import { requireBackups, loadConfigGateEnv } from '$lib/server/backups/route-guards';
/**
* POST /api/backup/configs/{id}/stop - Cancel a running backup
*
* @openapi
* summary: Cancel the in-flight backup for a configuration
* description: Permission ("backups:manage") and environment-access denials (403) and not-found (404) are produced by the shared route guards.
* path: id:integer! Backup configuration id (from GET /api/backup/configs)
* resp-200: Returns { success: true, stopped } where "stopped" indicates whether a running backup helper was actually killed
* resp-200-example: {"success":true,"stopped":true}
* resp-500: Failed to cancel the backup (internal error)
*/
export const POST: RequestHandler = async (event) => {
const { params, cookies } = event;
const auth = await authorize(cookies);
@@ -36,6 +36,14 @@ function prepareDestination(dest: any, opts: { includeEnvVars: boolean }): any {
return result;
}
/**
* GET /api/backup/destinations - List backup destinations
*
* @openapi
* summary: List all backup destinations (restic repositories); the password is stripped and cloud-credential env vars are omitted from the list view
* description: Permission denial (403, "backups:view") is produced by the shared requireBackups route guard.
* resp-200: Array of backup destination objects without secrets (no password, no envVars)
*/
export const GET: RequestHandler = async ({ cookies }) => {
const auth = await authorize(cookies);
const denied = await requireBackups(auth, 'view');
@@ -46,6 +54,19 @@ export const GET: RequestHandler = async ({ cookies }) => {
return json(destinations.map(d => prepareDestination(d, { includeEnvVars: false })));
};
/**
* POST /api/backup/destinations - Create a backup destination
*
* @openapi
* summary: Create a restic backup destination, auto-initialize and test the repository, and register its default maintenance schedules
* description: Permission denial (403, "backups:manage") is produced by the shared requireBackups route guard.
* body: {name:string!, repository:string!, password:string!, envVars:{}, flags:string, hostPath:string, policies:string}
* body-example: {"name":"S3 Offsite","repository":"s3:s3.amazonaws.com/my-bucket/restic","password":"***","envVars":{"AWS_ACCESS_KEY_ID":"***","AWS_SECRET_ACCESS_KEY":"***"}}
* resp-201: The created backup destination object (includes decrypted envVars since the caller just supplied them; password is stripped)
* resp-400: Invalid input — missing name/repository/password, unsupported/SSRF-blocked repository, invalid restic flags, or an invalid cron schedule in the policies
* resp-409: A destination with this name already exists
* resp-500: Failed to create the destination (persistence error)
*/
export const POST: RequestHandler = async (event) => {
const { request, cookies } = event;
const auth = await authorize(cookies);
@@ -37,6 +37,17 @@ function prepareDestination(dest: any, includeSecrets: boolean): any {
return result;
}
/**
* GET /api/backup/destinations/{id} - Get a single backup destination
*
* @openapi
* summary: Fetch a single backup destination; decrypted cloud-credential env vars are only included for callers who can manage backups, and the password is always stripped
* description: Permission denial (403, "backups:view") is produced by the shared requireBackups route guard.
* path: id:integer! Backup destination id (from GET /api/backup/destinations)
* resp-200: The backup destination object (envVars included only for "backups:manage" callers; password always stripped)
* resp-400: Invalid id (not a number)
* resp-404: Destination not found
*/
export const GET: RequestHandler = async ({ params, cookies }) => {
const auth = await authorize(cookies);
const denied = await requireBackups(auth, 'view');
@@ -54,6 +65,21 @@ export const GET: RequestHandler = async ({ params, cookies }) => {
return json(prepareDestination(destination, canManage));
};
/**
* PUT /api/backup/destinations/{id} - Update a backup destination
*
* @openapi
* summary: Update a backup destination, re-validating repository and flags when supplied and re-registering maintenance schedules when policies change
* description: Permission denial (403, "backups:manage") is produced by the shared requireBackups route guard.
* path: id:integer! Backup destination id (from GET /api/backup/destinations)
* body: {name:string, repository:string, password:string, envVars:{}, flags:string, hostPath:string, policies:string}
* body-example: {"name":"S3 Offsite (renamed)","policies":"{\"pruneEnabled\":true,\"pruneSchedule\":\"0 0 1 * *\"}"}
* resp-200: The updated backup destination object (password stripped, envVars echoed back to the managing caller)
* resp-400: Invalid input — invalid id, unsupported/SSRF-blocked repository, invalid restic flags, invalid policy cron, or switching to a local repository used by a remote-environment config
* resp-404: Destination not found
* resp-409: A backup using this destination is currently running, or a destination with the new name already exists
* resp-500: Update failed (persistence error)
*/
export const PUT: RequestHandler = async (event) => {
const { params, request, cookies } = event;
const auth = await authorize(cookies);
@@ -140,6 +166,19 @@ export const PUT: RequestHandler = async (event) => {
}
};
/**
* DELETE /api/backup/destinations/{id} - Delete a backup destination
*
* @openapi
* summary: Delete a backup destination and unregister all its maintenance and dependent backup-config schedules
* description: Permission denial (403, "backups:manage") is produced by the shared requireBackups route guard.
* path: id:integer! Backup destination id (from GET /api/backup/destinations)
* resp-200: Returns { success: true } once the destination is deleted
* resp-200-example: {"success":true}
* resp-400: Invalid id (not a number)
* resp-404: Destination not found
* resp-409: A backup using this destination is currently running — try again once it finishes
*/
export const DELETE: RequestHandler = async (event) => {
const { params, cookies } = event;
const auth = await authorize(cookies);
@@ -6,6 +6,19 @@ import { auditBackupDestination } from '$lib/server/audit';
import { getBackupDestination, updateBackupDestinationTestStatus } from '$lib/server/db';
import { initRepository } from '$lib/server/backups';
/**
* POST /api/backup/destinations/{id}/init - Initialize the restic repository
*
* @openapi
* summary: Initialize the restic repository for a destination and record the resulting test status
* description: Permission denial (403, "backups:manage") is produced by the shared requireBackups route guard.
* path: id:integer! Backup destination id (from GET /api/backup/destinations)
* resp-200: Returns { success: true, message } when the repository is initialized
* resp-200-example: {"success":true,"message":"Repository initialized"}
* resp-400: Invalid id (not a number)
* resp-404: Destination not found
* resp-500: Repository initialization failed; returns { success: false, error }
*/
export const POST: RequestHandler = async (event) => {
const { params, cookies } = event;
const auth = await authorize(cookies);
@@ -9,14 +9,18 @@ import { rotateDestinationPassword } from '$lib/server/backups';
/**
* Rotate the restic repository password for a destination.
*
* Body: { currentPassword: string, newPassword: string }
*
* 200 — rotated, DB updated
* 400 — input invalid or current password wrong
* 404 — destination not found
* 409 — restic rotated but DB write failed (manual recovery needed; details
* include dbOutOfSync: true)
* 500 — restic call failed for an unrelated reason
* @openapi
* summary: Rotate the restic repository password for a destination and persist the new password
* description: Permission denial (403, "backups:manage") is produced by the shared requireBackups route guard.
* path: id:integer! Backup destination id (from GET /api/backup/destinations)
* body: {currentPassword:string!, newPassword:string!}
* body-example: {"currentPassword":"***","newPassword":"***"}
* resp-200: Returns { success: true } once the password is rotated and the database is updated
* resp-200-example: {"success":true}
* resp-400: Invalid input — invalid id, missing passwords, or the current password is incorrect
* resp-404: Destination not found
* resp-409: restic rotated the key but the database write failed (manual recovery needed; the response includes dbOutOfSync:true)
* resp-500: restic call failed for an unrelated reason
*/
export const POST: RequestHandler = async (event) => {
const { params, request, cookies } = event;
@@ -7,6 +7,20 @@ import { getBackupDestination, getBackupConfigs } from '$lib/server/db';
import { runRepoTask } from '$lib/server/backups';
import { createJobResponse } from '$lib/server/sse';
/**
* POST /api/backup/destinations/{id}/task - Run a repository maintenance task
*
* @openapi
* summary: Run a restic repository maintenance task (unlock, check, prune, stats, repair-index or repair-snapshots) against a destination
* description: Permission denial (403, "backups:manage") is produced by the shared requireBackups route guard. Streamed via createJobResponse — a task failure never surfaces as an HTTP 500; it comes back as a 200 with a failed job result (JSON-preferring callers) or a job/SSE 'result' event with success:false (streaming callers).
* path: id:integer! Backup destination id (from GET /api/backup/destinations)
* body: {task:string!}
* body-example: {"task":"check"}
* resp-200: The task result object — { success: true, ... } on success, or { success: false, error } when the task itself fails
* resp-400: Invalid input — invalid destination id, or an unknown task name (must be one of unlock, check, prune, stats, repair-index, repair-snapshots)
* resp-403: No access to an environment whose configs use this destination (enterprise)
* resp-404: Destination not found
*/
export const POST: RequestHandler = async (event) => {
const { params, request, cookies } = event;
const auth = await authorize(cookies);
@@ -8,6 +8,18 @@ import {
} from '$lib/server/db';
import { testRepository } from '$lib/server/backups';
/**
* POST /api/backup/destinations/{id}/test - Test a saved backup destination
*
* @openapi
* summary: Test connectivity to a saved backup destination's repository and update its stored test status
* description: Permission denial (403, "backups:manage") is produced by the shared requireBackups route guard.
* path: id:integer! Backup destination id (from GET /api/backup/destinations)
* resp-200: Test result — { success: true, status: "success" } when reachable, or { success: false, status: "needs_init" | "failed", error } otherwise
* resp-200-example: {"success":true,"status":"success"}
* resp-400: Invalid id (not a number)
* resp-404: Destination not found
*/
export const POST: RequestHandler = async ({ params, cookies }) => {
const auth = await authorize(cookies);
const denied = await requireBackups(auth, 'manage');
@@ -7,6 +7,18 @@ import { getBackupDestination } from '$lib/server/db';
import { verifyBackup } from '$lib/server/backups';
import { createJobResponse } from '$lib/server/sse';
/**
* POST /api/backup/destinations/{id}/verify - Verify backup integrity (streamed job)
*
* @openapi
* summary: Verify the integrity of a destination's backups by reading a data subset, streaming progress as a Server-Sent Events job
* description: Returns a text/event-stream that emits `progress` events and a final `result` event. Permission denial (403, "backups:manage") is produced by the shared requireBackups route guard.
* path: id:integer! Backup destination id (from GET /api/backup/destinations)
* body: {dataSubset:string}
* body-example: {"dataSubset":"5%"}
* resp-200: Server-Sent Events stream of progress and a final result event ({ success, ... })
* resp-400: Invalid destination id (not a number)
*/
export const POST: RequestHandler = async (event) => {
const { params, request, cookies } = event;
const auth = await authorize(cookies);
@@ -11,6 +11,16 @@ import { withGcsCredFile } from '$lib/server/backups/restic';
* Test a backup destination configuration.
* - With `destinationId`: uses saved credentials from DB
* - Without: uses inline credentials from request body (for testing before save)
*
* @openapi
* summary: Test a backup repository — either a saved destination (by destinationId) or inline credentials supplied for a not-yet-saved destination
* description: Provide `destinationId` to test a saved destination using its stored credentials, or omit it and supply `repository`/`password`/`envVars` to test before saving. Permission denial (403, "backups:manage") is produced by the shared requireBackups route guard. destinationId from GET /api/backup/destinations.
* body: {destinationId:integer, repository:string, password:string, envVars:{}}
* body-example: {"repository":"s3:s3.amazonaws.com/my-bucket/restic","password":"***","envVars":{"AWS_ACCESS_KEY_ID":"***","AWS_SECRET_ACCESS_KEY":"***"}}
* resp-200: Test result — { success: true } when the repository is reachable, or { success: false, needsInit?, error } otherwise
* resp-200-example: {"success":true}
* resp-400: Invalid input — missing repository/password for an inline test, or an unsupported/SSRF-blocked repository
* resp-404: Destination not found (when testing a saved destinationId)
*/
export const POST: RequestHandler = async ({ request, cookies }) => {
const auth = await authorize(cookies);
@@ -9,6 +9,12 @@ import { getInstanceId } from '$lib/server/backups/identity';
// abort the sync `restic snapshots` at ~15s) and headers don't survive polling. The
// client fetches the id here once and matches it against each snapshot's
// dockhand:instance= tag to tell own snapshots from foreign orphans (#1351).
/**
* @openapi
* summary: This install's stable backup instance id, used to tell own snapshots from foreign orphans
* description: Requires backups:view; 403 (no permission) and 404 (backup feature disabled) come from the shared requireBackups guard.
* resp-200: {instanceId:string!}
*/
export const GET: RequestHandler = async ({ cookies }) => {
const auth = await authorize(cookies);
const denied = await requireBackups(auth, 'view');
+12
View File
@@ -8,6 +8,18 @@ import { createJobResponse } from '$lib/server/sse';
import { auditRestore } from '$lib/server/audit';
import { guardSnapshotEnvAccess } from '$lib/server/backups/route-guards';
/**
* POST /api/backup/restore - Restore a snapshot (streamed job)
*
* @openapi
* summary: Restore a backup snapshot in-place or to a new location, streaming progress as a Server-Sent Events job
* description: Returns a text/event-stream that emits `progress` events and a final `result` event. An in-place restore is destructive and requires confirmOverwrite:true. Authorization and snapshot-environment access are checked before the request shape is validated. destinationId from GET /api/backup/destinations. snapshotId from GET /api/backup/snapshots. environmentId from GET /api/environments.
* body: {destinationId:integer!, snapshotId:string!, mode:string, targetType:string, volumes:array<string>, environmentId:integer, confirmOverwrite:boolean, targetPath:string, targetName:string, postRestore:string, volumeDestinations:array<{}>}
* body-example: {"destinationId":3,"snapshotId":"a1b2c3d4","mode":"new-location","targetType":"container","targetName":"nextcloud-restored","environmentId":1,"confirmOverwrite":false}
* resp-200: Server-Sent Events stream of progress and a final result event (status "success" or "error")
* resp-400: Invalid restore request; the response "issues" array lists the validation problems
* resp-403: Permission denied — requires "backups:manage", or no access to the target or snapshot-owning environment
*/
export const POST: RequestHandler = async (event) => {
const { request, cookies } = event;
const auth = await authorize(cookies);
@@ -7,6 +7,16 @@ import { validateSnapshotId } from '$lib/server/docker-validation';
import { guardSnapshotEnvAccess } from '$lib/server/backups/route-guards';
import { jobResult } from '$lib/server/sse';
/**
* @openapi
* summary: Preview what a restore would write (resolved targets + volume/stack contents)
* description: With includeTargets it also resolves the concrete restore targets (host paths / volumes); without it, returns the metadata-only preview the initial modal load relies on.
* body: {destinationId:integer!, snapshotId:string!, includeTargets:boolean, targetEnvId:integer}
* resp-200: object
* resp-200-desc: The snapshot preview, optionally with a resolved targets list
* resp-400: Missing required fields (destinationId, snapshotId)
* resp-403: Permission denied (needs backups:view) or access denied to the target environment
*/
export const POST: RequestHandler = async ({ request, cookies }) => {
const auth = await authorize(cookies);
const rbacDenied = await requireBackups(auth, 'manage');
@@ -11,6 +11,17 @@ import { isValidSnapshotId } from '$lib/server/backups/helpers';
* the helper container was never targeted by any stop path. Kills the restore
* helper for the given snapshotId (or all restore helpers if omitted) so restic
* exits and runRestore's failure/cleanup path runs.
*
* @openapi
* summary: Cancel a running restore — kills the restore helper for a given snapshotId, or all restore helpers when snapshotId is omitted
* description: snapshotId from GET /api/backup/snapshots. environmentId from GET /api/environments.
* body: {snapshotId:string, environmentId:integer}
* body-example: {"snapshotId":"a1b2c3d4"}
* resp-200: Returns { success: true, stopped } where "stopped" indicates whether a running restore helper was actually killed
* resp-200-example: {"success":true,"stopped":true}
* resp-400: Invalid snapshotId
* resp-403: Permission denied — requires "backups:manage", or no access to the targeted environment
* resp-500: Failed to cancel the restore (internal error)
*/
export const POST: RequestHandler = async (event) => {
const { request, cookies } = event;
@@ -7,6 +7,17 @@ import { listSnapshots } from '$lib/server/backups';
import { filterSnapshotsByEnvAccess } from '$lib/server/backups/route-guards';
import { jobResult } from '$lib/server/sse';
/**
* @openapi
* summary: List restic snapshots for a backup config or a single destination (job-polled)
* description: Pass configId to list across the config's destinations, or destinationId for one. Job-polled so a reverse proxy can't abort the sync restic snapshots read at ~15s; match each snapshot's instance tag against GET /api/backup/instance to tell own snapshots from foreign orphans.
* query: configId:integer Backup config id (mutually exclusive with destinationId)
* query: destinationId:integer Single destination id (mutually exclusive with configId)
* query: allDestinations:boolean With configId, include every destination of the config
* resp-400: Invalid or missing configId/destinationId
* resp-403: Permission denied (needs backups:view) or environment access denied
* resp-404: Backup config not found
*/
export const GET: RequestHandler = async ({ url, cookies, request }) => {
const auth = await authorize(cookies);
const denied = await requireBackups(auth, 'view');
@@ -8,6 +8,21 @@ import { getBackupDestination } from '$lib/server/db';
import { forgetSnapshot } from '$lib/server/backups';
import { guardSnapshotEnvAccess } from '$lib/server/backups/route-guards';
/**
* DELETE /api/backup/snapshots/{id} - Forget (delete) a snapshot
*
* @openapi
* summary: Forget and prune a single snapshot from a destination (destructive restic forget --prune), with a server-authoritative environment access gate
* path: id:string! Restic snapshot id to forget (from GET /api/backup/snapshots)
* query: destinationId:integer! Destination holding the snapshot (required) (from GET /api/backup/destinations)
* query: env:integer Optional environment id for an early enterprise access check; the authoritative gate resolves the snapshot's owning environment server-side (from GET /api/environments)
* resp-200: Returns { success: true } once the snapshot is forgotten and pruned
* resp-200-example: {"success":true}
* resp-400: Missing/invalid destinationId or an invalid snapshot id
* resp-403: Permission denied — requires "backups:manage", or no access to the snapshot's owning environment
* resp-404: Snapshot not found for this installation (not owned)
* resp-500: Failed to forget the snapshot (restic error)
*/
export const DELETE: RequestHandler = async (event) => {
const { params, url, cookies } = event;
const auth = await authorize(cookies);
@@ -7,6 +7,17 @@ import { browseSnapshot } from '$lib/server/backups';
import { guardSnapshotEnvAccess } from '$lib/server/backups/route-guards';
import { jobResult } from '$lib/server/sse';
/**
* @openapi
* summary: List directory entries at a path inside a snapshot (job-polled)
* description: Job-polled so a proxy can't abort the restic read at ~15s.
* path: id:string The restic snapshot id
* query: destinationId:integer Destination the snapshot lives in
* query: path:string Directory path inside the snapshot to list
* query: env:integer Environment context for access checks
* resp-400: Missing/invalid destinationId
* resp-403: Permission denied (needs backups:view) or environment access denied
*/
export const GET: RequestHandler = async ({ params, url, cookies, request }) => {
const auth = await authorize(cookies);
const denied = await requireBackups(auth, 'view');
@@ -8,6 +8,19 @@ import { guardSnapshotEnvAccess } from '$lib/server/backups/route-guards';
import { parseSnapshotLayout, redactSnapshotLayout } from '$lib/server/backups/snapshot-layout';
import { jobResult } from '$lib/server/sse';
/**
* @openapi
* summary: Read a single file (or archive) out of a snapshot (job-polled restic dump)
* description: Job-polled so a proxy can't abort the restic dump at ~15s. type selects the payload (inline preview vs archive); download requests a raw file/directory tar. A raw metadata.json download is refused (403) - it would bypass redaction.
* path: id:string The restic snapshot id
* query: destinationId:integer Destination the snapshot lives in
* query: path:string Path inside the snapshot (must resolve under /volumes or /metadata)
* query: type:string Payload kind (e.g. inline preview vs archive)
* query: download:string When set, stream the file/directory as a tar
* resp-400: Missing/invalid destinationId or path
* resp-403: Permission denied (needs backups:view), or a raw metadata.json download was refused
* resp-500: The restic dump failed
*/
export const GET: RequestHandler = async ({ params, url, cookies, request }) => {
const auth = await authorize(cookies);
const denied = await requireBackups(auth, 'view');
@@ -8,6 +8,14 @@ import { guardSnapshotEnvAccess } from '$lib/server/backups/route-guards';
import { redactSnapshotLayout } from '$lib/server/backups/snapshot-layout';
import { jobResult } from '$lib/server/sse';
/**
* @openapi
* summary: The snapshot's redacted metadata layout (job-polled restic dump)
* description: Job-polled so a proxy can't abort the restic dump at ~15s. The layout is redacted (stack secrets and container Config.Env/Labels stripped) before it leaves the server.
* path: id:string The restic snapshot id
* query: destinationId:integer Destination the snapshot lives in
* resp-400: Missing/invalid destinationId
*/
export const GET: RequestHandler = async ({ params, url, cookies, request }) => {
const auth = await authorize(cookies);
const denied = await requireBackups(auth, 'view');
@@ -7,6 +7,15 @@ import { guardSnapshotEnvAccess } from '$lib/server/backups/route-guards';
import { validateSnapshotId } from '$lib/server/docker-validation';
import { jobResult } from '$lib/server/sse';
/**
* @openapi
* summary: Diff two snapshots' file trees (added/changed/removed) - job-polled
* description: Job-polled so a proxy can't abort the restic read at ~15s.
* query: destinationId:integer Destination both snapshots live in
* query: snapshotA:string The baseline snapshot id
* query: snapshotB:string The snapshot to compare against the baseline
* resp-400: Missing destinationId, snapshotA, or snapshotB
*/
export const GET: RequestHandler = async ({ url, cookies, request }) => {
const auth = await authorize(cookies);
const denied = await requireBackups(auth, 'view');
@@ -9,6 +9,15 @@ import { probeStackDir } from '$lib/server/backups';
// create/edit backup dialog can show the resolved host path + let the user pick which
// stack-dir entries to back up. Returns { kind:'listed', hostPath, entries } or
// { kind:'unknown', reason } (never throws for an operational failure).
/**
* @openapi
* summary: Probe the target host and list the actual contents of a stack's directory, for the backup create/edit dialog's file picker
* query: target:string! Target stack name
* query: env:integer Environment ID the stack belongs to (from GET /api/environments)
* resp-200: The stack directory probe result — kind "listed"/"tar" with hostPath/localStackDir and entries, or kind "helper-failed"/"unknown" with a reason (never throws for an operational failure)
* resp-400: target is required
* resp-403: Permission denied (requires backups:view), or no access to the target's environment (enterprise)
*/
export const GET: RequestHandler = async ({ url, cookies }) => {
const auth = await authorize(cookies);
const denied = await requireBackups(auth, 'view');
@@ -8,6 +8,15 @@ import { previewStackBackupPath } from '$lib/server/backups';
// Preview WHERE a stack's directory will be captured from on the host (the resolved host
// bind path), or `unknown` with a reason - so the create-schedule dialog can show it up front
// and warn before scheduling a backup that would hard-fail the stack-dir probe.
/**
* @openapi
* summary: Preview where a stack's directory will be captured from on the host, without running a backup
* query: target:string! Target stack name
* query: env:integer Environment ID the stack belongs to (from GET /api/environments)
* resp-200: The resolved capture path preview — kind "candidate" with hostPath, kind "tar" with composeFile only, or kind "unknown" with a reason
* resp-400: target is required
* resp-403: Permission denied (requires backups:view), or no access to the target's environment (enterprise)
*/
export const GET: RequestHandler = async ({ url, cookies }) => {
const auth = await authorize(cookies);
const denied = await requireBackups(auth, 'view');