import { z } from "zod"; import { describeRoute, resolver } from "hono-openapi"; import { backupWebhooksSchema } from "@zerobyte/core/backup-hooks"; import { publicVolumeSchema } from "@zerobyte/contracts/volumes"; import { repositorySchema } from "../repositories/repositories.dto"; import { backupProgressEventSchema } from "~/schemas/events-dto"; const retentionPolicySchema = z.object({ keepLast: z.number().optional(), keepHourly: z.number().optional(), keepDaily: z.number().optional(), keepWeekly: z.number().optional(), keepMonthly: z.number().optional(), keepYearly: z.number().optional(), keepWithinDuration: z.string().optional(), }); const backupScheduleSchema = z.object({ id: z.number(), shortId: z.string(), name: z.string(), volumeId: z.number(), repositoryId: z.string(), enabled: z.boolean(), cronExpression: z.string(), retentionPolicy: retentionPolicySchema.nullable(), excludePatterns: z.array(z.string()).nullable(), excludeIfPresent: z.array(z.string()).nullable(), includePaths: z.array(z.string()).nullable(), includePatterns: z.array(z.string()).nullable(), oneFileSystem: z.boolean(), customResticParams: z.array(z.string()).nullable(), backupWebhooks: backupWebhooksSchema.nullable(), maxRetries: z.number(), retryDelay: z.number().transform((ms) => Math.round(ms / 60000)), lastBackupAt: z.number().nullable(), lastBackupStatus: z.enum(["success", "error", "in_progress", "warning"]).nullable(), lastBackupError: z.string().nullable(), nextBackupAt: z.number().nullable(), createdAt: z.number(), updatedAt: z.number(), volume: publicVolumeSchema, repository: repositorySchema, }); const scheduleMirrorSchema = z.object({ scheduleId: z.string(), repositoryId: z.string(), enabled: z.boolean(), lastCopyAt: z.number().nullable(), lastCopyStatus: z.enum(["success", "error", "in_progress"]).nullable(), lastCopyError: z.string().nullable(), createdAt: z.number(), repository: repositorySchema, }); export const listBackupSchedulesResponse = backupScheduleSchema.array(); export type ListBackupSchedulesResponseDto = z.infer; export const listBackupSchedulesDto = describeRoute({ description: "List all backup schedules", tags: ["Backups"], operationId: "listBackupSchedules", responses: { 200: { description: "List of backup schedules", content: { "application/json": { schema: resolver(listBackupSchedulesResponse), }, }, }, }, }); export const getBackupScheduleResponse = backupScheduleSchema; export type GetBackupScheduleDto = z.infer; export const getBackupScheduleDto = describeRoute({ description: "Get a backup schedule by ID", tags: ["Backups"], operationId: "getBackupSchedule", responses: { 200: { description: "Backup schedule details", content: { "application/json": { schema: resolver(getBackupScheduleResponse), }, }, }, }, }); export const getBackupScheduleForVolumeResponse = backupScheduleSchema.nullable(); export type GetBackupScheduleForVolumeResponseDto = z.infer; export const getBackupScheduleForVolumeDto = describeRoute({ description: "Get a backup schedule for a specific volume", tags: ["Backups"], operationId: "getBackupScheduleForVolume", responses: { 200: { description: "Backup schedule details for the volume", content: { "application/json": { schema: resolver(getBackupScheduleForVolumeResponse), }, }, }, }, }); export const createBackupScheduleBody = z.object({ name: z.string().min(1).max(128), volumeId: z.union([z.string(), z.number()]), repositoryId: z.string(), enabled: z.boolean(), cronExpression: z.string(), retentionPolicy: retentionPolicySchema.optional(), excludePatterns: z.array(z.string()).optional(), excludeIfPresent: z.array(z.string()).optional(), includePaths: z.array(z.string()).optional(), includePatterns: z.array(z.string()).optional(), oneFileSystem: z.boolean().optional(), tags: z.array(z.string()).optional(), customResticParams: z.array(z.string()).optional(), backupWebhooks: backupWebhooksSchema.nullable().optional(), maxRetries: z.number().min(0).max(32).optional().default(2), retryDelay: z .number() .min(1) .max(1440) .optional() .default(15) .transform((minutes) => minutes * 60000), }); export type CreateBackupScheduleBody = z.infer; export const createBackupScheduleResponse = backupScheduleSchema.omit({ volume: true, repository: true }); export type CreateBackupScheduleDto = z.infer; export const createBackupScheduleDto = describeRoute({ description: "Create a new backup schedule for a volume", operationId: "createBackupSchedule", tags: ["Backups"], responses: { 201: { description: "Backup schedule created successfully", content: { "application/json": { schema: resolver(createBackupScheduleResponse), }, }, }, }, }); export const updateBackupScheduleBody = z.object({ name: z.string().min(1).max(128).optional(), repositoryId: z.string(), enabled: z.boolean().optional(), cronExpression: z.string(), retentionPolicy: retentionPolicySchema.optional(), excludePatterns: z.array(z.string()).optional(), excludeIfPresent: z.array(z.string()).optional(), includePaths: z.array(z.string()).optional(), includePatterns: z.array(z.string()).optional(), oneFileSystem: z.boolean().optional(), tags: z.array(z.string()).optional(), customResticParams: z.array(z.string()).optional(), backupWebhooks: backupWebhooksSchema.nullable().optional(), maxRetries: z.number().min(0).max(32).optional().default(2), retryDelay: z .number() .min(1) .max(1440) .optional() .default(15) .transform((minutes) => minutes * 60000), }); export type UpdateBackupScheduleBody = z.infer; export const updateBackupScheduleResponse = backupScheduleSchema.omit({ volume: true, repository: true }); export type UpdateBackupScheduleDto = z.infer; export const updateBackupScheduleDto = describeRoute({ description: "Update a backup schedule", operationId: "updateBackupSchedule", tags: ["Backups"], responses: { 200: { description: "Backup schedule updated successfully", content: { "application/json": { schema: resolver(updateBackupScheduleResponse), }, }, }, }, }); const deleteBackupScheduleResponse = z.object({ success: z.boolean(), }); export type DeleteBackupScheduleDto = z.infer; export const deleteBackupScheduleDto = describeRoute({ description: "Delete a backup schedule", operationId: "deleteBackupSchedule", tags: ["Backups"], responses: { 200: { description: "Backup schedule deleted successfully", content: { "application/json": { schema: resolver(deleteBackupScheduleResponse), }, }, }, }, }); const runBackupNowResponse = z.object({ success: z.boolean(), }); export type RunBackupNowDto = z.infer; export const runBackupNowDto = describeRoute({ description: "Trigger a backup immediately for a schedule", operationId: "runBackupNow", tags: ["Backups"], responses: { 200: { description: "Backup started successfully", content: { "application/json": { schema: resolver(runBackupNowResponse), }, }, }, }, }); const stopBackupResponse = z.object({ success: z.boolean(), }); export type StopBackupDto = z.infer; export const stopBackupDto = describeRoute({ description: "Stop a backup that is currently in progress", operationId: "stopBackup", tags: ["Backups"], responses: { 200: { description: "Backup stopped successfully", content: { "application/json": { schema: resolver(stopBackupResponse), }, }, }, 409: { description: "No backup is currently running for this schedule", }, }, }); const runForgetResponse = z.object({ success: z.boolean(), }); export type RunForgetDto = z.infer; export const runForgetDto = describeRoute({ description: "Manually apply retention policy to clean up old snapshots", operationId: "runForget", tags: ["Backups"], responses: { 200: { description: "Retention policy applied successfully", content: { "application/json": { schema: resolver(runForgetResponse), }, }, }, }, }); const getScheduleMirrorsResponse = scheduleMirrorSchema.array(); export type GetScheduleMirrorsDto = z.infer; export const getScheduleMirrorsDto = describeRoute({ description: "Get mirror repository assignments for a backup schedule", operationId: "getScheduleMirrors", tags: ["Backups"], responses: { 200: { description: "List of mirror repository assignments for the schedule", content: { "application/json": { schema: resolver(getScheduleMirrorsResponse), }, }, }, }, }); export const updateScheduleMirrorsBody = z.object({ mirrors: z .object({ repositoryId: z.string(), enabled: z.boolean(), }) .array(), }); export type UpdateScheduleMirrorsBody = z.infer; const updateScheduleMirrorsResponse = scheduleMirrorSchema.array(); export type UpdateScheduleMirrorsDto = z.infer; export const updateScheduleMirrorsDto = describeRoute({ description: "Update mirror repository assignments for a backup schedule", operationId: "updateScheduleMirrors", tags: ["Backups"], responses: { 200: { description: "Mirror assignments updated successfully", content: { "application/json": { schema: resolver(updateScheduleMirrorsResponse), }, }, }, }, }); const mirrorCompatibilitySchema = z.object({ repositoryId: z.string(), compatible: z.boolean(), reason: z.string().nullable(), }); const getMirrorCompatibilityResponse = mirrorCompatibilitySchema.array(); export type GetMirrorCompatibilityDto = z.infer; export const getMirrorCompatibilityDto = describeRoute({ description: "Get mirror compatibility info for all repositories relative to a backup schedule's primary repository", operationId: "getMirrorCompatibility", tags: ["Backups"], responses: { 200: { description: "List of repositories with their mirror compatibility status", content: { "application/json": { schema: resolver(getMirrorCompatibilityResponse), }, }, }, }, }); export const reorderBackupSchedulesBody = z.object({ scheduleShortIds: z.array(z.string()), }); const reorderBackupSchedulesResponse = z.object({ success: z.boolean(), }); export type ReorderBackupSchedulesDto = z.infer; export const reorderBackupSchedulesDto = describeRoute({ description: "Reorder backup schedules by providing an array of schedule short IDs in the desired order", operationId: "reorderBackupSchedules", tags: ["Backups"], responses: { 200: { description: "Backup schedules reordered successfully", content: { "application/json": { schema: resolver(reorderBackupSchedulesResponse), }, }, }, }, }); const missingSnapshotSchema = z.object({ short_id: z.string(), time: z.string(), size: z.number(), }); const getMirrorSyncStatusResponse = z.object({ sourceCount: z.number(), mirrorCount: z.number(), missingSnapshots: missingSnapshotSchema.array(), }); export type GetMirrorSyncStatusDto = z.infer; export const getMirrorSyncStatusDto = describeRoute({ description: "Get sync status for a specific mirror, including missing snapshots", operationId: "getMirrorSyncStatus", tags: ["Backups"], responses: { 200: { description: "Mirror sync status with missing snapshots", content: { "application/json": { schema: resolver(getMirrorSyncStatusResponse), }, }, }, }, }); export const syncMirrorBody = z.object({ snapshotIds: z.array(z.string()).optional(), }); const syncMirrorResponse = z.object({ success: z.boolean(), }); export type SyncMirrorDto = z.infer; export const syncMirrorDto = describeRoute({ description: "Sync selected snapshots to a specific mirror repository", operationId: "syncMirror", tags: ["Backups"], responses: { 200: { description: "Mirror sync started successfully", content: { "application/json": { schema: resolver(syncMirrorResponse), }, }, }, 409: { description: "Mirror is already syncing", }, }, }); const getBackupProgressResponse = backupProgressEventSchema.nullable(); export type GetBackupProgressDto = z.infer; export const getBackupProgressDto = describeRoute({ description: "Get the last known progress for a currently running backup. Returns null if no progress has been reported yet.", tags: ["Backup Schedules"], operationId: "getBackupProgress", responses: { 200: { description: "Current backup progress or null if not yet available", content: { "application/json": { schema: resolver(getBackupProgressResponse), }, }, }, }, });