104 lines
3.1 KiB
JavaScript
104 lines
3.1 KiB
JavaScript
/**
|
|
* Бэкапы MikroTik на локальной ФС (вместо S3).
|
|
* Логические ключи сохраняют вид backups/mikrotik/{serverId}/{ts}-{rand}.rsc
|
|
*/
|
|
|
|
const fs = require('fs');
|
|
const path = require('path');
|
|
const crypto = require('crypto');
|
|
|
|
const BACKUP_PREFIX = 'backups/mikrotik';
|
|
|
|
function backupRootDir() {
|
|
const raw = process.env.MIKROTIK_BACKUP_DIR || path.join(__dirname, '..', 'data', 'backups', 'mikrotik');
|
|
if (!fs.existsSync(raw)) {
|
|
fs.mkdirSync(raw, { recursive: true });
|
|
}
|
|
return raw;
|
|
}
|
|
|
|
function parseBackupKey(key) {
|
|
const m = String(key || '').match(/^backups\/mikrotik\/([^/]+)\/(.+)$/);
|
|
if (!m) return null;
|
|
return { safeId: m[1], fileName: m[2] };
|
|
}
|
|
|
|
function fsPathForKey(key) {
|
|
const p = parseBackupKey(key);
|
|
if (!p) return null;
|
|
return path.join(backupRootDir(), p.safeId, p.fileName);
|
|
}
|
|
|
|
function buildBackupKey(serverId, createdAt, suffix) {
|
|
const safeId = String(serverId || 'unknown').replace(/[^a-zA-Z0-9._-]/g, '_');
|
|
const ts = createdAt.toISOString().replace(/[:-]/g, '').replace(/\.\d+Z$/, 'Z');
|
|
const rand = suffix || crypto.randomBytes(3).toString('hex');
|
|
return `${BACKUP_PREFIX}/${safeId}/${ts}-${rand}.rsc`;
|
|
}
|
|
|
|
function etagForContent(content) {
|
|
const h = crypto.createHash('sha256').update(Buffer.from(String(content), 'utf8')).digest('hex');
|
|
return `"${h}"`;
|
|
}
|
|
|
|
async function writeBackupFile(key, content, contentType = 'text/plain') {
|
|
const p = parseBackupKey(key);
|
|
if (!p) throw new Error('Invalid backup key');
|
|
const dir = path.join(backupRootDir(), p.safeId);
|
|
if (!fs.existsSync(dir)) fs.mkdirSync(dir, { recursive: true });
|
|
const fp = path.join(dir, p.fileName);
|
|
await fs.promises.writeFile(fp, String(content), 'utf8');
|
|
const st = await fs.promises.stat(fp);
|
|
const etag = etagForContent(String(content));
|
|
return {
|
|
etag,
|
|
lastModified: st.mtime.toISOString(),
|
|
contentLength: st.size,
|
|
};
|
|
}
|
|
|
|
async function readBackupFile(key) {
|
|
const fp = fsPathForKey(key);
|
|
if (!fp) throw new Error('Invalid backup key');
|
|
const body = await fs.promises.readFile(fp, 'utf8');
|
|
const st = await fs.promises.stat(fp);
|
|
const etag = etagForContent(body);
|
|
return {
|
|
body,
|
|
etag,
|
|
lastModified: st.mtime.toISOString(),
|
|
contentLength: st.size,
|
|
};
|
|
}
|
|
|
|
async function listBackupObjects(serverId, maxKeys = 200) {
|
|
const safeId = String(serverId || '').replace(/[^a-zA-Z0-9._-]/g, '_');
|
|
const dir = path.join(backupRootDir(), safeId);
|
|
if (!fs.existsSync(dir)) return [];
|
|
const names = await fs.promises.readdir(dir);
|
|
const out = [];
|
|
for (const name of names) {
|
|
if (out.length >= maxKeys) break;
|
|
const fp = path.join(dir, name);
|
|
const st = await fs.promises.stat(fp);
|
|
if (!st.isFile()) continue;
|
|
const key = `${BACKUP_PREFIX}/${safeId}/${name}`;
|
|
out.push({
|
|
key,
|
|
size: st.size,
|
|
lastModified: st.mtime.toISOString(),
|
|
etag: etagForContent(await fs.promises.readFile(fp, 'utf8')),
|
|
});
|
|
}
|
|
return out;
|
|
}
|
|
|
|
module.exports = {
|
|
BACKUP_PREFIX,
|
|
buildBackupKey,
|
|
writeBackupFile,
|
|
readBackupFile,
|
|
listBackupObjects,
|
|
backupRootDir,
|
|
};
|