Files
MikrotikManager/packages/contracts/src/backups.ts
T

126 lines
6.7 KiB
TypeScript

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<typeof backupScheduleSettingsDtoSchema>
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<typeof backupStorageSettingsDtoSchema>
export type PutBackupStorageSettings = z.infer<typeof putBackupStorageSettingsSchema>
export type BackupItemDto = z.infer<typeof backupItemDtoSchema>
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<typeof createBackupJobResponseSchema>
export type BackupJobStatusResponse = z.infer<typeof backupJobResponseSchema>
export type CreateBackupRequest = z.infer<typeof createBackupRequestSchema>