fix(backups): отклонять восстановление старых частичных экспортов
This commit is contained in:
1 parent
faf452d149
commit
659e658f18
16 files changed
+1175
-12
No files matched your search
@@ -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[] = []
|
||||
|
||||
@@ -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 }) }
|
||||
}
|
||||
@@ -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))
|
||||
|
||||
Reference in new issue
Block a user