Fil.Adapter.Local (Fil v0.1.0)

Copy Markdown View Source

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: :error skips the temporary file and opens the destination with O_EXCL instead. If something is already there, the write returns a Fil.AlreadyExistsError.
  • 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.
  • 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/3 sets :etag to a weak "size-mtime" tag. It's good enough to notice a change, but it can't prove there was none. :content_type is always nil, because the filesystem doesn't store one.
  • the filesystem has no URLs. Attach Fil.Plugin.URL for public and signed URLs, and Fil.Plug serves them.
  • the filesystem stores no checksums. checksum: on a write is accepted and ignored, and so is verify_checksum: true on 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 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

FilLocal
read/3File.read/1
write/4temporary file, then File.rename/2 (:file.open/2 with :exclusive for if_exists: :error)
rm/3File.rm/1, a missing file mapped to success
stat/3File.stat/2, plus a pass over the file for checksum:
ls/3File.ls/1, walked depth-first when recursive
cp/4File.cp/2
rename/4File.rename/2
rm_rf/3File.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).

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 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()}