diff --git a/backend/routes/mikrotikBackupRoutes.js b/backend/routes/mikrotikBackupRoutes.js index a1268ea..b06b556 100644 --- a/backend/routes/mikrotikBackupRoutes.js +++ b/backend/routes/mikrotikBackupRoutes.js @@ -280,9 +280,16 @@ async function runBackupNow(req, res) { s3, }); } catch (error) { - console.error('runBackupNow error:', error); - const msg = error?.message || 'Error running backup now'; - return sendError(res, 500, msg, 'E_BACKUP_RUN'); + console.error('runBackupNow error:', error?.message, error?.stack, error?.response?.data); + let msg = error?.message || 'Error running backup now'; + const detail = error?.response?.data; + if (detail && typeof detail === 'object') { + const d = detail.detail || detail.message; + if (d) msg += ` (${d})`; + } else if (typeof detail === 'string') { + msg += ` (${detail})`; + } + return sendError(res, 500, msg, 'E_BACKUP_RUN', process.env.NODE_ENV !== 'production' ? { stack: error?.stack } : undefined); } } diff --git a/backend/services/mikrotikApplyService.js b/backend/services/mikrotikApplyService.js index 1640b9d..5feab9b 100644 --- a/backend/services/mikrotikApplyService.js +++ b/backend/services/mikrotikApplyService.js @@ -355,68 +355,80 @@ async function applyBlock(client, block, dryRun) { async function fetchExportViaFile(client) { const crypto = require('crypto'); const basename = `backup_${Date.now()}_${crypto.randomBytes(4).toString('hex')}.rsc`; + // Явно указываем flash/ — экспорт сохраняется в flash + const filePath = `flash/${basename}`; - await client.command('export', { compact: '', file: basename }); - - // REST API: file/print — пробуем GET с query в URL (некоторые версии ожидают это) - const qs = `?.proplist=.id,name,contents&name=${encodeURIComponent(basename)}`; - let printRes = await client.print(`file/print${qs}`); - let data = printRes?.data; - let list = Array.isArray(data) ? data : (data ? [data] : []); - - // Если по name в URL не нашли — пробуем POST с .query в body - if (list.length === 0) { - printRes = await client.command('file/print', { - '.proplist': ['.id', 'name', 'contents'], - '.query': [`name=${basename}`], - }); - data = printRes?.data; - list = Array.isArray(data) ? data : (data ? [data] : []); + try { + await client.command('export', { compact: '', file: filePath }); + } catch (err) { + // Попробуем без flash/ — старые версии RouterOS могут сохранять иначе + try { + await client.command('export', { compact: '', file: basename }); + } catch (err2) { + const msg = err?.message || err2?.message || String(err); + const detail = err?.response?.data || err2?.response?.data; + console.error('[fetchExportViaFile] export failed:', msg, detail); + throw new Error(`Export failed: ${msg}${detail ? ` (${JSON.stringify(detail).slice(0, 200)})` : ''}`); + } } - // Если всё ещё пусто — печатаем все файлы и ищем по имени вручную - if (list.length === 0) { - printRes = await client.command('file/print', { + // POST file/print — получаем все файлы и ищем по имени (надёжнее GET с query) + let list = []; + try { + const printRes = await client.command('file/print', { '.proplist': ['.id', 'name', 'contents'], }); - data = printRes?.data; - list = Array.isArray(data) ? data : (data ? [data] : []); - const found = list.find( + const data = printRes?.data; + const raw = Array.isArray(data) ? data : (data ? [data] : []); + const found = raw.find( (f) => (f.name || '') === basename || + (f.name || '') === filePath || (f.name || '').endsWith('/' + basename) || (f.name || '').endsWith(basename), ); if (found) list = [found]; + } catch (err) { + console.error('[fetchExportViaFile] file/print failed:', err?.message, err?.response?.data); + throw new Error(`file/print failed: ${err?.message || String(err)}`); } const file = list[0]; if (!file) { - throw new Error('Failed to read export file from MikroTik (file not found after export)'); + throw new Error('Export file not found after export (check write permissions on router)'); } let config = String(file.contents || ''); - // Для файлов >60KB API не возвращает contents в print — читаем через /file/read чанками + // Для файлов >60KB API не возвращает contents в print — читаем через file/read if (!config && file.name) { const chunkSize = 32768; let offset = 0; const chunks = []; - for (;;) { - const readRes = await client.command('file/read', { - file: file.name, - offset: String(offset), - 'chunk-size': String(chunkSize), - }); - const rd = readRes?.data; - const chunk = Array.isArray(rd) ? (rd[0]?.data ?? rd[0]) : (rd?.data ?? rd); - const str = typeof chunk === 'string' ? chunk : (chunk ? String(chunk) : ''); - if (!str) break; - chunks.push(str); - offset += str.length; - if (str.length < chunkSize) break; + try { + for (;;) { + const readRes = await client.command('file/read', { + file: file.name, + offset: String(offset), + 'chunk-size': String(chunkSize), + }); + const rd = readRes?.data; + const chunk = Array.isArray(rd) ? (rd[0]?.data ?? rd[0]) : (rd?.data ?? rd); + const str = typeof chunk === 'string' ? chunk : (chunk ? String(chunk) : ''); + if (!str) break; + chunks.push(str); + offset += str.length; + if (str.length < chunkSize) break; + } + config = chunks.join(''); + } catch (err) { + console.error('[fetchExportViaFile] file/read failed:', err?.message); + try { + const id = file['.id']; + if (id) await client.remove(`file/${id}`); + } catch (_) {} + throw new Error(`file/read failed: ${err?.message || String(err)}`); } - config = chunks.join(''); } if (!config) { @@ -424,7 +436,7 @@ async function fetchExportViaFile(client) { const id = file['.id']; if (id) await client.remove(`file/${id}`); } catch (_) {} - throw new Error('Failed to read export file from MikroTik (file/print and file/read returned no contents)'); + throw new Error('File/print and file/read returned no contents (router may restrict file access)'); } try {