import { z } from "zod" export const backupFrequencySchema = z.enum(["daily", "weekly", "monthly"]) export const backupFormatSchema = z.enum(["rsc", "backup"]) export const backupStorageProviderSchema = z.enum(["local", "s3"]) export const backupObjectStorageSchema = z.enum(["local", "s3", "both"]) export const backupScheduleSettingsDtoSchema = z.object({ enabled: z.boolean(), frequency: backupFrequencySchema, hour: z.number().int().min(0).max(23), minute: z.number().int().min(0).max(59), weekDay: z.number().int().min(0).max(6), monthDay: z.number().int().min(1).max(28), keepCount: z.number().int().min(1).max(365), format: backupFormatSchema, serverIds: z.array(z.string()), lastRunAt: z.string().nullable().optional(), lastDurationMs: z.number().int().nullable().optional(), lastError: z.string().nullable().optional(), updatedAt: z.string().optional(), }) export const putBackupScheduleSettingsSchema = z.object({ enabled: z.boolean().optional(), frequency: backupFrequencySchema.optional(), hour: z.number().int().min(0).max(23).optional(), minute: z.number().int().min(0).max(59).optional(), weekDay: z.number().int().min(0).max(6).optional(), monthDay: z.number().int().min(1).max(28).optional(), keepCount: z.number().int().min(1).max(365).optional(), format: backupFormatSchema.optional(), serverIds: z.array(z.string()).optional(), }) export type BackupScheduleSettingsDto = z.infer export const backupStorageSettingsDtoSchema = z.object({ provider: backupStorageProviderSchema, s3Endpoint: z.string(), s3Region: z.string(), s3Bucket: z.string(), s3Prefix: z.string(), s3AccessKeyId: z.string(), secretConfigured: z.boolean(), s3ForcePathStyle: z.boolean(), keepLocalCopy: z.boolean(), lastTestAt: z.string().nullable().optional(), lastTestError: z.string().nullable().optional(), updatedAt: z.string().optional(), }) export const putBackupStorageSettingsSchema = z.object({ provider: backupStorageProviderSchema.optional(), s3Endpoint: z.string().optional(), s3Region: z.string().optional(), s3Bucket: z.string().optional(), s3Prefix: z.string().optional(), s3AccessKeyId: z.string().optional(), s3SecretAccessKey: z.string().optional(), s3ForcePathStyle: z.boolean().optional(), keepLocalCopy: z.boolean().optional(), }) export const backupItemDtoSchema = z.object({ id: z.string().min(1), serverId: z.number().int().positive().safe().nullable(), serverName: z.string(), filename: z.string().min(1), sizeBytes: z.number().int().nonnegative().safe(), createdAt: z.string().datetime({ offset: true }), kind: z.enum(["manual", "auto"]), notes: z.string().optional(), storage: backupObjectStorageSchema, s3Key: z.string().nullable().optional(), uploadError: z.string().nullable().optional(), }) export type BackupStorageSettingsDto = z.infer export type PutBackupStorageSettings = z.infer export type BackupItemDto = z.infer const backupServerIdSchema = z.union([ z.string().regex(/^[1-9][0-9]*$/).refine(value => Number.isSafeInteger(Number(value))), z.number().int().positive().safe(), ]) export const createBackupRequestSchema = z.object({ requestId: z.string().uuid().optional(), serverIds: z.array(backupServerIdSchema).min(1).refine(ids => new Set(ids.map(String)).size === ids.length, "Дубли серверов"), notes: z.string().trim().max(500).optional(), }) export const backupJobStatusSchema = z.enum(["queued", "running", "done", "failed"]) const backupProgressSchema = z.object({ status: backupJobStatusSchema, total: z.number().int().positive().safe(), completed: z.number().int().nonnegative().safe(), }) export const createBackupJobResponseSchema = backupProgressSchema.extend({ jobId: z.string().min(1), serverIds: z.array(z.string().min(1)).min(1) }).superRefine((job, ctx) => { if (job.serverIds.length !== job.total || new Set(job.serverIds).size !== job.total) ctx.addIssue({ code: "custom", message: "Цели не соответствуют числу устройств" }) if (job.completed > job.total || (job.status === "done" && job.completed !== job.total)) ctx.addIssue({ code: "custom", message: "Некорректный прогресс задачи" }) }) export const backupJobResponseSchema = backupProgressSchema.extend({ id: z.string().min(1), serverIds: z.array(z.string().regex(/^[1-9][0-9]*$/)).min(1), notes: z.string().optional(), requestedAt: z.string().datetime({ offset: true }), startedAt: z.string().datetime({ offset: true }).optional(), finishedAt: z.string().datetime({ offset: true }).optional(), created: z.array(backupItemDtoSchema), failures: z.array(z.object({ serverId: z.string().min(1), error: z.string().min(1) })), }).superRefine((job, ctx) => { if (job.completed > job.total || (job.status === "done" && job.completed !== job.total)) ctx.addIssue({ code: "custom", message: "Некорректный прогресс задачи" }) if (["done", "failed"].includes(job.status) && !job.finishedAt) ctx.addIssue({ code: "custom", message: "Завершение не подтверждено временем" }) if (job.created.some(item => item.serverId === null || !job.serverIds.includes(String(item.serverId))) || job.failures.some(failure => failure.serverId !== "job" && !job.serverIds.includes(failure.serverId))) ctx.addIssue({ code: "custom", message: "Результат относится к другому устройству" }) const deviceFailures = job.failures.filter(failure => failure.serverId !== "job") if (job.created.length + deviceFailures.length !== job.completed) ctx.addIssue({ code: "custom", message: "Результаты не соответствуют прогрессу" }) if (new Set(job.created.map(item => item.id)).size !== job.created.length) ctx.addIssue({ code: "custom", message: "Дубли резервных копий" }) }) export const backupsListResponseSchema = z.array(backupItemDtoSchema).refine(items => new Set(items.map(item => item.id)).size === items.length, "Дубли резервных копий") export const restoreBackupResponseSchema = z.object({ backupId: z.string().min(1), serverId: z.number().int().positive().safe(), filename: z.string().min(1), serverName: z.string().min(1) }) export const syncBackupsResponseSchema = z.object({ imported: z.number().int().nonnegative().safe(), skipped: z.number().int().nonnegative().safe() }) export type CreateBackupJobResponse = z.infer export type BackupJobStatusResponse = z.infer export type CreateBackupRequest = z.infer