Fil.Tmp (Fil v0.2.0)

Copy Markdown View Source

Temporary files and directories, which are removed when the process that created them exits.

Fil.tmp/0 creates a directory and Fil.tmp/1 a file name in a directory of its own. Both return a plain Fil.Ref on a local disk rooted at that directory, so the whole API works on it:

report = Fil.tmp("report.pdf")
{:ok, _} = Fil.cp(Fil.ref(invoices, "2026/09.pdf"), report)
{text, 0} = System.cmd("pdftotext", [Fil.Tmp.path(report), "-"])

path/1 returns the absolute path of a ref on a temporary directory, for tools that need one. A tool can write into the directory too, and Fil lists what it wrote:

frames = Fil.tmp()
{_, 0} = System.cmd("ffmpeg", ["-i", Fil.Tmp.path(video), Fil.Tmp.path(frames.disk, "%03d.png")])
{:ok, pngs} = Fil.ls(frames)

Files in the directory are refs on its disk, such as Fil.ref(frames.disk, "001.png").

The directory

Each call creates a new directory in System.tmp_dir!/0 (which checks the TMPDIR, TMP and TEMP environment variables), named fil- and a random suffix. Only the operating system user that runs the node can open it (mode 0o700), because a copy from a disk with an encryption plugin is plaintext here. On Windows the mode does nothing.

The name passed to Fil.tmp/1 is used as it is, so tools, mail attachments and Fil.Plugin.ContentType see the real file name. It can have directories ("pages/1.png"), but it has to stay inside the directory: an absolute path, "", "." or a name that escapes with .. raises ArgumentError. Pass a name from user input, such as upload.filename, through Path.basename/1 first.

Cleanup

The directory belongs to the process that called Fil.tmp/0,1. It's removed with everything in it when that process exits, also when it's killed, by a process of the :fil application. So:

  • a temporary file created inside a Task is gone when the task returns. Create it in the caller and pass it in, or hand it to the caller with give_away/2.
  • a long-lived process (a GenServer, a LiveView, a channel) keeps its temporary files for as long as it runs. So does a request on Bandit over HTTP/1.1, whose process serves the whole keep-alive connection (see Uploads and temporary files). Call cleanup/1 once it's done with them. A write to a temporary ref after that creates an ordinary directory, with the default mode, which Fil doesn't remove.
  • Fil.rm_rf/1 on the directory removes it, but it stays registered to its owner, and a later write creates it again without the 0o700 mode. cleanup/1 removes both.
  • a temporary ref works only on the node that created it.
  • stopping the :fil application removes every temporary directory. A crash of the whole node (kill -9, a power loss) leaves them behind in the system's temporary directory.

Fil.tmp/0,1 raises when the :fil application isn't running, because nothing would remove the directory then.

Summary

Functions

Removes the temporary directories of pid now. Returns :ok.

Makes pid the owner of a temporary directory, so it's removed when pid exits instead of the calling process. The ref can name the directory or anything in it. Returns :ok.

Creates a temporary directory and returns a ref to it, with path ".". Fil.tmp/0 is the same function.

Creates a temporary directory and returns a ref to the file name in it. The file doesn't exist until something writes it. Fil.tmp/1 is the same function.

Returns the absolute path of a ref on a temporary directory: of the directory itself, a file in it or anything else on its disk.

Returns the absolute path of path on a temporary directory's disk. See path/1.

Functions

cleanup(pid \\ self())

@spec cleanup(pid()) :: :ok

Removes the temporary directories of pid now. Returns :ok.

For processes that run long after they're done with their temporary files. It never fails: a directory that can't be removed completely is left as it is. It leaves the temporary files of Fil.Adapter.Local writes in progress alone, so a write of pid still works. Cleaning up another process races with its give_away/2: a directory it hands away meanwhile is removed, and path/1 then points to nothing.

Don't use the refs afterwards. A write to one creates an ordinary directory in its place, with the default mode, which Fil doesn't remove.

give_away(ref, pid)

@spec give_away(Fil.Ref.t(), pid()) :: :ok

Makes pid the owner of a temporary directory, so it's removed when pid exits instead of the calling process. The ref can name the directory or anything in it. Returns :ok.

report = Fil.tmp("report.pdf")
Fil.write!(report, pdf)
:ok = Fil.Tmp.give_away(report, caller)

If pid is dead already, the directory is removed right away. It raises ArgumentError when the calling process doesn't own the directory, and for a pid on another node.

new()

@spec new() :: Fil.Ref.t()

Creates a temporary directory and returns a ref to it, with path ".". Fil.tmp/0 is the same function.

iex> Fil.Tmp.new()
#Fil.Ref<local:.>

When the directory can't be created, it raises the error struct Fil.Adapter.Local would return, such as a Fil.AccessDeniedError or a Fil.StorageFullError, with the directory as :path. Without the :fil application running, it raises a RuntimeError.

new(name)

@spec new(Path.t()) :: Fil.Ref.t()

Creates a temporary directory and returns a ref to the file name in it. The file doesn't exist until something writes it. Fil.tmp/1 is the same function.

iex> Fil.Tmp.new("pages/1.png")
#Fil.Ref<local:pages/1.png>

The name has to be a relative path that stays inside the directory (see "The directory" above). Anything else raises ArgumentError before a directory is created. Otherwise it raises like new/0.

path(ref)

@spec path(Fil.Ref.t()) :: Path.t()

Returns the absolute path of a ref on a temporary directory: of the directory itself, a file in it or anything else on its disk.

It raises ArgumentError for a ref on another disk, and after cleanup/1 or the owner's exit.

path(disk, path)

@spec path(Fil.Disk.t(), Path.t()) :: Path.t()

Returns the absolute path of path on a temporary directory's disk. See path/1.