Backend: SQLite storage, EvoBGP integration, filters in SQL
Made-with: Cursor
This commit is contained in:
@@ -0,0 +1,140 @@
|
||||
/**
|
||||
* Single-process SQLite connection, migrations, PRAGMAs.
|
||||
* Designed so the same API can later move to a dedicated storage service process.
|
||||
*/
|
||||
|
||||
const fs = require('fs');
|
||||
const path = require('path');
|
||||
const Database = require('better-sqlite3');
|
||||
|
||||
let _db = null;
|
||||
let _cachePurgeTimer = null;
|
||||
|
||||
function defaultDbPath() {
|
||||
return path.join(__dirname, '..', 'data', 'router-lists.db');
|
||||
}
|
||||
|
||||
function getDbPath() {
|
||||
const p = process.env.SQLITE_PATH || defaultDbPath();
|
||||
const dir = path.dirname(p);
|
||||
if (!fs.existsSync(dir)) {
|
||||
fs.mkdirSync(dir, { recursive: true });
|
||||
}
|
||||
return p;
|
||||
}
|
||||
|
||||
function applyMigration001(db) {
|
||||
const sql = fs.readFileSync(path.join(__dirname, 'migrations', '001_init.sql'), 'utf8');
|
||||
db.exec(sql);
|
||||
}
|
||||
|
||||
function applyMigration002(db) {
|
||||
const sql = fs.readFileSync(path.join(__dirname, 'migrations', '002_filter_rules.sql'), 'utf8');
|
||||
db.exec(sql);
|
||||
|
||||
const migrateBlobToRules = (objectKey, scope) => {
|
||||
const row = db.prepare('SELECT body FROM blobs WHERE object_key = ?').get(objectKey);
|
||||
if (!row) return;
|
||||
let bodyStr = '';
|
||||
try {
|
||||
bodyStr = Buffer.isBuffer(row.body) ? row.body.toString('utf8') : String(row.body);
|
||||
} catch {
|
||||
return;
|
||||
}
|
||||
let arr = [];
|
||||
try {
|
||||
const parsed = JSON.parse(bodyStr);
|
||||
arr = Array.isArray(parsed) ? parsed : [];
|
||||
} catch {
|
||||
return;
|
||||
}
|
||||
const now = Date.now();
|
||||
const delRules = db.prepare('DELETE FROM app_filter_rules WHERE scope = ?');
|
||||
const ins = db.prepare(
|
||||
`INSERT INTO app_filter_rules (scope, position, community, gateway, description, extra_json, updated_at)
|
||||
VALUES (?, ?, ?, ?, ?, ?, ?)`,
|
||||
);
|
||||
const run = () => {
|
||||
delRules.run(scope);
|
||||
for (let i = 0; i < arr.length; i++) {
|
||||
const it = arr[i] || {};
|
||||
const community = String(it.community || '').trim();
|
||||
const gateway = String(it.gateway || '').trim();
|
||||
const description = String(it.description || '').trim();
|
||||
const extra = { ...it };
|
||||
delete extra.community;
|
||||
delete extra.gateway;
|
||||
delete extra.description;
|
||||
ins.run(scope, i, community, gateway, description, JSON.stringify(extra), now);
|
||||
}
|
||||
db.prepare('INSERT OR REPLACE INTO app_meta (key, value) VALUES (?, ?)').run(
|
||||
`filter_rules_mtime_${scope}`,
|
||||
String(now),
|
||||
);
|
||||
db.prepare('DELETE FROM blob_versions WHERE object_key = ?').run(objectKey);
|
||||
db.prepare('DELETE FROM blobs WHERE object_key = ?').run(objectKey);
|
||||
};
|
||||
db.transaction(run)();
|
||||
};
|
||||
|
||||
migrateBlobToRules('filters.json', 'global');
|
||||
migrateBlobToRules('filter-manager/simple-filters.json', 'simple');
|
||||
}
|
||||
|
||||
/**
|
||||
* @returns {import('better-sqlite3').Database}
|
||||
*/
|
||||
function openDatabase() {
|
||||
if (_db) return _db;
|
||||
|
||||
const filePath = getDbPath();
|
||||
const db = new Database(filePath);
|
||||
|
||||
db.pragma('journal_mode = WAL');
|
||||
db.pragma('synchronous = NORMAL');
|
||||
db.pragma('foreign_keys = ON');
|
||||
db.pragma(`busy_timeout = ${Number(process.env.SQLITE_BUSY_MS) || 8000}`);
|
||||
const cacheKb = Number(process.env.SQLITE_CACHE_KB);
|
||||
if (Number.isFinite(cacheKb) && cacheKb !== 0) {
|
||||
db.pragma(`cache_size = ${-Math.abs(Math.floor(cacheKb))}`);
|
||||
}
|
||||
|
||||
let userVersion = db.pragma('user_version', { simple: true });
|
||||
if (userVersion < 1) {
|
||||
applyMigration001(db);
|
||||
db.pragma('user_version = 1');
|
||||
const now = Date.now();
|
||||
db.prepare('INSERT OR IGNORE INTO schema_migrations (version, applied_at) VALUES (1, ?)').run(now);
|
||||
userVersion = 1;
|
||||
}
|
||||
if (userVersion < 2) {
|
||||
applyMigration002(db);
|
||||
db.pragma('user_version = 2');
|
||||
const now2 = Date.now();
|
||||
db.prepare('INSERT OR IGNORE INTO schema_migrations (version, applied_at) VALUES (2, ?)').run(now2);
|
||||
}
|
||||
|
||||
_db = db;
|
||||
|
||||
if (!_cachePurgeTimer) {
|
||||
const intervalMs = Math.max(60_000, Number(process.env.SQLITE_CACHE_PURGE_MS) || 300_000);
|
||||
_cachePurgeTimer = setInterval(purgeExpiredCacheEntries, intervalMs);
|
||||
_cachePurgeTimer.unref?.();
|
||||
}
|
||||
|
||||
return db;
|
||||
}
|
||||
|
||||
function purgeExpiredCacheEntries() {
|
||||
const db = openDatabase();
|
||||
const now = Date.now();
|
||||
try {
|
||||
db.prepare('DELETE FROM cache_entries WHERE expires_at IS NOT NULL AND expires_at < ?').run(now);
|
||||
} catch (_) {}
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
openDatabase,
|
||||
getDbPath,
|
||||
purgeExpiredCacheEntries,
|
||||
};
|
||||
Reference in New Issue
Block a user