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
|
||||
};
|
||||
Reference in New Issue
Block a user