Standard library · System and tools

os/temp

Making a temporary file or directory under a unique name.

import os.temp

For data that is only needed while the program runs, make a temporary file or directory: an unpacked archive, a file being built before it is published, the output of a tool. File and Directory create an empty one in the system temporary folder under a random name (salam- and 16 hex digits from crypto), so several copies of the program running at once do not collide, and return its path. Removing it is up to you: nothing is cleaned up when the program exits.

Read and write the files with fs.file, and build paths inside a temporary directory with fs.path. For replacing a real file safely, fs.file.AtomicWrite is often the better tool.

What people get wrong:

  • Not checking Directory. It returns empty text when it fails, and a later path.Join("", name) then writes into the current folder.
  • Trusting File blindly. It always returns a path, even when the file could not be created, and it creates the file first and narrows its permissions right after, so for a moment it has the default permissions. For secrets, make a Directory (only you can enter it) and create the files inside it.
  • Moving a finished file out with a rename. The temporary folder is often on another disk, where a rename fails; copy it instead.

A scratch directory for one job

The usual pattern is three lines: make a directory with Directory, put defer temp.RemoveDir(work) right under it, and build every path inside it with path.Join. The deferred call runs on every way out of the function, an early ret included, so the directory is gone however the job ends. Directory returns an empty string when it cannot make the directory, so check for that before you use it.

os-temp-scratch.salam
import fs.dir
import fs.file
import fs.path
import os.temp

func main:
    work := temp.Directory()
    if work == "":
        println "could not make a work directory"
        ret 1
    end
    defer temp.RemoveDir(work)
    file.WriteText(path.Join(work, "part-1.txt"), "Salam ")
    file.WriteText(path.Join(work, "part-2.txt"), "world\n")
    mut joined := ""
    each name in ["part-1.txt", "part-2.txt"]:
        joined = joined + file.ReadText(path.Join(work, name))
    end
    print joined
    println dir.ListFiles(work).len(), "parts,", dir.Size(work), "bytes"
    ret 0
end
OutputSalam world 2 parts, 12 bytes

Build a file in steps, then publish it

When a file is written in several steps, write it to a temporary file first and copy it to its real name only when it is complete. Whoever reads people.csv then never sees half of it. Use file.Copy here, not file.Move: the temporary directory is often on another disk (a tmpfs in memory on many Linux systems), and a rename cannot cross disks.

os-temp-staging.salam
import fs.file
import os.temp

func main:
    staging := temp.File()
    file.Append(staging, "id,name\n")
    file.Append(staging, "1,Sara\n")
    file.Append(staging, "2,Reza\n")
    rows := file.ReadLines(staging)
    println "staged", rows.len(), "lines"
    if file.Copy(staging, "people.csv") == 0:
        println "published people.csv,", file.Size("people.csv"), "bytes"
    end
    temp.Remove(staging)
    println "staging left behind:", file.Exists(staging)
    print file.ReadText("people.csv")
    file.Delete("people.csv")
end
Outputstaged 3 lines published people.csv, 22 bytes staging left behind: false id,name 1,Sara 2,Reza

When the whole content is ready in one string, fs.file.AtomicWrite is simpler: it writes the temporary copy next to the target, on the same disk, and renames it in one step.

Who can read a temporary file

The system temporary directory is shared by every account on the machine. That is why File makes its file readable and writable by you alone (600) and Directory makes its directory open to you alone (700). A token or a downloaded document you put there stays out of reach of other users. Leave it like that, and widen the permissions with fs.Chmod only when another account really has to read the file.

os-temp-permissions.salam
import fs
import os.temp

func main:
    secret := temp.File()
    work := temp.Directory()
    println "file:", fs.Permissions(secret)
    println "directory:", fs.Permissions(work)
    temp.Remove(secret)
    temp.RemoveDir(work)
end
Outputfile: 600 directory: 700

Errors, and where the files go

Remove and RemoveDir return 0 on success and -1 on failure, and they record an IOError in os.error. The usual failure is removing the same file twice. RemoveDir on a directory that is already gone counts as success.

os-temp-errors.salam
import os.error
import os.temp

func main:
    p := temp.File()
    println "first remove:", temp.Remove(p)
    error.Clear()
    println "second remove:", temp.Remove(p)
    if error.HasError():
        println "message:", error.LastMessage()
        println "is an I/O error:", error.LastCode() == error.IOError
    end
end
Outputfirst remove: 0 second remove: -1 message: failed to remove temp file is an I/O error: true

The files go into the directory that os.TempDir names: the TEMP, TMP or TMPDIR environment variable, checked in that order, and /tmp when none is set. Nothing removes them when the program exits, and a program that crashes leaves them behind, so a long running service should give each job its own directory and remove it when the job is done. The names are random (salam- and hex digits), so never compare them or show them in output that someone reads later.

Functions

File

func File(): str
Makes an empty file named salam- plus 16 random hex digits and .tmp in the system temporary directory, sets its permissions to 600 and returns its path. It returns the path even when creation failed.
os-temp-File.salam
import os
import os.temp

func main:
    p := temp.File()
    println os.Exists(p)
    temp.Remove(p)
    println os.Exists(p)
    d := temp.Directory()
    println os.IsDir(d)
    temp.RemoveDir(d)
    println os.Exists(d)
end
Outputtrue false true false

Directory

func Directory(): str
Makes an empty directory under a unique name with permissions 700 and returns its path, or empty text (with an os.error record) when it cannot.

The example for this item is shown with the ones above: os-temp-File.salam

Remove

func Remove(p: str): int
Removes a temporary file; 0 on success, -1 with an os.error record on failure.

The example for this item is shown with the ones above: os-temp-File.salam

RemoveDir

func RemoveDir(p: str): int
Removes a temporary directory with everything in it, using os.RemoveAll; 0 on success or when it is already gone, -1 on failure. Like os.RemoveAll, it follows symbolic links to folders.

The example for this item is shown with the ones above: os-temp-File.salam