Storehouse.Ecto (Storehouse v0.2.0)

Copy Markdown View Source

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.

Summary

Functions

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.

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.

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.

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

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

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

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.

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.

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.

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

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.

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

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.

Functions

add_files(changeset, field, file_or_files, opts \\ [])

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(repo, record, field, steps, opts \\ [])

@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(repo, record_or_changeset, opts \\ [])

@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(files, opts \\ [])

@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(record)

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

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

insert(repo, changeset, opts \\ [])

@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(changeset, field, file_or_files, opts \\ [])

@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(changeset, field, metadata)

@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(changeset, field, key, metadata)

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

remove_file(changeset, field, key)

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(changeset, saved)

@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(repo, queryable, field, steps, opts \\ [])

@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(schema, field)

@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(repo, changeset, opts \\ [])

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