# `PhoenixKit.Modules.Storage.Reorganizer`
[🔗](https://github.com/BeamLabEU/phoenix_kit/blob/v2.26.1/lib/modules/storage/reorganizer.ex#L1)

Moves every module's legacy media folders to where the host's
`attachments_parent_folder` / `attachments_folder_name` hooks now put new
ones.

The engine itself knows nothing about any module's schemas — it collects
plans (plain maps, see `PhoenixKit.Modules.Storage.Reorganizer.Action`)
from every enabled module's `PhoenixKit.Modules.Storage.Reorganizer.Source`
(`PhoenixKit.ModuleRegistry.all_media_reorganizers/0`), diffs each plan
against the current folder state, and — when asked to `apply?` — applies
each action in its own transaction. A source raising, a single action
failing, or a naming conflict never halts the run; every outcome is kept
in the report.

Nothing is ever hard-deleted. `:trash` only soft-deletes a folder that is
still empty (0 files, 0 links, 0 live child folders) at apply time
(`PhoenixKit.Modules.Storage.trash_folder/1`); the engine never creates
folders.

A real unique-constraint violation aborts the surrounding Postgres
transaction, so a failed `UPDATE` can't be retried inside the same
transaction. Rather than retry, `on_conflict: :suffix` SELECTs the target
name for a collision *before* writing and picks a free `"name (N)"`, so
its `UPDATE` hits no naming constraint at all; `on_conflict: :report`
skips the pre-check and lets the `UPDATE` — the one write its transaction
ever attempts — hit the constraint, reporting `:conflict` and rolling
back.

    {:ok, report} = PhoenixKit.Modules.Storage.Reorganizer.run(actor_uuid, apply?: false)
    IO.puts(PhoenixKit.Modules.Storage.Reorganizer.format_report(report))

# `apply_one`

```elixir
@spec apply_one(PhoenixKit.Modules.Storage.Reorganizer.Action.t()) ::
  PhoenixKit.Modules.Storage.Reorganizer.Action.t()
```

Applies a single normalized action, returning it with `:outcome` set (and
`:error`/`:reason` on anything that isn't a clean success). Runs in one
transaction — a failure at any step rolls back everything the action did.

Never raises: an exception, throw, or exit anywhere in the apply path
(including an `after_move` callback) is caught and turned into outcome
`:failed` — a single bad action never loses the already-committed actions
from the report.

# `format_report`

```elixir
@spec format_report(%{
  actions: [PhoenixKit.Modules.Storage.Reorganizer.Action.t()],
  summary: map(),
  applied?: boolean()
}) :: String.t()
```

Renders the report as a fixed-width text table: one row per `{source,
kind}` with columns `total moved renamed backfilled restored conflicts
failed trashed reported`, then a details section — everything not a clean
move (conflicts, failures, reports, trashes, restores), and in dry-run
every planned action, so the owner sees what `--apply` would do.

# `plan`

```elixir
@spec plan(String.t() | nil, keyword()) :: [
  PhoenixKit.Modules.Storage.Reorganizer.Action.t()
]
```

Collects and normalizes actions from every source, dropping no-ops.
Never writes (engine-side) — a `Source`'s own hooks may still create
folders/pointers, since `plan/2` calls into the module's normal
parent/name resolution.

A source's `plan/2` is isolated: a raise, throw, exit, or a non-list
return becomes one `:source_error` `:report` action for that source and
every other source still runs. Within a source's own list, one action
`Action.new!/1` can't normalize (missing/invalid field) becomes one
`:invalid_action` `:report` for that action only — the rest of that
source's plan (and every other source) is unaffected.

# `run`

```elixir
@spec run(String.t() | nil, keyword()) ::
  {:ok,
   %{
     actions: [PhoenixKit.Modules.Storage.Reorganizer.Action.t()],
     summary: map(),
     applied?: boolean()
   }}
```

`plan/2`, then — when `apply?: true` — applies every planned action, each
in its own transaction. Never halts: a failure, conflict, or source error
is kept in the report and the run continues.

# `sources`

```elixir
@spec sources(:all | [String.t()] | [module()]) :: [module()]
```

Resolves the `sources:` option to a list of `Source` modules.

`:all` collects every enabled module's `media_reorganizer/0`
(`PhoenixKit.ModuleRegistry.all_media_reorganizers/0`). A list of
module-key strings resolves each to the enabled module's registered
source. A list of modules is used as-is (accepted directly so tests can
exercise a stub `Source` without registering a fake `PhoenixKit.Module`).

# `summarize`

```elixir
@spec summarize([PhoenixKit.Modules.Storage.Reorganizer.Action.t()]) :: %{
  required({String.t(), atom()}) =&gt; map()
}
```

Groups actions by `{source, kind}` and counts outcomes per group.

---

*Consult [api-reference.md](api-reference.md) for complete listing*
