PhoenixKitPhotos.Uploads (PhoenixKit Photos v0.3.0)

Copy Markdown View Source

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.

Summary

Functions

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

Whether scope may add files to the library library_uuid.

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

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

The library, or nil.

The most the panel's concurrency control offers.

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.

The most files one batch may hold.

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

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

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.

Types

outcome()

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

Functions

accept()

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

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

can_upload?(scope, library_uuid)

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

Whether scope may add files to the library library_uuid.

entry_error(arg1)

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

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

error_message(arg1)

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

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

library(uuid)

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

The library, or nil.

max_concurrency_choice()

@spec max_concurrency_choice() :: pos_integer()

The most the panel's concurrency control offers.

max_concurrent_uploads()

@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()

@spec max_entries() :: pos_integer()

The most files one batch may hold.

max_file_size()

@spec max_file_size() :: pos_integer()

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

raw_extensions()

@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(path, entry, scope, library_uuid)

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