Work on a file after it is stored: what it is read for (an image's EXIF,
a placeholder, a video's length, a PDF's pages), each step an analyzer,
what it made of the file kept in the file's :analysis, by its name, as
a Storehouse.Analysis: its status, its result, why it failed.
defmodule MyApp.Analyzers.Pages do
@behaviour Storehouse.Analyzer
@impl true
def analyze(%Storehouse.File{content_type: "application/pdf"}, path, _opts) do
{output, 0} = System.cmd("pdfinfo", [path])
[_, pages] = Regex.run(~r/Pages:\s+(\d+)/, output)
{:ok, %{"pages" => String.to_integer(pages)}}
end
def analyze(_file, _path, _opts), do: :skip
end
{:ok, file} = Storehouse.analyze(file, [MyApp.Analyzers.Pages])
Storehouse.File.analyzed(file, MyApp.Analyzers.Pages.name())
#=> %{"pages" => 12}Steps
A step is an analyzer's module, or {module, opts}. They run in their
order, and each is run once: a step whose result the file has already
is skipped (force: true runs it again). A step that fails leaves the
others' results, and its own analysis is :failed, with why; the next
run tries it again. :skip means the step does not apply to the file,
and its analysis is :skipped.
The file is read from the store once for all the steps that need it
(needs_file?/0), to a temporary file, and that file is removed
after them. A file just stored from a path (Storehouse.store/2 with
:analyze) is read from that path, with no download.
Later
Storehouse.analyze_later/3 runs them out of the request, as a job of
the app's runner (Storehouse.Runner: Oban's, a process of the app's,
or one of its own); then the app's :then function gets the file with
its results, to save them. Storehouse.Ecto.analyze_later/5 saves them
in the record's column.
Telemetry
Each step is a span, [:storehouse, :analyze, ...], with the :key and
the :step, and result: :ok | :skip | :error (with the :reason).
Summary
Callbacks
Reads what the step reads from the file, at path on the local disk (or
nil, for a step that does not need the file: needs_file?/0).
opts are the step's, with the store as :adapter.
{:ok, result} is kept as the step's result, a map JSON can hold
(Storehouse.Metadata); :skip when the step does not apply to the
file.
The name the step's result is kept under. Optional: the default is the
module's last part, in snake case (MyApp.Analyzers.Pages → "pages").
Whether the step reads the file's bytes. Optional: the default is true.
Types
Callbacks
@callback analyze(Storehouse.File.t(), path :: Path.t() | nil, opts :: keyword()) :: {:ok, map()} | {:ok, map(), Path.t()} | :skip | {:error, term()}
Reads what the step reads from the file, at path on the local disk (or
nil, for a step that does not need the file: needs_file?/0).
opts are the step's, with the store as :adapter.
{:ok, result} is kept as the step's result, a map JSON can hold
(Storehouse.Metadata); :skip when the step does not apply to the
file.
A step that changes the stored file (Storehouse.Analyzer.EditMetadata)
gives {:ok, result, path}, with the file as it is now at path, a
temporary file of its own: the steps after it read that one, and it is
removed after them.
@callback name() :: String.t()
The name the step's result is kept under. Optional: the default is the
module's last part, in snake case (MyApp.Analyzers.Pages → "pages").
@callback needs_file?() :: boolean()
Whether the step reads the file's bytes. Optional: the default is true.