Storehouse.Analyzer behaviour (Storehouse v0.2.0)

Copy Markdown View Source

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

Types

A step: an analyzer's module, or the module and its options.

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.

Functions

The name a step's result is kept under (name/0).

Types

step()

@type step() :: module() | {module(), keyword()}

A step: an analyzer's module, or the module and its options.

Callbacks

analyze(t, path, opts)

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

name()

(optional)
@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").

needs_file?()

(optional)
@callback needs_file?() :: boolean()

Whether the step reads the file's bytes. Optional: the default is true.

Functions

name(module)

@spec name(step()) :: String.t()

The name a step's result is kept under (name/0).