# `Fil.Adapter.Local`
[🔗](https://github.com/pehbehbeh/fil/blob/v0.1.0/lib/fil/adapter/local.ex#L1)

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` (`t: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

| `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 |

# `t`

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

