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

What the photo uploader asks of the server: who may add to a library, which files
it takes, and storing one finished upload. No LiveView in here; the
`PhoenixKitPhotos.Components.PhotoUploader` component calls it, and so can anything
else that has a file on disk (an importer, a script).

## Who may

`can_upload?/2`: to a **user library**, its owner, a manager and a contributor
(`Libraries.allows?(role, :upload)`, so a viewer may not and a trashed library has
no one); to a **system library** such as Media, an Owner/Admin and a holder of the
`media` permission. Anything else, and a visitor who is not signed in, may not.
The check is made again before and after storage (`store/4`), because what
the browser says is not what the server decides. If access is lost during
storage, a newly created row is trashed; a pre-existing duplicate is retained.

## What it takes

Pictures and videos (`accept/0`): JPEG, PNG, GIF, WebP, HEIC/HEIF (an iPhone's),
AVIF, TIFF, BMP, SVG, camera raw (DNG, CR2, CR3, NEF, ARW, ORF, RW2, RAF, PEF…, when
the host has given MIME their types: `raw_extensions/0`); MP4, MOV (QuickTime), WebM,
3GP. A raw file is kept as it is; core develops its previews with libraw
(`PhoenixKit.Modules.Storage.CameraRaw`). (Every extension has to have a
known MIME type or LiveView refuses to set the upload up; `.m4v` and `.mkv` have none
in the MIME table, and phones record `.mov` and `.mp4`.) An SVG can carry a script,
so core never shows one as a page: it is kept as it is and downloads from its link,
and its sizes and previews are PNGs drawn from it by librsvg
(`PhoenixKit.Modules.Storage.SvgRaster`). Not documents: this is a photo library. The size limit is the site's `storage_max_upload_size_mb` setting
(default 500), the same one the media browser follows.

## Storing

`store/4` hashes the file from disk in chunks (`PhoenixKit.Utils.FileHash`), so a
video costs the same memory as a thumbnail, and hands it to
`PhoenixKit.Modules.Storage.store_file_in_buckets/7`, which keeps it, dedups it and
queues its renditions. It never raises: the caller is a progress callback.

# `outcome`

```elixir
@type outcome() ::
  {:added, map()}
  | {:duplicate, map()}
  | {:elsewhere, map()}
  | {:error, atom() | term()}
```

# `accept`

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

The file extensions the uploader takes, for `allow_upload/3`'s `:accept`.

# `can_upload?`

```elixir
@spec can_upload?(PhoenixKit.Users.Auth.Scope.t() | nil, term()) :: boolean()
```

Whether `scope` may add files to the library `library_uuid`.

# `entry_error`

```elixir
@spec entry_error(atom() | tuple()) :: String.t()
```

What to tell a person about a rejected entry (`Phoenix.Component.upload_errors/2`).

# `error_message`

```elixir
@spec error_message(term()) :: String.t()
```

What to tell a person about a file that did not get stored.

# `library`

```elixir
@spec library(term()) :: PhoenixKit.Modules.Storage.Library.t() | nil
```

The library, or nil.

# `max_concurrency_choice`

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

The most the panel's concurrency control offers.

# `max_concurrent_uploads`

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

How many files the uploader's hook keeps in flight at once when it feeds a drop to
LiveView: the panel's concurrency control starts here. The browser's own connection
limit (about six over HTTP/1.1) still caps what really transfers at the same time.

# `max_entries`

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

The most files one batch may hold.

# `max_file_size`

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

The largest file, in bytes: the site's `storage_max_upload_size_mb` setting.

# `raw_extensions`

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

The camera raw extensions the uploader takes: those of
`PhoenixKit.Modules.Storage.CameraRaw` whose type the host has given MIME, because
LiveView refuses to set an upload up for an extension MIME does not know. A host
turns them on with

    config :mime, :types, PhoenixKit.Modules.Storage.CameraRaw.mime_types()

written out as a literal map (config cannot call the module), followed by
`mix deps.compile mime --force`.

# `store`

```elixir
@spec store(Path.t(), map(), PhoenixKit.Users.Auth.Scope.t() | nil, term()) ::
  outcome()
```

Stores the finished upload at `path` (a LiveView upload `entry` says what it is
called and what the browser thought it was) in the library, for `scope`'s user.

  * `{:added, file}` — a new file (a duplicate that had been trashed is restored and
    counts as added: uploading it again means it is wanted)
  * `{:duplicate, file}` — the same bytes are already in this library
  * `{:elsewhere, file}` — the same bytes are already in another of this person's
    libraries, which this one cannot show
  * `{:error, reason}` — `:not_allowed`, `:unsupported_type`, or what storage said
    (`:quota_exceeded` when the library's owner has no room left for it)

---

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