# `Storehouse.Ecto`
[🔗](https://github.com/hattoriai/storehouse/blob/v0.2.0/lib/storehouse/ecto.ex#L2)

Files in records, with Ecto: a column of type `Storehouse.Ecto.File`
(or a list of them), files put in a changeset with `put_file/4`, made
permanent when the record is saved, and deleted once nothing has them.

    schema "users" do
      field :name, :string
      field :avatar, Storehouse.Ecto.File
    end

    def changeset(user, attrs, avatar) do
      user
      |> cast(attrs, [:name])
      |> Storehouse.Ecto.put_file(:avatar, avatar, accept: Storehouse.Check.images(), max_bytes: 5_000_000)
    end

## Temporary, then permanent

A file a person uploads for a form is temporary until the form is
saved: store it with `cache: true` (`Storehouse.Live.allow/3` and
`consume/3`, `Storehouse.store/2`), and its key is under the temporary
prefix (`Storehouse.cache_prefix/0`). When the record is saved,
`put_file/4` makes it permanent (`Storehouse.promote/2`): a copy in the
store (on S3, in the bucket), in the save's transaction. A form never
saved leaves its files under the prefix, where a lifecycle rule of the
bucket deletes them:

    {"Rules": [{"ID": "storehouse-cache", "Status": "Enabled",
                "Filter": {"Prefix": "cache/"},
                "Expiration": {"Days": 1}}]}

## Replaced and deleted files

A file the record no longer has (replaced, removed, or the record
deleted) is deleted from the store once the database has the change:
`insert/3`, `update/3` and `delete/3` save and then delete them.
Inside a transaction of yours, use `Repo` as usual, and delete
`replaced/2`'s files after your transaction commits: a rollback would
otherwise leave a record whose file is gone.

# `add_files`

```elixir
@spec add_files(
  Ecto.Changeset.t(),
  atom(),
  Storehouse.File.t() | [Storehouse.File.t()],
  keyword()
) ::
  Ecto.Changeset.t()
```

Adds files to those of a list field, after the files the record has:
each checked as `put_file/4` checks it, with the same options, and made
permanent when the record is saved. `:max_files` counts them all.

    user
    |> change()
    |> Storehouse.Ecto.add_files(:photos, uploaded, accept: Storehouse.Check.images(), max_files: 20)
    |> then(&Storehouse.Ecto.update(Repo, &1))

# `analyze_later`

```elixir
@spec analyze_later(
  module(),
  struct(),
  atom(),
  [Storehouse.Analyzer.step()],
  keyword()
) :: :ok
```

Runs the analysis steps of the files in `field` out of the request
(`Storehouse.analyze_later/3`), then saves their results in the
record's column: only each file's analysis, the steps that ran
merged into what it had, in a transaction that locks the row, so that what the app changed in
the meantime stays. A file no longer in the record, or a record
deleted, is left as it is.

    Storehouse.Ecto.analyze_later(Repo, user, :avatar, [MyApp.Analyzers.Colors])

`insert/3` and `update/3` do it for the files a save puts in a field,
with `analyze: [avatar: steps]`.

Takes the options of `Storehouse.analyze_later/3` but `:then`
(`:runner`, `:force`, `:max_bytes`, `:adapter`).

# `delete`

```elixir
@spec delete(module(), struct() | Ecto.Changeset.t(), keyword()) ::
  {:ok, struct()} | {:error, Ecto.Changeset.t()}
```

`repo.delete/2`, then the record's files are deleted from the store.
Inside a transaction, they are not: delete `files/1`'s after it
commits.

# `delete_files`

```elixir
@spec delete_files([Storehouse.File.t()], keyword()) :: :ok
```

Deletes files from the store (`Storehouse.delete_later/2`): jobs of the
app's runner (`Storehouse.Runner`), tried again when the store fails.

# `files`

```elixir
@spec files(struct()) :: [Storehouse.File.t()]
```

Every file of a record, in its fields of files.

# `insert`

```elixir
@spec insert(module(), Ecto.Changeset.t(), keyword()) ::
  {:ok, struct()} | {:error, Ecto.Changeset.t()}
```

`repo.insert/2`, as it is: the files are made permanent in its
transaction (`put_file/4`).

# `put_file`

```elixir
@spec put_file(
  Ecto.Changeset.t(),
  atom(),
  Storehouse.File.t() | [Storehouse.File.t()] | nil,
  keyword()
) ::
  Ecto.Changeset.t()
```

Puts a file (a `Storehouse.File`, a list of them for a list field, or
`nil` to remove it) in the changeset, checked, and made permanent when
the record is saved.

## Options

  * `:accept` - the types the field takes (`Storehouse.Check.images()`).
  * `:max_bytes`, `:max_pixels` - the limits.
  * `:owner` - the owner the files must belong to
    (`Storehouse.owns?/2`): a file under another's prefix is an error
    of the field, so that a record never takes another's file.
  * `:max_files` - for a list field, the most files it holds.
  * `:adapter` - the store, to make it permanent. The default is
    `Storehouse.adapter/0`.

A file over a limit, or of a type not taken, is an error of the field
(`validation: :storehouse`, with the `:reason`). For a list field,
`add_files/4` adds to the files the record has, and `remove_file/3`
takes one away.

# `put_metadata`

```elixir
@spec put_metadata(Ecto.Changeset.t(), atom(), map()) :: Ecto.Changeset.t()
```

Merges `metadata` into the metadata of the file in `field`
(`Storehouse.File.put_metadata/2`): a caption, a credit, what a person
entered. For a list field, give the `key` of the file. Metadata that
JSON cannot hold is an error of the field (`validation: :storehouse`,
`reason: {:invalid_metadata, path}`); a field with no such file is left
as it is.

    photo
    |> Storehouse.Ecto.put_metadata(:image, %{"caption" => params["caption"]})
    |> Repo.update()

# `put_metadata`

```elixir
@spec put_metadata(Ecto.Changeset.t(), atom(), Storehouse.Adapter.key(), map()) ::
  Ecto.Changeset.t()
```

# `remove_file`

```elixir
@spec remove_file(
  Ecto.Changeset.t(),
  atom(),
  Storehouse.File.t() | Storehouse.Adapter.key()
) ::
  Ecto.Changeset.t()
```

Takes the file with `key` (or the file given) out of `field`: from a
list field, or a single one, which is then `nil`. `update/3` deletes it
from the store once the record is saved. A field without it is left as
it is.

    user
    |> change()
    |> Storehouse.Ecto.remove_file(:photos, key)
    |> then(&Storehouse.Ecto.update(Repo, &1))

# `replaced`

```elixir
@spec replaced(Ecto.Changeset.t(), struct()) :: [Storehouse.File.t()]
```

The files a saved changeset leaves behind: those its record had in a
field of files, and does not have after it. Delete them once the
transaction has committed (`delete_files/2`, with the field's store as
`:adapter` when it has one, `store/2`).

# `reprocess`

```elixir
@spec reprocess(
  module(),
  Ecto.Queryable.t(),
  atom(),
  [Storehouse.Analyzer.step()],
  keyword()
) ::
  {:ok, map()}
```

Runs analysis steps over the files of `field` in every record
`queryable` gives: a step added after files were stored (placeholders
a year in), a step that failed, files from before analysis existed.

    Storehouse.Ecto.reprocess(Repo, MyApp.Photo, :image, [MyApp.Analyzers.Placeholder])
    #=> {:ok, %{records: 10_000, files: 9_412, skipped: 588, failed: []}}

The records are read a batch at a time, in the order of their primary
key, so a table of any size takes the memory of a batch. A file that
has every step's result already is skipped, so it can be stopped and
run again: it does only what is missing. `:after` starts after the
primary key a run got to (`:on_progress` gives it).

Each file is a job of the app's runner (`Storehouse.Runner`), which
saves its results in the record's column as `analyze_later/5` does;
with `now: true`, the steps run here instead, several files at once,
and the results are saved before it returns. `mix storehouse.reprocess`
runs it from the command line.

## Options

  * `:batch_size` - the records read at a time. The default is 500.
  * `:force` - every file, its steps run again. The default is
    `false`: only the files a step has yet to run on, or failed on.
  * `:now` - run the steps here, not as jobs. The default is `false`.
  * `:max_concurrency` - with `now: true`, how many files at once. The
    default is 4.
  * `:after` - the primary key to start after.
  * `:on_progress` - a function given, after each batch, the counts so
    far and `:last`, the batch's last primary key.
  * `:runner`, `:max_bytes` and `:adapter` - as for `analyze_later/5`.

Gives the counts: the `:records` read, the `:files` analyzed (or given
to the runner), those `:skipped`, done already, and those `:failed`,
`{primary_key, file_key, reason}`. A span, `[:storehouse, :reprocess]`.

# `store`

```elixir
@spec store(module(), atom()) :: module() | nil
```

The store of a schema's field of files (its `store:`,
`Storehouse.Ecto.File`), or `nil` for the default store.

    Storehouse.Ecto.store(Invoice, :pdf)
    #=> MyApp.PrivateFiles

# `update`

```elixir
@spec update(module(), Ecto.Changeset.t(), keyword()) ::
  {:ok, struct()} | {:error, Ecto.Changeset.t()}
```

`repo.update/2`, then the files it replaced or removed are deleted from
the store. Inside a transaction, they are not: delete `replaced/2`'s
after it commits.

---

*Consult [api-reference.md](api-reference.md) for complete listing*
