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; 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; 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; 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; 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; 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; const updateRepositoryResponse = repositorySchema; export type UpdateRepositoryDto = z.infer; 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; 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; 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; 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; 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; 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; 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; 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; 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; 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; 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; 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; 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), }, }, }, }, });