PhoenixKit. Modules. Storage. Reorganizer. ResourceSource
(phoenix_kit v2.40.1)
Copy Markdown
View Source
A reorganizer PhoenixKit.Modules.Storage.Reorganizer.Source built from a
declaration, for a module that keeps one media folder per record through
PhoenixKit.Modules.Storage.ResourceFolders. The module states what its
records are; this module applies the whole Source contract to them —
one implementation of the rules instead of one per module:
defmodule MyModule.MediaReorganizer do
alias PhoenixKit.Modules.Storage.Reorganizer.ResourceSource
def plan(actor_uuid, opts \\ []), do: ResourceSource.plan(spec(), actor_uuid, opts)
defp spec do
%{
source: "my_module",
app: :my_module,
pending_prefix: "my-module-attachment-pending-",
kinds: [
%{
kind: :item,
schema: MyModule.Item,
prefix: "my-module-item-",
pointer: {:data, "files_folder_uuid"},
live: &where(&1, [r], r.status != "deleted")
}
]
}
end
endThe declaration
:source— the source name on every action:app— the OTP app whose:attachments_parent_folder/:attachments_folder_namehooks decide where folders belong:kinds— the record kinds, parents first (their folders move in this order):pending_prefix— the name prefix of folders made for unsaved records, when the module makes any; stale empty ones are trashed:noun— what a record is called in reports (default"record"):name_hook—falsefor a module whose runtime never asks the:attachments_folder_namehook (it finds folders by the deterministic name only): the plan then never proposes a host name its uploads would not follow (defaulttrue):extra—fun(actor_uuid, opts) -> [action]for reports only this module can make, appended to the plan
Each kind:
:kind,:schema— the atom the parent hook receives, the schema:prefix— the deterministic folder name isprefix <> uuid:pointer— where the record stores its folder uuid (PhoenixKit.Modules.Storage.ResourceFolders.pointer/0), ornilfor a module that finds folders by name only:live—fun(query) -> querykeeping the records that are live (default: every row); a record that is not live is an orphan's owner:subject— what the parent hook's third argument is::record(the full row, the default) or:uuid:label— the field naming a record in reports (default:name):fields— further columns candidate detection reads (default[]):order—fun(records) -> recordsreordering a kind's records (e.g. parents before children within the kind):orphan—:not_live(default) reports the folder of a missing or not-live record;:missingonly a missing one
Rules applied
The Source moduledoc is the contract; how each case is decided:
- No parent hook configured → report-only (orphans, pending folders);
a configured hook that is not callable is one
:hook_error. - A candidate is a record with a live folder — through its pointer or by its deterministic name anywhere; nothing else reaches a hook, and candidates are reloaded as full rows before any hook sees them.
- The parent hook failing (raise, throw, exit, anything but
{:ok, uuid}/{:ok, nil}/nil) skips the record into one:hook_error; so does a failing name hook. Logs name the failure's shape only. - A pointer-found folder keeps its name unless it still has the deterministic or a pending name; then the host name is proposed.
- By name: the host name directly under the parent (not adopted when
another record's pointer claims it — the deterministic name is
wanted instead), then the deterministic name under the parent, then
at the root. Two of these live at once is a
:duplicate; a copy anywhere else is:relocated, never moved. With a root answer and no copy at the root, a single copy elsewhere is adopted in place (:hook_nil) and several are a:duplicate. - A root answer never moves a folder that has a parent (
:hook_nil). - Two records on one folder, or two moves onto one target, are one
:duplicateeach; neither moves. - Orphans: deterministic-named folders at the root or under a parent a
hook named, whose record is missing or not live, and no record
claims. Pending folders: empty and older than
pending_days→:trash(report-only without a hook); not empty → a report naming their files. - A module with a pointer back-fills it (
after_move) and renames a taken target"name (N)"; one without reports the conflict.
Summary
Functions
The plan for spec (see the moduledoc) — Source.plan/2's answer.
Types
@type kind_spec() :: %{ :kind => atom(), :schema => module(), :prefix => String.t(), optional(:pointer) => PhoenixKit.Modules.Storage.ResourceFolders.pointer() | nil, optional(:live) => (Ecto.Queryable.t() -> Ecto.Query.t()), optional(:subject) => :record | :uuid, optional(:label) => atom(), optional(:fields) => [atom()], optional(:order) => ([struct()] -> [struct()]), optional(:orphan) => :not_live | :missing }