// 元数据落盘的统一入口。 // // rename 只保证目录项替换是原子的,不保证被替换的数据已经到磁盘: // 断电后可能出现"改名成功但文件内容是一段空洞"的结果,长度正常、内容全零。 // 因此改名前必须先 fsync 数据本身,改名后再 fsync 目录让目录项落盘。 // Windows 不允许对目录取句柄,那一步失败时忽略即可。 const fs = require('fs'); const path = require('path'); function writeSynced(file, data, encoding = 'utf8') { const fd = fs.openSync(file, 'w'); try { fs.writeFileSync(fd, data, encoding === null ? undefined : { encoding }); fs.fsyncSync(fd); } finally { fs.closeSync(fd); } } function syncDirectory(dir) { let fd = null; try { fd = fs.openSync(dir, 'r'); fs.fsyncSync(fd); } catch (e) { // Windows 无法 fsync 目录;其它平台失败也不该让写入整体失败 } finally { if (fd !== null) { try { fs.closeSync(fd); } catch (e) { /* ignore */ } } } } // 写入 dest,保留一份 .bak 以便下次读取时恢复。 // 失败时清理临时文件,并在目标已被改走时把备份换回去。 function writeJson(dest, value) { const temp = `${dest}.tmp`; const backup = `${dest}.bak`; let backedUp = false; fs.mkdirSync(path.dirname(dest), { recursive: true }); try { writeSynced(temp, JSON.stringify(value, null, 2)); if (fs.existsSync(backup)) fs.unlinkSync(backup); if (fs.existsSync(dest)) { fs.renameSync(dest, backup); backedUp = true; } fs.renameSync(temp, dest); syncDirectory(path.dirname(dest)); if (backedUp) { try { fs.unlinkSync(backup); } catch (e) { /* 保留备份不影响写入结果 */ } } } catch (e) { try { if (fs.existsSync(temp)) fs.unlinkSync(temp); } catch (cleanup) { /* ignore */ } try { if (backedUp && !fs.existsSync(dest) && fs.existsSync(backup)) fs.renameSync(backup, dest); } catch (rollback) { /* 下次读取时从 .bak 恢复 */ } throw e; } } // 二进制落地(封面等)。已存在则不覆盖,由调用方决定语义。 function writeBytesExclusive(dest, bytes) { const temp = `${dest}.tmp`; fs.mkdirSync(path.dirname(dest), { recursive: true }); const fd = fs.openSync(temp, 'wx'); try { fs.writeFileSync(fd, bytes); fs.fsyncSync(fd); } finally { fs.closeSync(fd); } fs.renameSync(temp, dest); syncDirectory(path.dirname(dest)); } module.exports = { writeJson, writeSynced, syncDirectory, writeBytesExclusive };