fix(backups): сверять сервер и файл подтверждения импорта

This commit is contained in:
Denozordec committed 2026-10-01 16:18:37 +07:00
1 parent af86d2c3cd
commit 99ff5aebaf
11 files changed
+51 -13

No files matched your search

@@ -7,7 +7,7 @@ import { permissionForRequest } from "../lib/permissions.js"
const app = Fastify(); app.setValidatorCompiler(validatorCompiler); app.setSerializerCompiler(serializerCompiler)
let deleteOutcome = "success", deleted = 0
let created = 0, restored = 0, eventCalls = 0, restoreFail = false
await app.register(createBackupsRoutes({ delete: async () => { deleted++; if (deleteOutcome === "error") throw new Error("S3 unavailable"); if (deleteOutcome === "missing") return null; return { id: "backup-1", serverId: 1, serverName: "router", filename: "config.rsc", sizeBytes: 0, createdAt: "2026-10-01T00:00:00Z", kind: "manual", storage: "both" } }, listServers: async () => [{ id: 1 }, { id: 2 }], create: async id => { created++; if (id === "2") throw new Error("RouterOS unavailable"); return { id: "backup-1", serverId: 1, serverName: "router", filename: "config.rsc", sizeBytes: 0, createdAt: "2026-10-01T00:00:00Z", kind: "manual", storage: "local" } }, restore: async () => { restored++; if (restoreFail) throw new Error("import failed"); return { filename: "config.rsc", serverName: "router" } }, appendEvent: async () => { eventCalls++; throw new Error("event DB unavailable") } }), { prefix: "/api" })
await app.register(createBackupsRoutes({ delete: async () => { deleted++; if (deleteOutcome === "error") throw new Error("S3 unavailable"); if (deleteOutcome === "missing") return null; return { id: "backup-1", serverId: 1, serverName: "router", filename: "config.rsc", sizeBytes: 0, createdAt: "2026-10-01T00:00:00Z", kind: "manual", storage: "both" } }, listServers: async () => [{ id: 1 }, { id: 2 }], create: async id => { created++; if (id === "2") throw new Error("RouterOS unavailable"); return { id: "backup-1", serverId: 1, serverName: "router", filename: "config.rsc", sizeBytes: 0, createdAt: "2026-10-01T00:00:00Z", kind: "manual", storage: "local" } }, restore: async () => { restored++; if (restoreFail) throw new Error("import failed"); return { backupId: "backup-1", serverId: 1, filename: "config.rsc", serverName: "router" } }, appendEvent: async () => { eventCalls++; throw new Error("event DB unavailable") } }), { prefix: "/api" })
const post = (payload: unknown) => app.inject({ method: "POST", url: "/api/backups/create", payload: payload as Record<string, unknown> })
for (const serverIds of [[], ["1", "missing"], ["1", "3"], ["1", "1"], ["0"], ["1junk"], [9007199254740992]]) assert.equal((await post({ serverIds })).statusCode, 400)
assert.equal(created, 0)
@@ -27,7 +27,7 @@ await new Promise(resolve => setImmediate(resolve)); assert.equal(created, befor
assert.equal((await post({ requestId, serverIds: ["1"], notes: "manual" })).statusCode, 202); assert.equal(created, before + 1)
assert.equal((await post({ requestId, serverIds: ["2"], notes: "manual" })).statusCode, 409)
assert.equal((await post({ requestId, serverIds: ["1"], notes: "other" })).statusCode, 409)
const success = await app.inject({ method: "POST", url: "/api/backups/backup-1/restore" }); assert.equal(success.statusCode, 200); assert.deepEqual(success.json(), { filename: "config.rsc", serverName: "router" }); assert.equal(restored, 1)
const success = await app.inject({ method: "POST", url: "/api/backups/backup-1/restore" }); assert.equal(success.statusCode, 200); assert.deepEqual(success.json(), { backupId: "backup-1", serverId: 1, filename: "config.rsc", serverName: "router" }); assert.equal(restored, 1)
restoreFail = true; const failure = await app.inject({ method: "POST", url: "/api/backups/backup-1/restore" }); assert.equal(failure.statusCode, 400); assert.equal(failure.json().error, "import failed")
assert.equal(permissionForRequest("POST", "/api/backups/create"), "mm:backups:write"); assert.equal(permissionForRequest("POST", "/api/backups/backup-1/restore"), "mm:backups:write"); assert.equal(permissionForRequest("GET", "/api/backups/jobs/job"), "mm:backups:read")
assert.equal((await app.inject({ method: "DELETE", url: "/api/backups/backup-1" })).statusCode, 204)
+2 -2
View File
@@ -468,7 +468,7 @@ export async function readBackupContent(meta: BackupMeta): Promise<Buffer> {
return await readFile(path.join(BACKUPS_DIR, meta.filename))
}
export async function restoreBackupToDevice(id: string): Promise<{ filename: string; serverName: string }> {
export async function restoreBackupToDevice(id: string): Promise<{ backupId: string; serverId: number; filename: string; serverName: string }> {
const meta = await getBackupById(id)
if (!meta) throw new Error("Бэкап не найден")
if (!meta.serverId) throw new Error("Сервер бэкапа удалён — восстановить нельзя")
@@ -478,7 +478,7 @@ export async function restoreBackupToDevice(id: string): Promise<{ filename: str
const client = MikrotikClient.fromServer(row)
const uploaded = await client.uploadTextFile(meta.filename, content.toString("utf8"), 60_000)
await client.importUploadedFile(uploaded)
return { filename: meta.filename, serverName: row.name }
return { backupId: meta.id, serverId: row.id, filename: meta.filename, serverName: row.name }
}
export async function syncBackupsFromS3(): Promise<{ imported: number; skipped: number }> {