fix(backups): сверять сервер и файл подтверждения импорта
This commit is contained in:
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)
|
||||
|
||||
@@ -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 }> {
|
||||
|
||||
Reference in new issue
Block a user