# `Fil.Ref`
[🔗](https://github.com/pehbehbeh/fil/blob/v0.1.0/lib/fil/ref.ex#L1)

A reference to a file on a disk: a `Fil.Disk` plus a path. It can point to a file that doesn't exist yet, such as the
destination of a copy.

Every `Fil` function that can fail accepts a ref in place of a `disk, path` pair, and every action on a file
returns the resulting ref, so calls chain:

    with {:ok, report} <- Fil.write(disk, "reports/q3.pdf", pdf),
         {:ok, _backup} <- Fil.cp(report, Fil.ref(other_disk, "backups/q3.pdf")) do
      {:ok, report}
    end

## The `:stat` field

`Fil.ls/2` fills in `:stat` from the listing, so you can filter a listing by size and then read, copy or delete the
same values:

    {:ok, reports} = Fil.ls(disk, "reports")

    reports
    |> Enum.filter(&(&1.stat.type == :regular and &1.stat.size > 1_000_000))
    |> Enum.each(&Fil.rm/1)

The stat is a snapshot from the time of the listing. Operations ignore it and never treat it as the current state, and
the refs they return have `stat: nil` instead of a stale value. This means:

  * `stat: nil` says nothing about the file. Call `Fil.stat/1` for the current state.
  * comparing whole structs compares the snapshots too. Compare `:disk` and `:path` to check for the same file.

## Inspect

A ref contains a disk, and a disk may contain credentials, so `Inspect` only prints the adapter label and the
path:

    iex> Fil.ref(Fil.disk(adapter: Fil.Adapter.Local, root: "/tmp/fil"), "uploads/a.txt")
    #Fil.Ref<local:uploads/a.txt>

For the same reason, refs aren't meant to be serialized. Store `ref.path` and rebuild the disk instead.

# `t`

```elixir
@type t() :: %Fil.Ref{disk: Fil.Disk.t(), path: String.t(), stat: Fil.Stat.t() | nil}
```

# `new`

```elixir
@spec new(Fil.Disk.t(), Path.t()) :: t()
```

Builds a ref.

The path is normalized when possible (see "Concepts" in `Fil`). A path that can't be normalized, such as one that
escapes the disk root, is kept as given. Using it later returns a `Fil.InvalidRequestError` instead of raising here.

    iex> disk = Fil.disk(adapter: Fil.Adapter.Local, root: "/tmp/fil")
    iex> Fil.Ref.new(disk, "./uploads//a.txt").path
    "uploads/a.txt"
    iex> Fil.Ref.new(disk, "").path
    "."
    iex> escape = Fil.Ref.new(disk, "../escape.txt")
    iex> escape.path
    "../escape.txt"
    iex> {:error, %Fil.InvalidRequestError{reason: :ebadpath}} = Fil.read(escape)

# `normalize`

```elixir
@spec normalize(t()) :: {:ok, t()} | {:error, Fil.InvalidRequestError.t()}
```

Canonicalizes a ref for an operation.

Normalizes the path, rejects escapes with a `Fil.InvalidRequestError` and drops `:stat`, so a listing snapshot is
never mistaken for the current state. Every public `Fil` function calls this before the adapter gets the path.

    iex> disk = Fil.disk(adapter: Fil.Adapter.Local, root: "/tmp/fil")
    iex> {:ok, normalized} = Fil.Ref.normalize(%Fil.Ref{disk: disk, path: "a/../b.txt"})
    iex> normalized.path
    "b.txt"
    iex> Fil.Ref.normalize(normalized) == {:ok, normalized}
    true
    iex> {:error, %Fil.InvalidRequestError{path: "../etc/passwd", reason: :ebadpath}} =
    ...>   Fil.Ref.normalize(%Fil.Ref{disk: disk, path: "../etc/passwd"})

