import {z} from "zod" import {ApiClientError,requestBlob,requestJson} from "@/shared/api/http-client" const MAX_RESTORE_BYTES=512*1024*1024 const magic=[80,71,68,77,80] const receiptSchema=z.object({ok:z.literal(true),eventError:z.string().min(1).optional(),listenerError:z.string().min(1).optional(),schedulerError:z.string().min(1).optional(),cleanupError:z.string().min(1).optional()}).strict() export type SystemDatabaseRestoreReceipt=z.output const warningSchema=receiptSchema.omit({ok:true}) export type SystemDatabaseWarnings=z.output type Options={signal?:AbortSignal;onDispatch?:()=>void} const signal=(options?:Options)=>options?.signal?AbortSignal.any([options.signal,AbortSignal.timeout(600_000)]):AbortSignal.timeout(600_000) function parseFilename(value:string|null):string{ let name="mikrotik-manager.dump" if(value){ const utf=/filename\*=UTF-8''([^;]+)/i.exec(value) const plain=/filename=(?:"([^"]+)"|([^;]+))/i.exec(value) try{name=utf?.[1]?decodeURIComponent(utf[1]):(plain?.[1]??plain?.[2]??name).trim()}catch{throw new Error("Имя файла бэкапа не подтверждено")} } if(!name||/[\\/\r\n\x00]/.test(name)||name==="."||name==="..")throw new Error("Имя файла бэкапа не подтверждено") return name } async function confirmDump(blob:Blob):Promise{ if(blob.sizehead[index]===value))throw new Error("Файл не похож на резервную копию PostgreSQL (PGDMP)") } export async function downloadSystemDatabaseBackup(baseUrl:string,options?:Options):Promise<{blob:Blob;filename:string;warnings:SystemDatabaseWarnings}>{ const activeSignal=signal(options) const response=await requestBlob(baseUrl,"/api/system/database/backup",{signal:activeSignal}) const blob=await response.blob() await confirmDump(blob);activeSignal.throwIfAborted() const rawWarnings=response.headers.get("X-System-Database-Outcome") let warnings:SystemDatabaseWarnings={} if(rawWarnings){ try{warnings=warningSchema.parse(JSON.parse(decodeURIComponent(rawWarnings)))}catch{warnings={eventError:"Бэкап подготовлен, но дополнительные результаты не подтверждены"}} } return {blob,filename:parseFilename(response.headers.get("Content-Disposition")),warnings} } export async function restoreSystemDatabaseBackup(baseUrl:string,file:File,options?:Options):Promise{ if(file.size>MAX_RESTORE_BYTES)throw new ApiClientError("Файл больше 512 МБ — уменьшите бэкап или обратитесь к администратору",413) const activeSignal=signal(options);activeSignal.throwIfAborted() await confirmDump(file);activeSignal.throwIfAborted() options?.onDispatch?.() const raw=await requestJson(baseUrl,"/api/system/database/restore",{method:"POST",headers:{"Content-Type":"application/octet-stream"},body:file,signal:activeSignal}) const receipt=receiptSchema.safeParse(raw) if(!receipt.success)throw new Error("Восстановление базы не подтверждено сервером") return receipt.data }