Files
SubMiner/src/shared/fs-utils.ts
T

61 lines
1.9 KiB
TypeScript

import fs from 'node:fs';
import path from 'node:path';
import { randomUUID } from 'node:crypto';
export function ensureDir(dirPath: string): void {
if (fs.existsSync(dirPath)) return;
fs.mkdirSync(dirPath, { recursive: true });
}
export function ensureDirForFile(filePath: string): void {
const dir = path.dirname(filePath);
if (!fs.existsSync(dir)) {
fs.mkdirSync(dir, { recursive: true });
}
}
function syncDirectory(directoryPath: string): void {
// Node cannot open Windows directory handles for fsync. The atomic rename still prevents
// torn files there, while supported platforms also flush the directory entry below.
if (process.platform === 'win32') return;
const directory = fs.openSync(directoryPath, 'r');
try {
fs.fsyncSync(directory);
} finally {
fs.closeSync(directory);
}
}
/** Writes and flushes a sibling temporary file before atomically replacing the target. */
export function writeTextFileAtomicallyDurable(filePath: string, content: string): void {
const directoryPath = path.dirname(filePath);
ensureDir(directoryPath);
const temporaryPath = path.join(directoryPath, `.${path.basename(filePath)}.${randomUUID()}.tmp`);
let temporaryFile: number | undefined;
try {
temporaryFile = fs.openSync(temporaryPath, 'wx', 0o600);
fs.writeFileSync(temporaryFile, content, 'utf8');
fs.fsyncSync(temporaryFile);
fs.closeSync(temporaryFile);
temporaryFile = undefined;
fs.renameSync(temporaryPath, filePath);
syncDirectory(directoryPath);
} catch (error) {
if (temporaryFile !== undefined) {
try {
fs.closeSync(temporaryFile);
} catch {
// Best effort cleanup preserves the original write failure.
}
}
try {
fs.rmSync(temporaryPath, { force: true });
} catch {
// The temporary file is disposable; the existing target stays untouched before rename.
}
throw error;
}
}