refactor(mikrotikBackupRoutes, mikrotikApplyService): improve error handling and logging for backup and file retrieval processes
Publish Fast Tabler Docker image / build-and-push-fast (push) Successful in 3m2s
Publish Fast Tabler Docker image / build-and-push-fast (push) Successful in 3m2s
This commit is contained in:
@@ -280,9 +280,16 @@ async function runBackupNow(req, res) {
|
|||||||
s3,
|
s3,
|
||||||
});
|
});
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('runBackupNow error:', error);
|
console.error('runBackupNow error:', error?.message, error?.stack, error?.response?.data);
|
||||||
const msg = error?.message || 'Error running backup now';
|
let msg = error?.message || 'Error running backup now';
|
||||||
return sendError(res, 500, msg, 'E_BACKUP_RUN');
|
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);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -355,68 +355,80 @@ async function applyBlock(client, block, dryRun) {
|
|||||||
async function fetchExportViaFile(client) {
|
async function fetchExportViaFile(client) {
|
||||||
const crypto = require('crypto');
|
const crypto = require('crypto');
|
||||||
const basename = `backup_${Date.now()}_${crypto.randomBytes(4).toString('hex')}.rsc`;
|
const basename = `backup_${Date.now()}_${crypto.randomBytes(4).toString('hex')}.rsc`;
|
||||||
|
// Явно указываем flash/ — экспорт сохраняется в flash
|
||||||
|
const filePath = `flash/${basename}`;
|
||||||
|
|
||||||
await client.command('export', { compact: '', file: basename });
|
try {
|
||||||
|
await client.command('export', { compact: '', file: filePath });
|
||||||
// REST API: file/print — пробуем GET с query в URL (некоторые версии ожидают это)
|
} catch (err) {
|
||||||
const qs = `?.proplist=.id,name,contents&name=${encodeURIComponent(basename)}`;
|
// Попробуем без flash/ — старые версии RouterOS могут сохранять иначе
|
||||||
let printRes = await client.print(`file/print${qs}`);
|
try {
|
||||||
let data = printRes?.data;
|
await client.command('export', { compact: '', file: basename });
|
||||||
let list = Array.isArray(data) ? data : (data ? [data] : []);
|
} catch (err2) {
|
||||||
|
const msg = err?.message || err2?.message || String(err);
|
||||||
// Если по name в URL не нашли — пробуем POST с .query в body
|
const detail = err?.response?.data || err2?.response?.data;
|
||||||
if (list.length === 0) {
|
console.error('[fetchExportViaFile] export failed:', msg, detail);
|
||||||
printRes = await client.command('file/print', {
|
throw new Error(`Export failed: ${msg}${detail ? ` (${JSON.stringify(detail).slice(0, 200)})` : ''}`);
|
||||||
'.proplist': ['.id', 'name', 'contents'],
|
}
|
||||||
'.query': [`name=${basename}`],
|
|
||||||
});
|
|
||||||
data = printRes?.data;
|
|
||||||
list = Array.isArray(data) ? data : (data ? [data] : []);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Если всё ещё пусто — печатаем все файлы и ищем по имени вручную
|
// POST file/print — получаем все файлы и ищем по имени (надёжнее GET с query)
|
||||||
if (list.length === 0) {
|
let list = [];
|
||||||
printRes = await client.command('file/print', {
|
try {
|
||||||
|
const printRes = await client.command('file/print', {
|
||||||
'.proplist': ['.id', 'name', 'contents'],
|
'.proplist': ['.id', 'name', 'contents'],
|
||||||
});
|
});
|
||||||
data = printRes?.data;
|
const data = printRes?.data;
|
||||||
list = Array.isArray(data) ? data : (data ? [data] : []);
|
const raw = Array.isArray(data) ? data : (data ? [data] : []);
|
||||||
const found = list.find(
|
const found = raw.find(
|
||||||
(f) =>
|
(f) =>
|
||||||
(f.name || '') === basename ||
|
(f.name || '') === basename ||
|
||||||
|
(f.name || '') === filePath ||
|
||||||
(f.name || '').endsWith('/' + basename) ||
|
(f.name || '').endsWith('/' + basename) ||
|
||||||
(f.name || '').endsWith(basename),
|
(f.name || '').endsWith(basename),
|
||||||
);
|
);
|
||||||
if (found) list = [found];
|
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];
|
const file = list[0];
|
||||||
if (!file) {
|
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 || '');
|
let config = String(file.contents || '');
|
||||||
|
|
||||||
// Для файлов >60KB API не возвращает contents в print — читаем через /file/read чанками
|
// Для файлов >60KB API не возвращает contents в print — читаем через file/read
|
||||||
if (!config && file.name) {
|
if (!config && file.name) {
|
||||||
const chunkSize = 32768;
|
const chunkSize = 32768;
|
||||||
let offset = 0;
|
let offset = 0;
|
||||||
const chunks = [];
|
const chunks = [];
|
||||||
for (;;) {
|
try {
|
||||||
const readRes = await client.command('file/read', {
|
for (;;) {
|
||||||
file: file.name,
|
const readRes = await client.command('file/read', {
|
||||||
offset: String(offset),
|
file: file.name,
|
||||||
'chunk-size': String(chunkSize),
|
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 rd = readRes?.data;
|
||||||
const str = typeof chunk === 'string' ? chunk : (chunk ? String(chunk) : '');
|
const chunk = Array.isArray(rd) ? (rd[0]?.data ?? rd[0]) : (rd?.data ?? rd);
|
||||||
if (!str) break;
|
const str = typeof chunk === 'string' ? chunk : (chunk ? String(chunk) : '');
|
||||||
chunks.push(str);
|
if (!str) break;
|
||||||
offset += str.length;
|
chunks.push(str);
|
||||||
if (str.length < chunkSize) break;
|
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) {
|
if (!config) {
|
||||||
@@ -424,7 +436,7 @@ async function fetchExportViaFile(client) {
|
|||||||
const id = file['.id'];
|
const id = file['.id'];
|
||||||
if (id) await client.remove(`file/${id}`);
|
if (id) await client.remove(`file/${id}`);
|
||||||
} catch (_) {}
|
} 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 {
|
try {
|
||||||
|
|||||||
Reference in New Issue
Block a user