atomic.js 2.1 KB

12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970
  1. import { promises as fs } from 'node:fs'
  2. import path from 'node:path'
  3. let counter = 0
  4. /**
  5. * 原子批量写:全部先落 .tmp,再备份旧文件并 rename,任一失败回滚。
  6. * 同目录 rename 原子(同卷),保证多文件"要么全成要么原样"(spec error-handling §3.1)。
  7. * @param {string} repoPath
  8. * @param {Array<{path: string, content: string}>} files 相对路径
  9. * @returns {Promise<string[]>} 已写的相对路径
  10. */
  11. export async function writeAtomicBatch(repoPath, files) {
  12. const seen = new Set()
  13. const plans = []
  14. try {
  15. for (const f of files) {
  16. if (seen.has(f.path)) throw new Error(`批量写入包含重复路径:${f.path}`)
  17. seen.add(f.path)
  18. const full = path.join(repoPath, f.path)
  19. await fs.mkdir(path.dirname(full), { recursive: true })
  20. const n = counter++
  21. const tmp = `${full}.wnwtmp.${process.pid}.${n}`
  22. const backup = `${full}.wnwbackup.${process.pid}.${n}`
  23. const plan = { tmp, final: full, backup, existed: false, rel: f.path }
  24. plans.push(plan)
  25. await fs.writeFile(tmp, f.content, 'utf8')
  26. try {
  27. const stat = await fs.stat(full)
  28. if (stat.isDirectory()) throw new Error(`目标路径是目录,不能写入文件:${f.path}`)
  29. await fs.rename(full, backup)
  30. plan.existed = true
  31. } catch (err) {
  32. if (err.code !== 'ENOENT') throw err
  33. }
  34. }
  35. for (const p of plans) {
  36. await fs.rename(p.tmp, p.final)
  37. }
  38. for (const p of plans) {
  39. if (p.existed) await fs.rm(p.backup, { force: true })
  40. }
  41. } catch (err) {
  42. for (const p of plans.toReversed()) {
  43. await restorePlan(p)
  44. }
  45. throw err
  46. }
  47. return plans.map((p) => p.rel)
  48. }
  49. async function restorePlan(plan) {
  50. try {
  51. await fs.rm(plan.tmp, { force: true })
  52. } catch {
  53. // 尽力回滚
  54. }
  55. try {
  56. if (plan.existed) {
  57. await fs.rm(plan.final, { force: true })
  58. await fs.rename(plan.backup, plan.final)
  59. } else {
  60. await fs.rm(plan.final, { force: true })
  61. }
  62. } catch {
  63. // 尽力回滚;调用方会收到原始错误
  64. }
  65. }