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)
endTemporary, 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
@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))
@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).
@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.
@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.
@spec files(struct()) :: [Storehouse.File.t()]
Every file of a record, in its fields of files.
@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).
@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 isStorehouse.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.
@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()
@spec put_metadata(Ecto.Changeset.t(), atom(), Storehouse.Adapter.key(), map()) :: Ecto.Changeset.t()
@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))
@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).
@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 isfalse: only the files a step has yet to run on, or failed on.:now- run the steps here, not as jobs. The default isfalse.:max_concurrency- withnow: 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_bytesand:adapter- as foranalyze_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].
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
@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.