initialize generic rms-software repository
Add the reusable RMS core application (server, web UI, plugins, tests, tools) with generic defaults, GPL licensing, and maintainer context documentation so deployments can consume this repo as software source independent of station-specific overlays.
This commit is contained in:
102
server/storage/providers/json.js
Normal file
102
server/storage/providers/json.js
Normal file
@@ -0,0 +1,102 @@
|
||||
const fs = require("fs");
|
||||
const fsp = require("fs/promises");
|
||||
|
||||
function createJsonStorage(options) {
|
||||
const dataDir = options.dataDir;
|
||||
|
||||
return {
|
||||
id: "json",
|
||||
async init() {
|
||||
await fsp.mkdir(dataDir, { recursive: true });
|
||||
},
|
||||
async exists(key) {
|
||||
return fs.existsSync(key);
|
||||
},
|
||||
async readJson(key, fallback = null) {
|
||||
try {
|
||||
const raw = await fsp.readFile(key, "utf8");
|
||||
const parsed = parseJsonWithTrailingRecovery(raw);
|
||||
if (parsed.ok) {
|
||||
if (parsed.repaired) {
|
||||
await backupCorruptJsonFile(key, raw, "trailing-garbage");
|
||||
await fsp.writeFile(key, `${JSON.stringify(parsed.value, null, 2)}\n`, "utf8");
|
||||
console.warn(`[json-storage] repaired trailing JSON data in ${key}`);
|
||||
}
|
||||
return parsed.value;
|
||||
}
|
||||
|
||||
if (fallback !== undefined) {
|
||||
await backupCorruptJsonFile(key, raw, "invalid-json");
|
||||
await fsp.writeFile(key, `${JSON.stringify(fallback, null, 2)}\n`, "utf8");
|
||||
console.warn(`[json-storage] invalid JSON in ${key}, restored fallback`);
|
||||
return fallback;
|
||||
}
|
||||
throw parsed.error;
|
||||
} catch (error) {
|
||||
if (error.code === "ENOENT") {
|
||||
return fallback;
|
||||
}
|
||||
throw error;
|
||||
}
|
||||
},
|
||||
async writeJson(key, value) {
|
||||
await fsp.writeFile(key, `${JSON.stringify(value, null, 2)}\n`, "utf8");
|
||||
},
|
||||
async appendText(key, text) {
|
||||
await fsp.appendFile(key, text, "utf8");
|
||||
},
|
||||
async readText(key, fallback = "") {
|
||||
try {
|
||||
return await fsp.readFile(key, "utf8");
|
||||
} catch (error) {
|
||||
if (error.code === "ENOENT") {
|
||||
return fallback;
|
||||
}
|
||||
throw error;
|
||||
}
|
||||
},
|
||||
async writeText(key, text) {
|
||||
await fsp.writeFile(key, text, "utf8");
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
function parseJsonWithTrailingRecovery(raw) {
|
||||
try {
|
||||
return { ok: true, repaired: false, value: JSON.parse(raw) };
|
||||
} catch (error) {
|
||||
const message = String(error && error.message ? error.message : "");
|
||||
const match = message.match(/position\s+(\d+)/i);
|
||||
if (!match) {
|
||||
return { ok: false, error };
|
||||
}
|
||||
const position = Number(match[1]);
|
||||
if (!Number.isFinite(position) || position <= 0 || position >= raw.length) {
|
||||
return { ok: false, error };
|
||||
}
|
||||
const prefix = raw.slice(0, position).trimEnd();
|
||||
if (!prefix) {
|
||||
return { ok: false, error };
|
||||
}
|
||||
try {
|
||||
return { ok: true, repaired: true, value: JSON.parse(prefix) };
|
||||
} catch {
|
||||
return { ok: false, error };
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async function backupCorruptJsonFile(key, raw, reason) {
|
||||
try {
|
||||
const stamp = new Date().toISOString().replace(/[:.]/g, "-");
|
||||
const suffix = String(reason || "corrupt").replace(/[^a-z0-9-]/gi, "-").toLowerCase();
|
||||
const backupPath = `${key}.${suffix}.${stamp}.bak`;
|
||||
await fsp.writeFile(backupPath, raw, "utf8");
|
||||
} catch {
|
||||
// best effort backup only
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
createJsonStorage
|
||||
};
|
||||
66
server/storage/providers/sqlite.js
Normal file
66
server/storage/providers/sqlite.js
Normal file
@@ -0,0 +1,66 @@
|
||||
const fs = require("fs");
|
||||
const path = require("path");
|
||||
|
||||
function createSqliteStorage(options) {
|
||||
const sqlitePath = options.sqlitePath;
|
||||
let db = null;
|
||||
|
||||
function requireDriver() {
|
||||
try {
|
||||
return require("node:sqlite").DatabaseSync;
|
||||
} catch {
|
||||
throw new Error("SQLite storage requires Node.js with built-in 'node:sqlite' (Node 22+)");
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
id: "sqlite",
|
||||
async init() {
|
||||
const Database = requireDriver();
|
||||
fs.mkdirSync(path.dirname(sqlitePath), { recursive: true });
|
||||
db = new Database(sqlitePath);
|
||||
db.exec("PRAGMA journal_mode = WAL");
|
||||
db.exec("CREATE TABLE IF NOT EXISTS kv (k TEXT PRIMARY KEY, v TEXT NOT NULL)");
|
||||
db.exec("CREATE TABLE IF NOT EXISTS logs (k TEXT NOT NULL, ts INTEGER NOT NULL, line TEXT NOT NULL)");
|
||||
},
|
||||
async exists(key) {
|
||||
const row = db.prepare("SELECT 1 AS ok FROM kv WHERE k = ? LIMIT 1").get(key);
|
||||
return Boolean(row);
|
||||
},
|
||||
async readJson(key, fallback = null) {
|
||||
const row = db.prepare("SELECT v FROM kv WHERE k = ? LIMIT 1").get(key);
|
||||
if (!row) {
|
||||
return fallback;
|
||||
}
|
||||
try {
|
||||
return JSON.parse(row.v);
|
||||
} catch {
|
||||
return fallback;
|
||||
}
|
||||
},
|
||||
async writeJson(key, value) {
|
||||
const raw = JSON.stringify(value);
|
||||
db.prepare("INSERT INTO kv(k, v) VALUES(?, ?) ON CONFLICT(k) DO UPDATE SET v = excluded.v").run(key, raw);
|
||||
},
|
||||
async appendText(key, text) {
|
||||
db.prepare("INSERT INTO logs(k, ts, line) VALUES(?, ?, ?)").run(key, Date.now(), text);
|
||||
},
|
||||
async readText(key, fallback = "") {
|
||||
const rows = db.prepare("SELECT line FROM logs WHERE k = ? ORDER BY ts ASC").all(key);
|
||||
if (!rows.length) {
|
||||
return fallback;
|
||||
}
|
||||
return rows.map((row) => row.line).join("");
|
||||
},
|
||||
async writeText(key, text) {
|
||||
db.prepare("DELETE FROM logs WHERE k = ?").run(key);
|
||||
if (text) {
|
||||
db.prepare("INSERT INTO logs(k, ts, line) VALUES(?, ?, ?)").run(key, Date.now(), text);
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
createSqliteStorage
|
||||
};
|
||||
Reference in New Issue
Block a user