The local filesystem, jailed under a root directory.
disk = Fil.disk(adapter: Fil.Adapter.Local, root: "priv/storage")Unlike on an object store, directories exist on their own and can be empty. Every path is checked against the root
again after expansion. Fil has 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.
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
For each Fil function, this section says which File or :file function it uses and where it departs from the
contract that every adapter follows. Anything it doesn't mention follows the
contract.
Fil.read/3
- Uses
File.read/1, or:file.pread/3to read only the part thatoffset:andlength:ask for. - Reading a directory returns a
Fil.InvalidRequestError. - Ignores
verify_checksum: true, because the filesystem stores no checksums.
Fil.stream/3
- Opens the file once to check that it can be read.
- Each time the stream is read, reads the file in chunks of 64 KiB, from
offset:on and up tolength:bytes.
Fil.write/4
- Writes the content (in memory or a stream) to a temporary file in the destination directory, named
.fil-and a unique suffix, then moves it into place withFile.rename/2. Readers never see a partial file. - A failed write leaves nothing behind and removes the directories it created. So does a writer that's killed, within the limits in Interrupted writes.
- With
if_exists: :error, hard-links the temporary file to the destination instead, which fails if the destination exists. On a filesystem without hard links, it creates the destination withO_EXCLfirst. - Ignores
checksum:. - Writing over a directory, or to
report.txt/xwhenreport.txtis a file, returns aFil.InvalidRequestError.
Fil.rm/3
- Uses
File.rm/1. A missing file counts as success. - Removing a directory returns a
Fil.InvalidRequestError.
Fil.stat/3
- Uses
File.stat/2. :etagis a"size-mtime"tag. It notices most changes, but misses a write in the same second that keeps the size.Fil.Plugsends it as a strong validator anyway, as nginx does.:content_typeisnil, because the filesystem doesn't store one (Fil.Plugguesses it from the extension).checksum:reads the whole file to compute the checksum.
Fil.ls/3
- Uses
File.ls/1, and walks the tree depth-first when recursive. - Lists empty directories too.
- Skips the temporary
.fil-files of writes. The disk reserves that prefix, so a file of your own whose name starts with.fil-is skipped as well. - A path that isn't a directory lists nothing, the same as a missing one.
Fil.cp/4
- Uses
File.cp/2. - With
if_exists: :error, copies to a temporary.fil-file instead and hard-links it to the destination, like a write. - A copy that fails removes the directories it created, like a write.
- Copying a directory returns a
Fil.InvalidRequestError. - A copy onto itself leaves the file as it is. With
if_exists: :error, it returns aFil.AlreadyExistsError.
Fil.rename/4
- Uses
File.rename/2, which moves directories too. - A move that fails removes the directories it created, like a write.
- With
if_exists: :error, hard-links the file to the destination, then removes the source. Where the link fails with:epermor:enotsup(a filesystem without hard links, or on Linux a file of another user withfs.protected_hardlinkson), it creates the destination withO_EXCLfirst and then moves the file there, like a write. A symlink is moved that way too, so the destination is the link and not its target. - With
if_exists: :error, a write that replaces the source after the link fails the move with aFil.ConflictErrorand leaves the destination as it was. A write in the short moment between that check and the removal of the source is lost. - With
if_exists: :error, a directory is still moved withFile.rename/2, which replaces an empty directory. A file or a directory with files in it returns aFil.AlreadyExistsError. - A move onto itself leaves the file as it is. With
if_exists: :error, it returns aFil.AlreadyExistsError.
Fil.rm_rf/3
- Uses
File.rm_rf/1and counts the files it removed. - Removes files whose name starts with
.fil-too, but doesn't count them.
Fil.url/3 and Fil.signed_url/3
- The filesystem has no URLs. Attach
Fil.Plugin.URLto build them, andFil.Plugserves them.
Fil.Disk.same_storage?/2
- Compares the expanded
:root, so"priv/storage"and its absolute path are the same storage.
Details
Interrupted writes
When a writer is killed before it's done (a request process that the server stops when the client disconnects, for
example), Fil's application removes its .fil- file and the directories it created once the process is gone. The
same goes for a copy with if_exists: :error. That's best effort: a filesystem that takes more than a second to
create the file or a directory after the kill (a slow network share) can leave it behind, and so can a crash of the
whole node (kill -9, a power loss). Local writes also work without the :fil application running, but then nothing
is removed after a kill. Stopping the application removes the temporary files of the writes in progress, so those
writes fail with a Fil.NotFoundError.
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 source that a write replaced during a move with if_exists: :error | Fil.ConflictError | :source_changed |
| 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()}