fix(settings): сохранять результат операций базы при вторичных сбоях

This commit is contained in:
Denozordec committed 2026-10-03 16:43:52 +07:00
1 parent 738c98b9d3
commit 92b65a30ce
15 files changed
+159 -32

No files matched your search

+12
View File
@@ -26,3 +26,15 @@ test("restore needs literal receipt; malformed/false/204/HTTP error never retry"
calls=0;globalThis.fetch=async()=>{calls++;return Response.json({error:"restore unknown"},{status:503})};await assert.rejects(restoreSystemDatabaseBackup("https://backend.test",dump),/restore unknown/);assert.equal(calls,1)
}finally{globalThis.fetch=original}
})
test("backup warning header is strict and malformed metadata preserves valid archive with warning",async()=>{
const original=globalThis.fetch;try{
const warnings={listenerError:"listener warning",schedulerError:"scheduler warning",eventError:"event warning"}
globalThis.fetch=async()=>new Response("PGDMPfixture",{headers:{"X-System-Database-Outcome":encodeURIComponent(JSON.stringify(warnings))}})
assert.deepEqual((await downloadSystemDatabaseBackup("https://backend.test")).warnings,warnings)
for(const header of ["%","false",encodeURIComponent(JSON.stringify({listenerError:4})),encodeURIComponent(JSON.stringify({error:"failed"}))]){
globalThis.fetch=async()=>new Response("PGDMPfixture",{headers:{"X-System-Database-Outcome":header}})
const result=await downloadSystemDatabaseBackup("https://backend.test");assert.equal(await result.blob.text(),"PGDMPfixture");assert.match(result.warnings.eventError!,/не подтверждены/)
}
}finally{globalThis.fetch=original}
})
+9 -2
View File
@@ -4,6 +4,8 @@ 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<typeof receiptSchema>
const warningSchema=receiptSchema.omit({ok:true})
export type SystemDatabaseWarnings=z.output<typeof warningSchema>
type Options={signal?:AbortSignal}
const signal=(options?:Options)=>options?.signal?AbortSignal.any([options.signal,AbortSignal.timeout(600_000)]):AbortSignal.timeout(600_000)
function parseFilename(value:string|null):string{
@@ -21,12 +23,17 @@ async function confirmDump(blob:Blob):Promise<void>{
const head=new Uint8Array(await blob.slice(0,magic.length).arrayBuffer())
if(!magic.every((value,index)=>head[index]===value))throw new Error("Файл не похож на резервную копию PostgreSQL (PGDMP)")
}
export async function downloadSystemDatabaseBackup(baseUrl:string,options?:Options):Promise<{blob:Blob;filename:string}>{
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()
return {blob,filename:parseFilename(response.headers.get("Content-Disposition"))}
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<SystemDatabaseRestoreReceipt>{
if(file.size>MAX_RESTORE_BYTES)throw new ApiClientError("Файл больше 512 МБ — уменьшите бэкап или обратитесь к администратору",413)