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

This commit is contained in:
Denozordec committed 2026-10-01 16:22:06 +07:00
1 parent 99ff5aebaf
commit 39570656c9
10 files changed
+63 -8

No files matched your search

@@ -0,0 +1,24 @@
import assert from "node:assert/strict"
import { test } from "node:test"
import { MikrotikClient, MikrotikError } from "./mikrotik.js"
import { BackupRestoreUnknownError, importBackupOnce } from "./backup-restore.js"
const makeClient = () => new MikrotikClient({ host: "router.invalid", port: 80, useSsl: false, verifySsl: false, username: "test", password: "test" })
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[] = []
client.post = async <T>(endpoint: string): Promise<T> => { calls.push(endpoint); throw error }
await assert.rejects(client.importUploadedFile("config.rsc"), error); assert.deepEqual(calls, ["/import"])
}
})
test("unsupported endpoint retains one fallback and successful import is not repeated", async () => {
for (const status of [200, 404, 405]) {
const client = makeClient(), calls: string[] = []
client.post = async <T>(endpoint: string): Promise<T> => { calls.push(endpoint); if (endpoint === "/import" && status !== 200) throw new MikrotikError(status, endpoint, "unsupported"); return { ok: true } as T }
await client.importUploadedFile("config.rsc"); assert.deepEqual(calls, status === 200 ? ["/import"] : ["/import", "/execute"])
}
})
test("failed import has unknown outcome and no application retry", async () => {
let calls = 0
await assert.rejects(importBackupOnce("config.rsc", async () => { calls++; throw new Error("lost response") }), BackupRestoreUnknownError)
assert.equal(calls, 1); await importBackupOnce("config.rsc", async () => ({}))
})
+6
View File
@@ -0,0 +1,6 @@
export class BackupRestoreUnknownError extends Error {
constructor(options?: ErrorOptions) { super("Результат импорта неизвестен. Проверьте конфигурацию роутера перед повтором.", options); this.name = "BackupRestoreUnknownError" }
}
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
@@ -8,6 +8,7 @@ import type {
PutBackupStorageSettings,
} from "@mmapp/contracts/backups"
import { mapBackupEntry } from "./backup-dto.js"
import { importBackupOnce } from "./backup-restore.js"
import { deleteStoredBackup } from "./backup-deletion.js"
import { planBackupStorageUpdate } from "./backup-storage-update.js"
import { db } from "../db/index.js"
@@ -477,7 +478,7 @@ export async function restoreBackupToDevice(id: string): Promise<{ backupId: str
const content = await readBackupContent(meta)
const client = MikrotikClient.fromServer(row)
const uploaded = await client.uploadTextFile(meta.filename, content.toString("utf8"), 60_000)
await client.importUploadedFile(uploaded)
await importBackupOnce(uploaded, file => client.importUploadedFile(file))
return { backupId: meta.id, serverId: row.id, filename: meta.filename, serverName: row.name }
}
+2 -1
View File
@@ -784,7 +784,8 @@ export class MikrotikClient {
async importUploadedFile(fileName: string): Promise<unknown> {
try {
return await this.post("/import", { "file-name": fileName }, 120_000)
} catch {
} catch (error) {
if (!(error instanceof MikrotikError) || ![404, 405].includes(error.statusCode)) throw error
return await this.post("/execute", { script: `/import file-name="${fileName}"` }, 120_000)
}
}