zerobyte/app/server/modules/repositories/repositories.dto.ts
Nico d479bfaddc
feat: snapshot restores through rpc (#930)
* feat: snapshot restores through rpc

* fix(restore): do not wait for lock before returning response

* chore: fix liniting issue
2026-05-31 21:48:32 +02:00

593 lines
14 KiB
TypeScript

import { z } from "zod";
import { describeRoute, resolver } from "hono-openapi";
import {
COMPRESSION_MODES,
OVERWRITE_MODES,
REPOSITORY_BACKENDS,
REPOSITORY_STATUS,
repositoryConfigSchema,
doctorResultSchema,
resticSnapshotSummarySchema,
resticStatsSchema,
} from "@zerobyte/core/restic";
export const repositorySchema = z.object({
id: z.string(),
shortId: z.string(),
provisioningId: z.string().nullable(),
name: z.string(),
type: z.enum(REPOSITORY_BACKENDS),
config: repositoryConfigSchema,
compressionMode: z.enum(COMPRESSION_MODES).nullable(),
status: z.enum(REPOSITORY_STATUS).nullable(),
lastChecked: z.number().nullable(),
lastError: z.string().nullable(),
doctorResult: doctorResultSchema.nullable(),
createdAt: z.number(),
updatedAt: z.number(),
});
const listRepositoriesResponse = repositorySchema.array();
export type ListRepositoriesDto = z.infer<typeof listRepositoriesResponse>;
export const listRepositoriesDto = describeRoute({
description: "List all repositories",
tags: ["Repositories"],
operationId: "listRepositories",
responses: {
200: {
description: "List of repositories",
content: {
"application/json": {
schema: resolver(listRepositoriesResponse),
},
},
},
},
});
export const createRepositoryBody = z.object({
name: z.string(),
compressionMode: z.enum(COMPRESSION_MODES).optional(),
config: repositoryConfigSchema,
});
const createRepositoryResponse = z.object({
message: z.string(),
repository: z.object({
id: z.string(),
shortId: z.string(),
name: z.string(),
}),
});
export const createRepositoryDto = describeRoute({
description: "Create a new restic repository",
operationId: "createRepository",
tags: ["Repositories"],
responses: {
201: {
description: "Repository created successfully",
content: {
"application/json": {
schema: resolver(createRepositoryResponse),
},
},
},
},
});
const getRepositoryResponse = repositorySchema;
export type GetRepositoryDto = z.infer<typeof getRepositoryResponse>;
export const getRepositoryDto = describeRoute({
description: "Get a single repository by ID",
tags: ["Repositories"],
operationId: "getRepository",
responses: {
200: {
description: "Repository details",
content: {
"application/json": {
schema: resolver(getRepositoryResponse),
},
},
},
},
});
const repositoryStatsSchema = resticStatsSchema;
const getRepositoryStatsResponse = repositoryStatsSchema;
export type GetRepositoryStatsDto = z.infer<typeof getRepositoryStatsResponse>;
export const getRepositoryStatsDto = describeRoute({
description: "Get repository storage and compression statistics",
tags: ["Repositories"],
operationId: "getRepositoryStats",
responses: {
200: {
description: "Repository statistics",
content: {
"application/json": {
schema: resolver(getRepositoryStatsResponse),
},
},
},
},
});
const refreshRepositoryStatsResponse = repositoryStatsSchema;
export type RefreshRepositoryStatsDto = z.infer<typeof refreshRepositoryStatsResponse>;
export const refreshRepositoryStatsDto = describeRoute({
description: "Refresh repository storage and compression statistics",
tags: ["Repositories"],
operationId: "refreshRepositoryStats",
responses: {
200: {
description: "Refreshed repository statistics",
content: {
"application/json": {
schema: resolver(refreshRepositoryStatsResponse),
},
},
},
},
});
const deleteRepositoryResponse = z.object({
message: z.string(),
});
export type DeleteRepositoryDto = z.infer<typeof deleteRepositoryResponse>;
export const deleteRepositoryDto = describeRoute({
description: "Delete a repository",
tags: ["Repositories"],
operationId: "deleteRepository",
responses: {
200: {
description: "Repository deleted successfully",
content: {
"application/json": {
schema: resolver(deleteRepositoryResponse),
},
},
},
},
});
export const updateRepositoryBody = z.object({
name: z.string().optional(),
compressionMode: z.enum(COMPRESSION_MODES).optional(),
config: repositoryConfigSchema.optional(),
});
export type UpdateRepositoryBody = z.infer<typeof updateRepositoryBody>;
const updateRepositoryResponse = repositorySchema;
export type UpdateRepositoryDto = z.infer<typeof updateRepositoryResponse>;
export const updateRepositoryDto = describeRoute({
description: "Update a repository's name or settings",
tags: ["Repositories"],
operationId: "updateRepository",
responses: {
200: {
description: "Repository updated successfully",
content: {
"application/json": {
schema: resolver(updateRepositoryResponse),
},
},
},
400: {
description: "Invalid repository update payload",
},
404: {
description: "Repository not found",
},
409: {
description: "Repository with this name already exists",
},
},
});
const snapshotSchema = z.object({
short_id: z.string(),
time: z.number(),
paths: z.array(z.string()),
size: z.number(),
duration: z.number(),
tags: z.array(z.string()),
retentionCategories: z.array(z.string()),
hostname: z.string().optional(),
summary: resticSnapshotSummarySchema.optional(),
});
const listSnapshotsResponse = snapshotSchema.array();
export type ListSnapshotsDto = z.infer<typeof listSnapshotsResponse>;
export const listSnapshotsFilters = z.object({
backupId: z.string().optional(),
});
export const listSnapshotsDto = describeRoute({
description: "List all snapshots in a repository",
tags: ["Repositories"],
operationId: "listSnapshots",
responses: {
200: {
description: "List of snapshots",
content: {
"application/json": {
schema: resolver(listSnapshotsResponse),
},
},
},
},
});
const getSnapshotDetailsResponse = snapshotSchema;
export type GetSnapshotDetailsDto = z.infer<typeof getSnapshotDetailsResponse>;
export const getSnapshotDetailsDto = describeRoute({
description: "Get details of a specific snapshot",
tags: ["Repositories"],
operationId: "getSnapshotDetails",
responses: {
200: {
description: "Snapshot details",
content: {
"application/json": {
schema: resolver(getSnapshotDetailsResponse),
},
},
},
},
});
const snapshotFileNodeSchema = z.object({
name: z.string(),
type: z.string(),
path: z.string(),
uid: z.number().optional(),
gid: z.number().optional(),
size: z.number().optional(),
mode: z.number().optional(),
mtime: z.string().optional(),
atime: z.string().optional(),
ctime: z.string().optional(),
});
const listSnapshotFilesResponse = z.object({
snapshot: z.object({
id: z.string(),
short_id: z.string(),
time: z.string(),
hostname: z.string().optional(),
paths: z.array(z.string()),
}),
files: snapshotFileNodeSchema.array(),
offset: z.number(),
limit: z.number(),
total: z.number(),
hasMore: z.boolean(),
});
export type ListSnapshotFilesDto = z.infer<typeof listSnapshotFilesResponse>;
export const listSnapshotFilesQuery = z.object({
path: z.string().optional(),
offset: z.coerce.number().int().optional(),
limit: z.coerce.number().int().optional(),
});
export const listSnapshotFilesDto = describeRoute({
description: "List files and directories in a snapshot",
tags: ["Repositories"],
operationId: "listSnapshotFiles",
responses: {
200: {
description: "List of files and directories in the snapshot",
content: {
"application/json": {
schema: resolver(listSnapshotFilesResponse),
},
},
},
},
});
const DUMP_PATH_KINDS = {
file: "file",
dir: "dir",
} as const;
const dumpPathKindSchema = z.enum(DUMP_PATH_KINDS);
export type DumpPathKind = z.infer<typeof dumpPathKindSchema>;
export const dumpSnapshotQuery = z.object({
path: z.string().optional(),
kind: dumpPathKindSchema.optional(),
});
export const dumpSnapshotDto = describeRoute({
description: "Download a snapshot path as a tar archive (folders) or raw file stream (single files)",
tags: ["Repositories"],
operationId: "dumpSnapshot",
responses: {
200: {
description: "Snapshot content stream",
content: {
"application/x-tar": {
schema: { type: "string", format: "binary" },
},
"application/octet-stream": {
schema: { type: "string", format: "binary" },
},
},
},
},
});
const overwriteModeSchema = z.enum(OVERWRITE_MODES);
export const restoreSnapshotBody = z.object({
snapshotId: z.string(),
include: z.array(z.string()).optional(),
selectedItemKind: dumpPathKindSchema.optional(),
exclude: z.array(z.string()).optional(),
excludeXattr: z.array(z.string()).optional(),
delete: z.boolean().optional(),
targetPath: z.string().optional(),
targetAgentId: z.string().optional(),
overwrite: overwriteModeSchema.optional(),
});
const restoreSnapshotResponse = z.object({
restoreId: z.string(),
status: z.literal("started"),
});
export type RestoreSnapshotDto = z.infer<typeof restoreSnapshotResponse>;
export const restoreSnapshotDto = describeRoute({
description: "Restore a snapshot to a target path on the filesystem",
tags: ["Repositories"],
operationId: "restoreSnapshot",
responses: {
202: {
description: "Snapshot restore started",
content: {
"application/json": {
schema: resolver(restoreSnapshotResponse),
},
},
},
},
});
const startDoctorResponse = z.object({
message: z.string(),
repositoryId: z.string(),
});
export type StartDoctorDto = z.infer<typeof startDoctorResponse>;
export const startDoctorDto = describeRoute({
description:
"Start an asynchronous doctor operation on a repository to fix common issues (unlock, check, repair index). The operation runs in the background and sends results via SSE events.",
tags: ["Repositories"],
operationId: "startDoctor",
responses: {
202: {
description: "Doctor operation started",
content: {
"application/json": {
schema: resolver(startDoctorResponse),
},
},
},
409: {
description: "Doctor operation already in progress",
},
},
});
const cancelDoctorResponse = z.object({
message: z.string(),
});
export type CancelDoctorDto = z.infer<typeof cancelDoctorResponse>;
export const cancelDoctorDto = describeRoute({
description: "Cancel a running doctor operation on a repository",
tags: ["Repositories"],
operationId: "cancelDoctor",
responses: {
200: {
description: "Doctor operation cancelled",
content: {
"application/json": {
schema: resolver(cancelDoctorResponse),
},
},
},
409: {
description: "No doctor operation is currently running",
},
},
});
const rcloneRemoteSchema = z.object({
name: z.string(),
type: z.string(),
});
const listRcloneRemotesResponse = rcloneRemoteSchema.array();
export const listRcloneRemotesDto = describeRoute({
description: "List all configured rclone remotes on the host system",
tags: ["Rclone"],
operationId: "listRcloneRemotes",
responses: {
200: {
description: "List of rclone remotes",
content: {
"application/json": {
schema: resolver(listRcloneRemotesResponse),
},
},
},
},
});
const deleteSnapshotResponse = z.object({
message: z.string(),
});
export type DeleteSnapshotDto = z.infer<typeof deleteSnapshotResponse>;
export const deleteSnapshotDto = describeRoute({
description: "Delete a specific snapshot from a repository",
tags: ["Repositories"],
operationId: "deleteSnapshot",
responses: {
200: {
description: "Snapshot deleted successfully",
content: {
"application/json": {
schema: resolver(deleteSnapshotResponse),
},
},
},
},
});
export const deleteSnapshotsBody = z.object({
snapshotIds: z.array(z.string()).min(1),
});
const deleteSnapshotsResponse = z.object({
message: z.string(),
});
export type DeleteSnapshotsResponseDto = z.infer<typeof deleteSnapshotsResponse>;
export const deleteSnapshotsDto = describeRoute({
description: "Delete multiple snapshots from a repository",
tags: ["Repositories"],
operationId: "deleteSnapshots",
responses: {
200: {
description: "Snapshots deleted successfully",
content: {
"application/json": {
schema: resolver(deleteSnapshotsResponse),
},
},
},
},
});
export const tagSnapshotsBody = z.object({
snapshotIds: z.array(z.string()).min(1),
add: z.array(z.string()).optional(),
remove: z.array(z.string()).optional(),
set: z.array(z.string()).optional(),
});
const tagSnapshotsResponse = z.object({
message: z.string(),
});
export type TagSnapshotsResponseDto = z.infer<typeof tagSnapshotsResponse>;
export const tagSnapshotsDto = describeRoute({
description: "Tag multiple snapshots in a repository",
tags: ["Repositories"],
operationId: "tagSnapshots",
responses: {
200: {
description: "Snapshots tagged successfully",
content: {
"application/json": {
schema: resolver(tagSnapshotsResponse),
},
},
},
},
});
const refreshSnapshotsResponse = z.object({
message: z.string(),
count: z.number(),
});
export type RefreshSnapshotsDto = z.infer<typeof refreshSnapshotsResponse>;
export const refreshSnapshotsDto = describeRoute({
description: "Clear snapshot cache and force refresh from repository",
tags: ["Repositories"],
operationId: "refreshSnapshots",
responses: {
200: {
description: "Snapshot cache cleared and refreshed",
content: {
"application/json": {
schema: resolver(refreshSnapshotsResponse),
},
},
},
},
});
export const devPanelExecBody = z.object({
command: z.string(),
args: z.array(z.string()).optional(),
});
export const devPanelExecDto = describeRoute({
description: "Execute a restic command against a repository (dev panel only)",
tags: ["Repositories"],
operationId: "devPanelExec",
responses: {
200: {
description: "Command output stream (SSE)",
content: {
"text/event-stream": {
schema: { type: "string" },
},
},
},
403: {
description: "Dev panel not enabled",
},
},
});
const unlockRepositoryResponse = z.object({
success: z.boolean(),
message: z.string(),
});
export type UnlockRepositoryDto = z.infer<typeof unlockRepositoryResponse>;
export const unlockRepositoryDto = describeRoute({
description: "Unlock a repository by removing all stale locks",
tags: ["Repositories"],
operationId: "unlockRepository",
responses: {
200: {
description: "Repository unlocked successfully",
content: {
"application/json": {
schema: resolver(unlockRepositoryResponse),
},
},
},
},
});