import { createHash } from "node:crypto"; import fs from "node:fs/promises"; import path from "node:path"; export function sha256(content: string): string { // 개행 정규화 — Windows 체크아웃(autocrlf)에서 해시가 흔들리는 것을 막는다 return createHash("sha256").update(content.replace(/\r\n/g, "\n"), "utf8").digest("hex"); } export async function exists(p: string): Promise { try { await fs.access(p); return true; } catch { return false; } } export async function readIfExists(p: string): Promise { try { return await fs.readFile(p, "utf8"); } catch { return null; } } /** 임시 파일에 쓴 뒤 rename — 중간에 죽어도 반쯤 쓰인 파일이 남지 않는다 */ export async function writeAtomic(p: string, content: string): Promise { await fs.mkdir(path.dirname(p), { recursive: true }); const tmp = `${p}.${process.pid}.tmp`; await fs.writeFile(tmp, content, "utf8"); await fs.rename(tmp, p); } /** 사용자가 고친 파일을 덮기 전에 원본을 남겨둔다 */ export async function backup(p: string): Promise { const cur = await readIfExists(p); if (cur === null) return null; const dest = `${p}.orig`; await fs.writeFile(dest, cur, "utf8"); return dest; } /** 파일을 지우고, 비게 된 상위 디렉터리를 stopAt 까지 정리한다 */ export async function removeFileAndPrune(p: string, stopAt: string): Promise { await fs.rm(p, { force: true }); let dir = path.dirname(p); const stop = path.resolve(stopAt); while (path.resolve(dir).startsWith(stop) && path.resolve(dir) !== stop) { try { const rest = await fs.readdir(dir); if (rest.length > 0) break; await fs.rmdir(dir); } catch { break; } dir = path.dirname(dir); } // stopAt 자체도 비었으면 함께 정리한다 try { if ((await fs.readdir(stop)).length === 0) await fs.rmdir(stop); } catch { /* 남아있으면 그대로 둔다 */ } }