module

Hwaro::Utils::FileSafe

Class methods

atomic_write(path : String | Path, content : String) : Nil

Write content to path atomically: write to a same-directory temp file, then rename over the target. hwaro serve rewrites output files while HTTP fibers stream them to the browser — a plain File.write truncates first, so a request landing mid-rebuild could read an empty or half-written page. Rename is atomic on the same filesystem, so readers see either the old bytes or the new bytes.

The temp name is unique per process AND fiber so parallel render workers writing sibling outputs can't collide on it.

Source
mkdir_p(path : String | Path, mode : Int32 = 511) : Nil

Equivalent to FileUtils.mkdir_p but tolerates concurrent creation of any path component. Safe to call from MT workers without an external mutex.

We walk parents ourselves so EEXIST is absorbed per component. Crystal's Dir.mkdir_p is exists? → mkdir for each parent and the leaf, so two workers calling mkdir_p("/out/a/b/x") and mkdir_p("/out/a/b/y") can race on every shared parent (/out, /out/a, /out/a/b). A single retry of the whole call isn't enough: the retry's parent walk can re-race on a different shared parent, raise again, and a post-hoc Dir.exists?(leaf) check is false because we never reached the leaf — so the EEXIST bubbles out and a render fails ("Unable to create directory: '…': File exists"). Tolerating EEXIST per component avoids the cascade.

Source