From 89d35cd7ef32d06a9e4eeb030a03735073b38549 Mon Sep 17 00:00:00 2001 From: shats Date: Mon, 9 Feb 2026 19:50:29 +0700 Subject: [PATCH] feat(mikrotikBackup): add endpoint and UI for manual backup execution --- backend/routes/mikrotikBackupRoutes.js | 116 +++++++++++++++++++++++- backend/server.js | 1 + frontend/src/MikrotikBackupsManager.jsx | 32 +++++++ 3 files changed, 148 insertions(+), 1 deletion(-) diff --git a/backend/routes/mikrotikBackupRoutes.js b/backend/routes/mikrotikBackupRoutes.js index bc12d97..79f2510 100644 --- a/backend/routes/mikrotikBackupRoutes.js +++ b/backend/routes/mikrotikBackupRoutes.js @@ -12,8 +12,11 @@ */ const crypto = require('crypto'); -const { sendError } = require('../middleware/errorHandler'); +const { sendError, sendOk } = require('../middleware/errorHandler'); const { writeS3TextObject, readS3TextObject, listS3Objects } = require('../services/s3Service'); +const { readServersFromS3 } = require('./serversRoutes'); +const { createRosClient } = require('../services/mikrotikApplyService'); +const { decrypt } = require('../utils/encryption'); const BACKUP_PREFIX = 'backups/mikrotik'; @@ -194,11 +197,122 @@ async function diffBackups(req, res) { } } +/** + * Вспомогательная функция: получить MikroTik REST-клиент по serverId + */ +async function getMikrotikClientForServerId(serverId) { + const servers = await readServersFromS3(); + const server = servers.find((s) => (s.id || s.dns || s.ip) === serverId); + if (!server || server.type !== 'jumphost') { + const err = new Error('Jumphost server not found'); + err.code = 'E_NOT_FOUND'; + throw err; + } + const host = server.mikrotikHost || server.ip || server.dns; + if (!host) { + const err = new Error('MikroTik host is not configured for this server'); + err.code = 'E_HOST'; + throw err; + } + let password = ''; + if (server.encryptedMikrotikPassword) { + try { + password = decrypt(server.encryptedMikrotikPassword); + } catch (e) { + const err = new Error('Failed to decrypt MikroTik password'); + err.code = 'E_CREDENTIALS'; + throw err; + } + } else { + const err = new Error('MikroTik credentials not configured for this server'); + err.code = 'E_CREDENTIALS'; + throw err; + } + const port = server.mikrotikPort; + const restPort = port === 80 || port === 443 || port === 8443 ? port : 80; + const user = server.mikrotikUser || 'admin'; + const creds = { host, port: restPort, user, password, secure: false }; + return createRosClient(creds); +} + +/** + * POST /api/mikrotik/backups/run + * Body: { serverId: string } + * + * Немедленно делает /rest/export compact на указанном MikroTik и сохраняет результат в S3. + */ +async function runBackupNow(req, res) { + try { + const { serverId } = req.body || {}; + if (!serverId || typeof serverId !== 'string') { + return sendError(res, 400, 'serverId is required', 'E_BAD_REQUEST'); + } + + let client; + try { + client = await getMikrotikClientForServerId(serverId); + } catch (e) { + if (e.code === 'E_NOT_FOUND') { + return sendError(res, 400, e.message, e.code); + } + if (e.code === 'E_HOST' || e.code === 'E_CREDENTIALS') { + return sendError(res, 400, e.message, e.code); + } + throw e; + } + + // /rest/export с compact, как в планировщике + const resp = await client.command('export', { compact: '' }); + const data = resp && resp.data; + let config = ''; + if (typeof data === 'string') config = data; + else if (Array.isArray(data)) { + config = data + .map((line) => { + if (typeof line === 'string') return line; + if (line && typeof line === 'object') { + if (line.rsc) return String(line.rsc); + if (line.config) return String(line.config); + return JSON.stringify(line); + } + return String(line ?? ''); + }) + .join('\n'); + } else if (data && typeof data === 'object') { + config = JSON.stringify(data, null, 2); + } else { + config = String(data || ''); + } + + if (!config || !config.trim()) { + return sendError(res, 502, 'Empty export result from MikroTik', 'E_EMPTY_EXPORT'); + } + + const { key, createdAt, s3 } = await saveBackupForServer(serverId, config, { + source: 'manual', + comment: 'Manual backup from UI (/api/mikrotik/backups/run)', + }); + + return sendOk(res, { + ok: true, + key, + serverId, + createdAt, + s3, + }); + } catch (error) { + console.error('runBackupNow error:', error); + const msg = error?.message || 'Error running backup now'; + return sendError(res, 500, msg, 'E_BACKUP_RUN'); + } +} + module.exports = { createBackup, listBackups, getBackup, diffBackups, saveBackupForServer, + runBackupNow, }; diff --git a/backend/server.js b/backend/server.js index 4340bc7..065eb3c 100644 --- a/backend/server.js +++ b/backend/server.js @@ -461,6 +461,7 @@ app.post('/api/mikrotik/backups', writeLimiter, mikrotikBackupRoutes.createBacku app.get('/api/mikrotik/backups', mikrotikBackupRoutes.listBackups); app.get('/api/mikrotik/backups/item', mikrotikBackupRoutes.getBackup); app.post('/api/mikrotik/backups/diff', mikrotikBackupRoutes.diffBackups); +app.post('/api/mikrotik/backups/run', writeLimiter, mikrotikBackupRoutes.runBackupNow); // === MIKROTIK VALIDATION === app.post('/api/mikrotik/validate', async (req, res) => { diff --git a/frontend/src/MikrotikBackupsManager.jsx b/frontend/src/MikrotikBackupsManager.jsx index 698c027..2412ea2 100644 --- a/frontend/src/MikrotikBackupsManager.jsx +++ b/frontend/src/MikrotikBackupsManager.jsx @@ -268,6 +268,38 @@ function MikrotikBackupsManager() { Обновить список +