The local filesystem, jailed under a root directory.
disk = Fil.disk(adapter: Fil.Adapter.Local, root: "priv/storage")Behaviour
- writes are atomic: content goes to a temporary file in the destination directory and is then renamed into place, so readers never see a partial file. Missing parent directories are created first.
if_exists: :errorskips the temporary file and opens the destination withO_EXCLinstead. If something is already there, the write returns aFil.AlreadyExistsError.- every path is checked against the root again after expansion.
Filhas already rejected../escapes, so this is defense in depth. The check only looks at the path string: a symlink inside the root can still point outside it. - listing a missing directory, or a path that isn't a directory, returns
{:ok, []}, the same as a missing prefix on an object store. stat/3sets:etagto a weak"size-mtime"tag. It's good enough to notice a change, but it can't prove there was none.:content_typeis alwaysnil, because the filesystem doesn't store one.- the filesystem has no URLs. Attach
Fil.Plugin.URLfor public and signed URLs, andFil.Plugserves them. - the filesystem stores no checksums.
checksum:on a write is accepted and ignored, and so isverify_checksum: trueon a read.checksum:on a stat reads the whole file to compute it.
Options
:root(String.t/0) - The directory every path is resolved against. Defaults toFile.cwd!()when the disk is built, so changing the working directory later doesn't move the disk. A relative root is expanded once, when the disk is built.
Operations
Fil | Local |
|---|---|
read/3 | File.read/1 |
write/4 | temporary file, then File.rename/2 (:file.open/2 with :exclusive for if_exists: :error) |
rm/3 | File.rm/1, a missing file mapped to success |
stat/3 | File.stat/2, plus a pass over the file for checksum: |
ls/3 | File.ls/1, walked depth-first when recursive |
cp/4 | File.cp/2 |
rename/4 | File.rename/2 |
rm_rf/3 | File.rm_rf/1, counting the files it removed |
Errors
:reason is the POSIX atom from File, adjusted so it's the same on macOS and Linux (deleting a directory is
:eisdir on both, a parent that's a file :enotdir).
| Situation | Fil error | :reason |
|---|---|---|
a missing file or directory, or a path through a file (report.txt/x) | Fil.NotFoundError | :enoent |
| missing permissions, a read-only filesystem | Fil.AccessDeniedError | :eacces, :eperm, :erofs |
| reading, copying or deleting a directory, or writing over one | Fil.InvalidRequestError | :eisdir |
| writing, copying or renaming to a path under a file | Fil.InvalidRequestError | :enotdir |
| a name that's too long, a symlink loop | Fil.InvalidRequestError | :enametoolong, :eloop |
| a path that resolves outside the root | Fil.InvalidRequestError | :ebadpath |
| an exclusive create finding the file already there | Fil.AlreadyExistsError | :eexist |
| a full disk, a used-up quota | Fil.StorageFullError | :enospc, :edquot |
| too many open files | Fil.UnavailableError | :emfile, :enfile |
| any other POSIX error | Fil.UnknownError | the atom |
Summary
Types
@type t() :: %Fil.Adapter.Local{root: String.t()}