fix(storage): protect Yomitan profiles across Electron runtime changes (#211)

This commit is contained in:
sudacode authored and GitHub committed 2026-10-01 21:44:53 -07:00
1 parent 15cb9fe918
commit a5d0b63792
22 files changed
+934 -21

No files matched your search

+46
View File
@@ -1,5 +1,6 @@
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;
@@ -12,3 +13,48 @@ export function ensureDirForFile(filePath: string): void {
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;
}
}