# `PhoenixKitPhotos.Bulk`
[🔗](https://github.com/BeamLabEU/phoenix_kit_photos/blob/v0.3.0/lib/phoenix_kit_photos/bulk.ex#L1)

What can be done to many files at once. A person gets here through the timeline's selection
and an agent through the API tools (`PhoenixKitPhotos.API`); both take the same two steps,
so an agent cannot do what a person's screen would refuse.

`plan/4` reads and writes nothing: it checks every file for the person acting and says which
can go ahead and which cannot, and why. `run/2` does what a plan says. Nothing here trusts
a client's list: a file the person may not read counts as one that does not exist, and a
plan's files are read from the database, never taken from the request.

| Action | Needs | Does |
|---|---|---|
| `:download` | read | nothing here: `PhoenixKitPhotos.Web.Download` streams the plan |
| `:links` | read | nothing here: the page lists the addresses |
| `:copy` | read, and upload in the target library | a new file in the target library |
| `:move` | edit, and upload in the target | copy, then the original to the trash |
| `:tag` | edit | adds tags to `metadata["tags"]`, or removes them |
| `:trash` | edit | `Storage.trash_file/1`, which `:restore` undoes |
| `:restore` | edit | `Storage.restore_file/1` |

Nothing here deletes for good: a trashed file comes back with `:restore` until the site's
trash retention empties it. A move leaves the original in the trash, and the new file has
all it had (date, place, tags, title and description), since after the retention it is the
only one left. An image that has been edited stays where it is: its unedited original is not
something a new library could hold. Copying someone else's file brings only what core gives a
stranger (`Storage.link_copy/3`), so not their tags, their place or a date they set by hand.

Files here are the timeline's: photos and videos. `:trash` and `:move` ask the host's removal
guard (`config :phoenix_kit, :file_removal_guard`), and anything it says is in use stays.

# `action`

```elixir
@type action() :: :download | :links | :copy | :move | :tag | :trash | :restore
```

# `plan`

```elixir
@type plan() :: %{
  action: action(),
  auth: PhoenixKit.Users.Auth.Scope.t() | nil,
  files: [PhoenixKit.Modules.Storage.File.t()],
  refused: [refusal()],
  target: PhoenixKit.Modules.Storage.Library.t() | nil,
  add: [String.t()],
  remove: [String.t()]
}
```

# `reason`

```elixir
@type reason() ::
  :not_found
  | :not_allowed
  | :same_library
  | :already_there
  | :not_trashed
  | :trashed
  | :no_target
  | :edited
  | :in_use
```

# `refusal`

```elixir
@type refusal() :: %{id: String.t(), name: String.t() | nil, reason: reason()}
```

# `action`

```elixir
@spec action(term()) :: action() | nil
```

The action named by a client's string, or nil.

# `fingerprint`

```elixir
@spec fingerprint(plan()) :: String.t()
```

A hash of the values a plan would touch, for a confirmation that must fail when they changed:
the same for the same data, whatever order the ids came in, and untouched by what the action
does not depend on (a new tag does not void a confirmed trash).

# `limit`

```elixir
@spec limit() :: pos_integer()
```

The most files one action takes.

# `plan`

```elixir
@spec plan(action(), [String.t()], PhoenixKit.Users.Auth.Scope.t() | nil, map()) ::
  plan()
```

Checks `ids` for `action` by `auth`.

`params` holds what the action needs: `"library"` (a uuid) for `:copy` and `:move`, `"add"` and
`"remove"` (lists of tags, or text with commas) for `:tag`.

# `reason_text`

```elixir
@spec reason_text(atom()) :: String.t()
```

What to tell a person about a file that could not be included.

# `run`

```elixir
@spec run(plan(), keyword()) :: %{
  action: action(),
  done: [String.t()],
  failed: [refusal()],
  created: [String.t()]
}
```

Does what the plan says. Each file stands alone: one that fails is reported and the rest go
on. `:on_progress` is called with how many are done and how many there are.

# `tags`

```elixir
@spec tags(term()) :: [String.t()]
```

Tags as a list: from a list or from text with commas, trimmed, without doubles, at most 20 of
at most 60 characters.

---

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