Fil.Adapter.Local (Fil v0.2.0)

Copy Markdown View Source

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 to File.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

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 to length: 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 with File.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 with O_EXCL first.
  • Ignores checksum:.
  • Writing over a directory, or to report.txt/x when report.txt is a file, returns a Fil.InvalidRequestError.

Fil.rm/3

Fil.stat/3

  • Uses File.stat/2.
  • :etag is a "size-mtime" tag. It notices most changes, but misses a write in the same second that keeps the size. Fil.Plug sends it as a strong validator anyway, as nginx does.
  • :content_type is nil, because the filesystem doesn't store one (Fil.Plug guesses 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 a Fil.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 :eperm or :enotsup (a filesystem without hard links, or on Linux a file of another user with fs.protected_hardlinks on), it creates the destination with O_EXCL first 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 a Fil.ConflictError and 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 with File.rename/2, which replaces an empty directory. A file or a directory with files in it returns a Fil.AlreadyExistsError.
  • A move onto itself leaves the file as it is. With if_exists: :error, it returns a Fil.AlreadyExistsError.

Fil.rm_rf/3

  • Uses File.rm_rf/1 and 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

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

SituationFil error:reason
a missing file or directory, or a path through a file (report.txt/x)Fil.NotFoundError:enoent
missing permissions, a read-only filesystemFil.AccessDeniedError:eacces, :eperm, :erofs
reading, copying or deleting a directory, or writing over oneFil.InvalidRequestError:eisdir
writing, copying or renaming to a path under a fileFil.InvalidRequestError:enotdir
a name that's too long, a symlink loopFil.InvalidRequestError:enametoolong, :eloop
a path that resolves outside the rootFil.InvalidRequestError:ebadpath
an exclusive create finding the file already thereFil.AlreadyExistsError:eexist
a source that a write replaced during a move with if_exists: :errorFil.ConflictError:source_changed
a full disk, a used-up quotaFil.StorageFullError:enospc, :edquot
too many open filesFil.UnavailableError:emfile, :enfile
any other POSIX errorFil.UnknownErrorthe atom

Summary

Types

t()

@type t() :: %Fil.Adapter.Local{root: String.t()}