fix(backups): отклонять восстановление старых частичных экспортов

This commit is contained in:
Denozordec committed 2026-10-14 11:16:29 +07:00
1 parent faf452d149
commit 659e658f18
16 files changed
+1175 -12

No files matched your search

+5 -1
View File
@@ -1,8 +1,12 @@
import assert from "node:assert/strict"
import { test } from "node:test"
import { MikrotikClient, MikrotikError } from "./mikrotik.js"
import { BackupRestoreUnknownError, importBackupOnce } from "./backup-restore.js"
import { assertRestorableBackupScript, BackupRestoreUnknownError, importBackupOnce } from "./backup-restore.js"
const makeClient = () => new MikrotikClient({ host: "router.invalid", port: 80, useSsl: false, verifySsl: false, username: "test", password: "test" })
test("legacy partial exports are rejected while native and limited scripts remain usable", () => {
for (const prefix of ["", "\uFEFF\r\n "]) assert.throws(() => assertRestorableBackupScript(prefix + "# synthetic export generated by MikrotikManager\n/system identity\nset name=legacy"), /частичным экспортом/)
for (const script of ["# oct/14/2026 by RouterOS 7.24.5\n/system identity\nset name=native", "/interface bridge\nadd name=owned-probe", "# harmless fixture\n"]) assert.doesNotThrow(() => assertRestorableBackupScript(script))
})
test("import does not fallback after ambiguous failure or command rejection", async () => {
for (const error of [new Error("connection lost after apply"), new Error("Invalid JSON"), new MikrotikError(500, "/import", "error"), new MikrotikError(400, "/import", "script failed"), new MikrotikError(401, "/import", "unauthorized")]) {
const client = makeClient(), calls: string[] = []
+5
View File
@@ -1,6 +1,11 @@
export class BackupRestoreUnknownError extends Error {
constructor(options?: ErrorOptions) { super("Результат импорта неизвестен. Проверьте конфигурацию роутера перед повтором.", options); this.name = "BackupRestoreUnknownError" }
}
export function assertRestorableBackupScript(script: string): void {
if (/^#\s*synthetic export generated by MikrotikManager\b/i.test(script.trimStart())) {
throw new Error("Этот файл создан старым частичным экспортом MikroTik Manager. Его нельзя использовать для восстановления конфигурации; создайте новую нативную резервную копию.")
}
}
export async function importBackupOnce(file: string, apply: (file: string) => Promise<unknown>): Promise<void> {
try { await apply(file) } catch (cause) { throw new BackupRestoreUnknownError({ cause }) }
}
+2 -1
View File
@@ -9,7 +9,7 @@ import type {
} from "@mmapp/contracts/backups"
import { mapBackupEntry } from "./backup-dto.js"
import { pruneBackupRecords } from "./backup-prune.js"
import { importBackupOnce } from "./backup-restore.js"
import { assertRestorableBackupScript, importBackupOnce } from "./backup-restore.js"
import { deleteStoredBackup } from "./backup-deletion.js"
import { assertBackupStorageConnectionChange, planBackupStorageUpdate } from "./backup-storage-update.js"
import { withBackupStorageOperation, withBackupStorageSettingsUpdate } from "./backup-storage-operation.js"
@@ -502,6 +502,7 @@ export async function restoreBackupToDevice(id: string): Promise<{ backupId: str
const row = await getServerRowById(meta.serverId)
if (!row) throw new Error("Сервер не найден")
const content = await readBackupContent(meta)
assertRestorableBackupScript(content.toString("utf8"))
const client = MikrotikClient.fromServer(row)
const uploaded = await client.uploadTextFile(meta.filename, content.toString("utf8"), 60_000)
await importBackupOnce(uploaded, file => client.importUploadedFile(file))