/** * Локальное хранилище объектов (SQLite). Сохранены имена экспортов readS3TextObject / writeS3JsonObject и т.д. для совместимости с роутами. * Справочники EvoBGP не хранятся здесь — см. evobgpClient.js */ const { openDatabase } = require('../db/sqliteDb'); const blobStorage = require('./blobStorage'); openDatabase(); const BUCKET_NAME = process.env.SQLITE_BUCKET_LABEL || 'sqlite'; const cache = { text: new Map(), head: new Map(), max: 100, ttlMs: 30_000, }; async function streamToString(stream) { if (!stream) return ''; if (typeof stream.transformToString === 'function') { return await stream.transformToString(); } return await new Promise((resolve, reject) => { const chunks = []; stream.on('data', (c) => chunks.push(Buffer.isBuffer(c) ? c : Buffer.from(String(c)))); stream.once('error', reject); stream.once('end', () => resolve(Buffer.concat(chunks).toString('utf-8'))); }); } function getCache(map, key) { const v = map.get(key); if (!v) return null; if (Date.now() > v.at + cache.ttlMs) { map.delete(key); return null; } return v.value; } function setCache(map, key, value) { if (map.size >= cache.max) { const firstKey = map.keys().next().value; if (firstKey) map.delete(firstKey); } map.set(key, { value, at: Date.now() }); } function invalidateCacheForKey(key) { try { cache.text.delete(key); } catch (_) {} try { cache.head.delete(key); } catch (_) {} } async function readS3TextObject(key) { const cached = getCache(cache.text, key); if (cached) return cached; const row = blobStorage.readBlob(key); if (!row) { const err = new Error('NoSuchKey'); err.code = 'NoSuchKey'; throw err; } const out = { body: row.body, etag: row.etag, lastModified: row.lastModified, contentLength: row.contentLength, }; setCache(cache.text, key, out); return out; } async function headS3ObjectEtag(key) { const cached = getCache(cache.head, key); if (cached && cached.etag) return cached.etag; const etag = blobStorage.headEtag(key); if (etag) setCache(cache.head, key, { etag }); return etag; } async function headMeta(key) { return blobStorage.headMetaRow(key); } async function writeS3TextObject(key, content, contentType = 'text/plain') { const db = openDatabase(); blobStorage.writeBlobTx(db, key, content, contentType); invalidateCacheForKey(key); return headMeta(key); } async function writeS3JsonObject(key, data) { return writeS3TextObject(key, JSON.stringify(data, null, 2), 'application/json'); } async function deleteS3Object(key) { blobStorage.deleteBlob(key); invalidateCacheForKey(key); } async function listS3Objects(prefix, { maxKeys = 100 } = {}) { return blobStorage.listByPrefix(prefix, maxKeys); } async function streamPaginatedText({ key, mapLine, q, offset = 0, limit = 0 }) { let text = ''; try { const row = await readS3TextObject(key); text = row.body || ''; } catch (e) { if (e?.code === 'NoSuchKey') { return { items: [], total: 0 }; } throw e; } const lines = text.split('\n'); let total = 0; const items = []; const qstr = q ? String(q).toLowerCase() : ''; const matchesQuery = (line) => !qstr || line.toLowerCase().includes(qstr); for (let i = 0; i < lines.length; i++) { const line = String(lines[i] || '').trim(); if (!line) continue; if (!matchesQuery(line)) continue; total++; const pos = total - 1; if (limit > 0) { if (pos >= offset && items.length < limit) items.push(mapLine(line)); } else { items.push(mapLine(line)); } } return { items, total }; } function listBlobVersionsForKey(key, maxKeys = 50) { return blobStorage.listVersions(key, maxKeys); } function rollbackBlobVersion(key, versionId) { openDatabase(); return blobStorage.rollbackToVersionId(key, versionId); } /** Заглушка: прямой вызов S3 SDK больше не используется. */ const s3 = { send() { throw new Error('S3 SDK removed: use readS3TextObject / writeS3TextObject or evobgpClient'); }, }; module.exports = { s3, BUCKET_NAME, streamToString, readS3TextObject, headS3ObjectEtag, headMeta, writeS3TextObject, writeS3JsonObject, deleteS3Object, streamPaginatedText, invalidateCacheForKey, listS3Objects, listBlobVersionsForKey, rollbackBlobVersion, };