Standard library · System and tools

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, so ReadAll, ReadBytes, WriteBytes and the digest functions cut a binary file short. For images, archives and other binary data use ReadBinaryFile and WriteBinaryFile from archive, which carry a pointer and a length, and the pointer forms in crypto such as Sha256Bytes.
  • Expecting Move to cross disks. It is a rename and fails between file systems; fs.dir.Move falls back to copying, or copy and delete yourself.
  • Assuming AtomicWrite keeps the old file's permissions. It writes a new file, which gets the default permissions; a file you had narrowed to 0o600 becomes readable again unless you Chmod it after each save.
  • Using Exists to mean "is a file". It is true for directories too; combine it with fs.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 =.

fs-file-guide-config.salam
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")
end
Output3 settings name: Corner Shop port: 8080

For 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.

fs-file-guide-log.salam
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")
end
Outputlog has 4 lines, 50 bytes last two: sent invoice 1042 stopped

Check 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.

fs-file-guide-checksum.salam
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")
end
Outputintact: true after a change: false

The 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.

fs-file-guide-save.salam
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")
end
Outputcurrent: {"name": "Sara", "city": "Tabriz"} backup: {"name": "Sara"} temp file left: false save failed: atomic write failed

Serve 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.

fs-file-guide-mime.salam
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
end
Outputindex.html .html text/html app.js .js text/javascript logo.SVG .SVG image/svg+xml photo.jpeg .jpeg image/jpeg data.json .json application/json notes (no extension) application/octet-stream backup.7z .7z application/octet-stream

An 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

JobFunction
Read a small text file wholeReadText, or ReadLines for a vector of lines
Read only the start of a fileReadBytes
Replace a file's contentWriteText, or AtomicWrite when the file matters
Add to the end of a fileAppend
Work through a big file piece by pieceOpen, then the io functions, then Close
Size, times and permissions in one callStat
Duplicate, move or deleteCopy, Move (or Rename), Delete
Check a downloadSha256Hex
Content type for a web responseMimeType

Types

FileInfo

struct FileInfo
Everything about a file.
NameTypeDescription
sizei64 = 0The size in bytes.
mod_timei64 = 0When it was last changed.
accessed_timei64 = 0When it was last read.
created_timei64 = 0When it was created.
permissionsint = 0The permissions, as octal digits such as 644.
existsbool = falseTrue when the path exists (a directory counts); the other fields are 0 when it does not.
fs-file-Stat.salam
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
end
Outputtrue 3 true true 3 true true true true 600 .png image/png application/pdf false

Functions

Open

func Open(path: str, mode: str): File*
Opens a file to work through it piece by piece; the modes are the same as os.Open. Returns null when it cannot.
fs-file-ReadText.salam
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)
end
OutputThe children of Adam are limbs of one body having been created of one essence true 2 having been created of one essence ABC language=english The children of Adam are limbs of one body

Close

func Close(f: File*)
Closes an open file.

The example for this item is shown with the ones above: fs-file-ReadText.salam

ReadText

func ReadText(path: str): str
Reads the whole file as text; empty when the file cannot be opened.

The example for this item is shown with the ones above: fs-file-ReadText.salam

ReadAll

func ReadAll(path: str): str
Reads the whole file, the same as ReadText. 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): str
Reads up to the first n 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>
Returns the file's lines in a vector, without the line breaks and with a trailing \r removed.

The example for this item is shown with the ones above: fs-file-ReadText.salam

WriteText

func WriteText(path: str, data: str): int
Writes text to a file, creating it or replacing what was there; returns the bytes written, or -1.

The example for this item is shown with the ones above: fs-file-ReadText.salam

WriteBytes

func WriteBytes(path: str, data: str): int
The same as WriteText. 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): int
Adds to the end of a file, creating it when missing; returns the bytes written, or -1.

The example for this item is shown with the ones above: fs-file-ReadText.salam

AtomicWrite

func AtomicWrite(path: str, data: str): int
Writes path.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): i64
The size of a file in bytes; -1 when it is missing or is a directory.

The example for this item is shown with the ones above: fs-file-Stat.salam

ModTime

func ModTime(p: str): i64
When it was last changed.

The example for this item is shown with the ones above: fs-file-Stat.salam

AccessedTime

func AccessedTime(p: str): i64
When it was last read.

The example for this item is shown with the ones above: fs-file-Stat.salam

CreatedTime

func CreatedTime(p: str): i64
When it was created.

The example for this item is shown with the ones above: fs-file-Stat.salam

Permissions

func Permissions(p: str): int
The permissions, as octal digits.

The example for this item is shown with the ones above: fs-file-Stat.salam

Chmod

func Chmod(p: str, mode: int): int
Changes the permissions; give the value in octal, such as 0o600.

The example for this item is shown with the ones above: fs-file-Stat.salam

Extension

func Extension(p: str): str
The file's extension with its dot and its original case, such as .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): str
The file's MIME type from its extension, ignoring case, for serving it over the web; application/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): FileInfo
Everything about a file at once. On Unix it runs the system stat 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): int
Copies a file's bytes, replacing the destination; permissions are not copied. 0 or -1.
fs-file-Copy.salam
import 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")
end
Outputfalse true hello false

Move

func Move(src: str, dst: str): int
Moves a file by renaming it, so it fails (-1) between two disks. fs.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): int
Renames a file; the same as Move.

The example for this item is shown with the ones above: fs-file-Copy.salam

Delete

func Delete(p: str): int
Deletes a file; 0 or -1. Any failure is recorded in os.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): bool
True when the path exists, file or directory.

The example for this item is shown with the ones above: fs-file-Stat.salam

Md5Hex

func Md5Hex(p: str): str
The MD5 digest of the file's content; for a quick comparison only, not for security.
fs-file-Sha256Hex.salam
import 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()
end
Output900150983cd24fb0d6963f7d28e17f72 a9993e364706816aba3e25717850c26c9cd0d89d ba7816bf8f01cfea414140de5dae2223b00361a396177a9cb410ff61f20015ad 128

Sha1Hex

func Sha1Hex(p: str): str
The SHA-1 digest of the file's content.

The example for this item is shown with the ones above: fs-file-Sha256Hex.salam

Sha256Hex

func Sha256Hex(p: str): str
The SHA-256 digest of the file's content, for checking that a download arrived intact.

The example for this item is shown with the ones above: fs-file-Sha256Hex.salam

Sha512Hex

func Sha512Hex(p: str): str
The SHA-512 digest of the file's content.

The example for this item is shown with the ones above: fs-file-Sha256Hex.salam