#fs
Filesystem helpers built on the host file primitives.
The primitives are in scope without an import: readFile, writeFile,
appendFile, readFileBytes, writeFileBytes, fileExists, listDir,
makeDir, removeFile, rename, removeDir, statFile, and
canonicalizePath. This module adds a FileStat record over
statFile's tuple and the composed operations copyFile,
replaceDurably, mkdirAll, mkdirAllDurably, walkDir, isDir,
isFile, and fileSize.
Every operation returns Result String a, with the host's error message
in Err. File operations run only in a built program, not under the
interpreter.
#Metadata
#FileStat
data FileStat
= FileStat { size : Int, isDir : Bool, isFile : Bool, mtime : Float }What stat reports about a path: its size in bytes, whether it is a
directory, whether it is a regular file, and its modification time in
seconds since the Unix epoch.
Instances: Eq, Debug
#stat
stat : (path : String) -> <FileRead path> Result String FileStat
stat pThe metadata of a path as a FileStat, or Err when the path cannot
be examined, for instance because it does not exist.
#isDir
isDir : (path : String) -> <FileRead path> Result String Bool
isDir pWhether a path exists and is a directory.
#isFile
isFile : (path : String) -> <FileRead path> Result String Bool
isFile pWhether a path exists and is a regular file.
#fileSize
fileSize : (path : String) -> <FileRead path> Result String Int
fileSize pThe size of a file in bytes.
#Operations
#copyFile
copyFile : (src : String) -> (dst : String) -> <FileRead src, FileWrite dst> Result String Unit
copyFile src dstCopies the bytes of src to dst, replacing any existing dst.
A read failure is reported before anything is written.
#replaceDurably
replaceDurably : String -> String -> String -> <FileWrite> Result String Unit
replaceDurably staged target contentReplaces target with content so that a crash leaves either the
old file or the new one, never a partial write.
The content is written to staged at io.ownerOnlyMode (0600),
staged is flushed, renamed onto target, and then the directory that
holds target is flushed, in that order. The first step that fails
returns its Err and the rest do not run. The mode is set before any
byte is written, so neither the umask nor a leftover staged at a wider
mode can widen it.
staged must be on the same filesystem as target. A crash before the
rename leaves staged behind, so a caller that stages into a directory
it later lists must remove that residue itself. The directory holding
staged is not flushed, and neither is the entry of target's
directory in its own parent; mkdirAllDurably covers that.
> replaceDurably "stdlib/no-such-doctest-dir/r.tmp" "stdlib/no-such-doctest-dir/r" "v1"
Err "No such file or directory"#mkdirAll
mkdirAll : String -> <FileWrite> Result String Unit
mkdirAll pathCreates a directory and every missing parent, like mkdir -p.
A directory that already exists is not an error.
#mkdirAllDurably
mkdirAllDurably : String -> <FileRead, FileWrite> Result String Unit
mkdirAllDurably pathCreates a directory and every missing parent, like mkdirAll, and
makes each directory it creates durable by flushing that directory's
parent right after creating it.
A path that already exists is left alone and nothing is flushed, so a call on an existing directory costs one existence check.
Known limit: a directory that already exists is taken to be durable. That is false for a directory whose creator stopped between creating it and flushing its parent. A later call finds it present and skips the flush, so a crash after that can still lose it. Closing the gap would mean flushing the parent on every call.
> mkdirAllDurably "stdlib"
Ok ()
> mkdirAllDurably "stdlib/fs.mdk/sub"
Err "Not a directory"#walkDir
walkDir : String -> <FileRead> Result String (List String)
walkDir rootEvery path under a directory, files and subdirectories both, depth first.
Each result is the full path, joined onto root. Err on the first
directory that cannot be read or entry that cannot be examined.
#fixtureFiles
fixtureFiles : String -> <FileRead> Result String (List String)
fixtureFiles rootEvery regular file under root, depth first, or Err when there are
none.
walkDir with directories filtered out. A directory that reads cleanly
but holds no files is an Err, so a test that iterates over a fixture
directory cannot pass by iterating over nothing. Every result is a path
under root.
> fixtureFiles "stdlib/no-such-fixture-doctest-dir"
Err "No such file or directory"
> map (all (contains "/effect_set_fixtures/")) (fixtureFiles "test/effect_set_fixtures")
Ok True#fixtureDirs
fixtureDirs : String -> <FileRead> Result String (List String)
fixtureDirs rootEvery top-level subdirectory of root, or Err when there are none.
Not recursive. For a corpus where each fixture is a whole directory
rather than a single file; fixtureFiles filters directories out. A
directory with no subdirectories is an Err, as for fixtureFiles.
Every result is a path under root.
> fixtureDirs "stdlib/no-such-fixture-doctest-dir"
Err "No such file or directory"
> map (all (contains "/import_order_fixtures/")) (fixtureDirs "test/import_order_fixtures")
Ok True#expectUnitCount
expectUnitCount : Int -> List a -> Result String Unit
expectUnitCount want unitsOk when units has exactly want elements, otherwise an Err
naming both counts.
A floor for a corpus with no roster to check against: it catches a
corpus that grew or shrank, but not which unit changed. When a roster
exists, test_process.unrosteredUnits and test_process.missingUnits
say which.
> expectUnitCount 2 ["a", "b"]
Ok ()
> expectUnitCount 3 ["a", "b"]
Err "expected 3 units, found 2"