refactor(#5): share network validation and persistence runtime

This commit is contained in:
Josh Creek
2026-02-25 21:56:13 +00:00
parent b5d34456af
commit 5954f63f22
5 changed files with 633 additions and 868 deletions
+165
View File
@@ -0,0 +1,165 @@
import { constants } from 'node:fs';
import {
access,
copyFile,
mkdir,
open,
readFile,
readdir,
rename,
stat,
unlink
} from 'node:fs/promises';
import path from 'node:path';
/**
* @typedef {{ machines: unknown[]; devices: unknown[] }} NetworkData
* @typedef {{ source: string; updatedAt: string }} NetworkFileMetadata
* @typedef {{ data: NetworkData } & NetworkFileMetadata} NetworkFileReadResult
*/
const NETWORK_READ_ONLY = process.env.NETWORK_READ_ONLY === 'true';
const DATA_FILE_DEFAULT = path.resolve(process.cwd(), 'data/network.json');
const BACKUP_DIR_DEFAULT = path.resolve(process.cwd(), 'data/.backups');
/**
* @returns {string}
*/
function resolveDataFilePath() {
return path.resolve(process.env.NETWORK_DATA_FILE ?? DATA_FILE_DEFAULT);
}
/**
* @returns {string}
*/
function resolveBackupDirectory() {
return path.resolve(process.env.NETWORK_BACKUP_DIR ?? BACKUP_DIR_DEFAULT);
}
/**
* @returns {boolean}
*/
export function isWriteEnabled() {
return !NETWORK_READ_ONLY;
}
/**
* @param {string} dataFilePath
* @returns {Promise<string>}
*/
async function readUpdatedAt(dataFilePath) {
const dataFileStats = await stat(dataFilePath);
return dataFileStats.mtime.toISOString();
}
/**
* @returns {Promise<NetworkFileReadResult>}
*/
export async function readNetworkFile() {
const source = resolveDataFilePath();
const raw = await readFile(source, 'utf8');
const parsed = JSON.parse(raw);
const updatedAt = await readUpdatedAt(source);
return {
data: parsed,
source,
updatedAt
};
}
/**
* @param {string} dataFilePath
* @param {string} backupDirectory
* @returns {Promise<void>}
*/
export async function createBackupIfPresent(dataFilePath, backupDirectory) {
try {
await access(dataFilePath, constants.F_OK);
} catch {
return;
}
await mkdir(backupDirectory, { recursive: true });
const base = path.basename(dataFilePath);
const timestamp = new Date().toISOString().replace(/[:.]/g, '-');
const backupFilePath = path.join(backupDirectory, `${base}.${timestamp}.bak`);
await copyFile(dataFilePath, backupFilePath);
}
/**
* @param {string} backupDirectory
* @param {string} baseName
* @param {number} [keep]
* @returns {Promise<void>}
*/
export async function trimBackups(backupDirectory, baseName, keep = 5) {
/** @type {Array<{ name: string; time: number }>} */
let entries = [];
try {
const files = await readdir(backupDirectory);
entries = await Promise.all(
files
.filter((file) => file.startsWith(`${baseName}.`) && file.endsWith('.bak'))
.map(async (file) => {
const filePath = path.join(backupDirectory, file);
const info = await stat(filePath);
return { name: filePath, time: info.mtimeMs };
})
);
} catch {
return;
}
entries.sort((a, b) => b.time - a.time);
await Promise.all(entries.slice(keep).map((entry) => unlink(entry.name).catch(() => undefined)));
}
/**
* @param {NetworkData} data
* @returns {Promise<NetworkFileMetadata>}
*/
export async function writeNetworkFile(data) {
const source = resolveDataFilePath();
const backupDirectory = resolveBackupDirectory();
const directory = path.dirname(source);
await mkdir(directory, { recursive: true });
await createBackupIfPresent(source, backupDirectory);
const temporaryPath = `${source}.tmp-${Date.now()}-${process.pid}`;
const jsonPayload = `${JSON.stringify(data, null, '\t')}\n`;
const fileHandle = await open(temporaryPath, 'w');
try {
await fileHandle.writeFile(jsonPayload, 'utf8');
await fileHandle.sync();
} finally {
await fileHandle.close();
}
await rename(temporaryPath, source);
await trimBackups(backupDirectory, path.basename(source), 5);
const updatedAt = await readUpdatedAt(source);
return {
source,
updatedAt
};
}
/**
* @returns {Promise<boolean>}
*/
export async function checkWritableState() {
if (!isWriteEnabled()) {
return false;
}
const source = resolveDataFilePath();
const directory = path.dirname(source);
try {
await mkdir(directory, { recursive: true });
await access(directory, constants.W_OK);
} catch {
return false;
}
return true;
}