fs/file
Reading and writing files, file information, copying and moving, and MD5 and SHA digests.
import fs.file
The fs.file package is the fullest set of tools for working with one file: read it whole or as lines, write, append, replace it safely, copy, move and delete it, look up its size, times and permissions, hash it and guess its MIME type. Its copy, move, delete and atomic write functions record failures in os.error; reads and plain writes do not, so check what they return. For folders use fs.dir, for paths fs.path, and for streaming a large file the handle functions of io.
Writes return the number of bytes written, or -1. Reads return empty text when the file cannot be opened, the same as for an empty file; ask Exists when the difference matters.
What people get wrong:
- Treating it as binary-safe. Every read and write goes through a
str, which ends at the first zero byte, soReadAll,ReadBytes,WriteBytesand the digest functions cut a binary file short. For images, archives and other binary data useReadBinaryFileandWriteBinaryFilefrom archive, which carry a pointer and a length, and the pointer forms in crypto such asSha256Bytes. - Expecting
Moveto cross disks. It is a rename and fails between file systems;fs.dir.Movefalls back to copying, or copy and delete yourself. - Assuming
AtomicWritekeeps the old file's permissions. It writes a new file, which gets the default permissions; a file you had narrowed to0o600becomes readable again unless youChmodit after each save. - Using
Existsto mean "is a file". It is true for directories too; combine it withfs.dir.IsDir.
Read a settings file line by line
ReadLines splits a file at its line breaks and drops a trailing \r, so a file saved on Windows reads the same as one saved on Linux. That makes simple key = value files easy: trim each line, skip blank lines and comments, and split at the first =.
import fs.file
import str
func main:
file.WriteText("app.conf", "# shop settings\nname = Corner Shop\n\nport = 8080\ncurrency = IRR\n")
settings := HashMap {} as HashMap<str, str>
each line in file.ReadLines("app.conf"):
t := line.trim()
if t.len() == 0 or str.StartsWith(t, "#"): continue end
eq_at := str.IndexOf(t, "=")
if eq_at < 0: continue end
settings.put(t.substr(0, eq_at).trim(), t.substr(eq_at + 1, t.len() - eq_at - 1).trim())
end
println settings.size(), "settings"
println "name:", settings.get("name")
println "port:", settings.get("port")
file.Delete("app.conf")
endFor a format with sections, quoting or nesting, use a real parser such as encoding.toml, encoding.json or encoding.envfile.
Append to a log
Append opens the file, adds the text at the end and closes it again, and creates the file on the first call. Each call is a separate open and close, which is fine for a line now and then. For a busy log, open the file once with Open(p, "ab"), write through io.Write and Close it at the end.
import fs.file
func log(line: str):
file.Append("app.log", line.concat("\n"))
end
func main:
log("started")
log("loaded 3 orders")
log("sent invoice 1042")
log("stopped")
lines := file.ReadLines("app.log")
println "log has", lines.len(), "lines,", file.Size("app.log"), "bytes"
println "last two:"
mut i := lines.len() - 2
until i < lines.len():
println " ", lines.get(i)
i += 1
end
file.Delete("app.log")
endCheck that a file arrived intact
Download pages often publish a SHA-256 digest next to each file. Compute the digest of your copy with Sha256Hex and compare the two: a change of even one byte gives a completely different digest. Md5Hex and Sha1Hex are fine for noticing accidental changes, such as a cache key, but anyone can build two different files with the same MD5 or SHA-1 on purpose, so do not trust them against an attacker.
import fs.file
func verify(p: str): bool:
want := file.ReadText(p.concat(".sha256")).trim()
ret file.Sha256Hex(p) == want
end
func main:
file.WriteBytes("release.tar", "pretend this is a release archive")
file.WriteText("release.tar.sha256", file.Sha256Hex("release.tar").concat("\n"))
println "intact:", verify("release.tar")
file.Append("release.tar", "!")
println "after a change:", verify("release.tar")
file.Delete("release.tar")
file.Delete("release.tar.sha256")
endThe digest functions read the file into a string first, and a Salam string ends at the first zero byte. For text files that does not matter. A binary file that contains a zero byte, which most archives and images do, is hashed only up to that byte, so check such files with the system sha256sum or with the buffer functions in crypto, such as Sha256Bytes. The same limit applies to ReadAll, ReadText, ReadBytes and WriteBytes.
Save without losing the old version
AtomicWrite writes the new content to name.tmp next to the target and then renames it over the target in one step. A program that crashes in the middle leaves either the old file or the new one, never half of each. It does not flush the data to the disk first, so after a power cut a file saved a moment earlier can still be lost on some file systems, and two programs saving the same file at once share the same .tmp name. Copying the old file to .bak first also keeps one previous version. When the folder does not exist, AtomicWrite returns -1 and leaves its message in os.error.
import fs.file
import os.error
func save(p: str, data: str): bool:
if file.Exists(p): file.Copy(p, p.concat(".bak")) end
if file.AtomicWrite(p, data) != 0:
println "save failed:", error.LastMessage()
ret false
end
ret true
end
func main:
save("profile.json", "{\"name\": \"Sara\"}")
save("profile.json", "{\"name\": \"Sara\", \"city\": \"Tabriz\"}")
println "current:", file.ReadText("profile.json")
println "backup: ", file.ReadText("profile.json.bak")
println "temp file left:", file.Exists("profile.json.tmp")
save("no-such-dir/profile.json", "{}")
file.Delete("profile.json")
file.Delete("profile.json.bak")
endServe files with the right type
A web server has to send a Content-Type with each file. MimeType picks it from the extension alone, without upper and lower case mattering, and knows the common web, image, audio, video and archive types; anything else is application/octet-stream, which browsers download instead of showing. Extension keeps the dot and the original case, and is empty when the name has no extension.
import fs.file
func main:
each name in ["index.html", "app.js", "logo.SVG", "photo.jpeg", "data.json", "notes", "backup.7z"]:
ext := file.Extension(name)
if ext == "": println name, "(no extension)", file.MimeType(name)
else: println name, ext, file.MimeType(name)
end
end
endAn HTTP server usually wants net.mime instead: its MimeType adds ; charset=utf-8 to text types and knows web fonts, and the package also parses Content-Type headers and multipart bodies.
Which function for which job
| Job | Function |
|---|---|
| Read a small text file whole | ReadText, or ReadLines for a vector of lines |
| Read only the start of a file | ReadBytes |
| Replace a file's content | WriteText, or AtomicWrite when the file matters |
| Add to the end of a file | Append |
| Work through a big file piece by piece | Open, then the io functions, then Close |
| Size, times and permissions in one call | Stat |
| Duplicate, move or delete | Copy, Move (or Rename), Delete |
| Check a download | Sha256Hex |
| Content type for a web response | MimeType |
Types
FileInfo
struct FileInfo| Name | Type | Description |
|---|---|---|
size | i64 = 0 | The size in bytes. |
mod_time | i64 = 0 | When it was last changed. |
accessed_time | i64 = 0 | When it was last read. |
created_time | i64 = 0 | When it was created. |
permissions | int = 0 | The permissions, as octal digits such as 644. |
exists | bool = false | True when the path exists (a directory counts); the other fields are 0 when it does not. |
import fs.file
func main:
file.WriteText("picture.png", "PNG")
info := file.Stat("picture.png")
println info.exists, info.size, info.mod_time > 0, info.permissions > 0
println file.Size("picture.png"), file.Exists("picture.png")
println file.ModTime("picture.png") > 0, file.AccessedTime("picture.png") > 0, file.CreatedTime("picture.png") >= 0
file.Chmod("picture.png", 0o600)
println file.Permissions("picture.png")
println file.Extension("picture.png"), file.MimeType("picture.png"), file.MimeType("letter.pdf")
println file.Stat("missing.txt").exists
endFunctions
Open
func Open(path: str, mode: str): File*import fs.file
func main:
file.WriteText("poem.txt", "The children of Adam are limbs of one body\n")
file.Append("poem.txt", "having been created of one essence\n")
print file.ReadText("poem.txt")
println file.ReadAll("poem.txt") == file.ReadText("poem.txt")
lines := file.ReadLines("poem.txt")
println lines.len(), lines.get(1)
file.WriteBytes("data.bin", "ABCDEF")
println file.ReadBytes("data.bin", 3)
file.AtomicWrite("settings.txt", "language=english")
println file.ReadText("settings.txt")
f := file.Open("poem.txt", "rb")
println f.readline()
file.Close(f)
endClose
func Close(f: File*)The example for this item is shown with the ones above: fs-file-ReadText.salam
ReadText
func ReadText(path: str): strThe example for this item is shown with the ones above: fs-file-ReadText.salam
ReadAll
func ReadAll(path: str): strReadText. The result is a str, so a binary file is cut at its first zero byte.The example for this item is shown with the ones above: fs-file-ReadText.salam
ReadBytes
func ReadBytes(path: str, n: int): strn bytes of the file, such as a header to recognise its format; cut short at a zero byte.The example for this item is shown with the ones above: fs-file-ReadText.salam
ReadLines
func ReadLines(path: str): Vector<str>\r removed.The example for this item is shown with the ones above: fs-file-ReadText.salam
WriteText
func WriteText(path: str, data: str): intThe example for this item is shown with the ones above: fs-file-ReadText.salam
WriteBytes
func WriteBytes(path: str, data: str): intWriteText. The data is a str, so it stops at the first zero byte and is not safe for binary content.The example for this item is shown with the ones above: fs-file-ReadText.salam
Append
func Append(path: str, data: str): intThe example for this item is shown with the ones above: fs-file-ReadText.salam
AtomicWrite
func AtomicWrite(path: str, data: str): intpath.tmp and renames it over the file, so an interrupted program leaves either the complete new version or the old one, never a half written file. Returns 0 or -1 (with an os.error record). The new file gets default permissions, and the data is not flushed to disk first.The example for this item is shown with the ones above: fs-file-ReadText.salam
Size
func Size(p: str): i64The example for this item is shown with the ones above: fs-file-Stat.salam
ModTime
func ModTime(p: str): i64The example for this item is shown with the ones above: fs-file-Stat.salam
AccessedTime
func AccessedTime(p: str): i64The example for this item is shown with the ones above: fs-file-Stat.salam
CreatedTime
func CreatedTime(p: str): i64The example for this item is shown with the ones above: fs-file-Stat.salam
Permissions
func Permissions(p: str): intThe example for this item is shown with the ones above: fs-file-Stat.salam
Chmod
func Chmod(p: str, mode: int): int0o600.The example for this item is shown with the ones above: fs-file-Stat.salam
Extension
func Extension(p: str): str.JPG; empty when there is none.The example for this item is shown with the ones above: fs-file-Stat.salam
MimeType
func MimeType(p: str): strapplication/octet-stream for an unknown extension. The file is not opened.The example for this item is shown with the ones above: fs-file-Stat.salam
Stat
func Stat(p: str): FileInfostat command four times, so it is not faster than the single calls.The example for this item is shown with the ones above: fs-file-Stat.salam
Copy
func Copy(src: str, dst: str): intimport fs.file
func main:
file.WriteText("original.txt", "hello")
file.Copy("original.txt", "copy.txt")
file.Move("copy.txt", "moved.txt")
file.Rename("moved.txt", "final.txt")
println file.Exists("copy.txt"), file.Exists("final.txt"), file.ReadText("final.txt")
file.Delete("final.txt")
println file.Exists("final.txt")
endMove
func Move(src: str, dst: str): intfs.dir.Move can copy instead.The example for this item is shown with the ones above: fs-file-Copy.salam
Rename
func Rename(src: str, dst: str): intMove.The example for this item is shown with the ones above: fs-file-Copy.salam
Delete
func Delete(p: str): intos.error as FileNotFound, a missing permission included.The example for this item is shown with the ones above: fs-file-Copy.salam
Exists
func Exists(p: str): boolThe example for this item is shown with the ones above: fs-file-Stat.salam
Md5Hex
func Md5Hex(p: str): strimport fs.file
func main:
file.WriteText("message.txt", "abc")
println file.Md5Hex("message.txt")
println file.Sha1Hex("message.txt")
println file.Sha256Hex("message.txt")
println file.Sha512Hex("message.txt").len()
endSha1Hex
func Sha1Hex(p: str): strThe example for this item is shown with the ones above: fs-file-Sha256Hex.salam
Sha256Hex
func Sha256Hex(p: str): strThe example for this item is shown with the ones above: fs-file-Sha256Hex.salam
Sha512Hex
func Sha512Hex(p: str): strThe example for this item is shown with the ones above: fs-file-Sha256Hex.salam