File serving controller with signed URL support.
Handles secure file retrieval with token-based authentication and cache headers.
Summary
Functions
Authorizes a file-info read. The owner (file.user_uuid) or an Owner/Admin may
read; everyone else — and a missing file — is {:error, :not_found}, the
same result, so the endpoint is not a file-existence oracle.
Get file information without serving the file.
Requires an authenticated caller for the info endpoint. It lives in the
unauthenticated [:browser, :phoenix_kit_auto_setup] scope, so this is the
gate: before it, any visitor got file metadata and a freshly-minted signed URL
for any uuid, which handed out capability URLs the signing scheme exists to
withhold (issue #687 class).
Serve the DZI manifest for an image, generating it lazily if it doesn't exist yet.
Serve a single DZI tile, generating it lazily if it doesn't exist yet.
Serve a file variant by ID with signed URL token.
Downloads an edited image's unedited original.
A link to file_uuid's unedited original (see unedited/2) that works
for the signed-in user user_uuid for an hour, whether or not
ImageEditing would let them edit the file on its own. Hand it out only
where that decision has been made — the image editor does.
Functions
@spec authorize_file_read(term(), PhoenixKit.Users.Auth.User.t()) :: {:ok, map()} | {:error, :not_found}
Authorizes a file-info read. The owner (file.user_uuid) or an Owner/Admin may
read; everyone else — and a missing file — is {:error, :not_found}, the
same result, so the endpoint is not a file-existence oracle.
The staff bypass is Scope.system_role?/1 (Owner/Admin), NOT
can_access_admin_area?/1: a holder of a single module permission must not be
able to read every other user's file metadata and signed variant URLs.
Get file information without serving the file.
Request
GET /api/files/:file_uuid/infoResponse
Success (200):
{
"file_uuid": "uuid",
"original_filename": "photo.jpg",
"mime_type": "image/jpeg",
"file_type": "image",
"size": 1234567,
"status": "active",
"variants": [
{
"variant_name": "original",
"mime_type": "image/jpeg",
"size": 1234567,
"width": 1920,
"height": 1080,
"url": "/file/uuid/original/token"
}
]
}
@spec require_user(term()) :: {:ok, PhoenixKit.Users.Auth.User.t()} | {:error, :no_user}
Requires an authenticated caller for the info endpoint. It lives in the
unauthenticated [:browser, :phoenix_kit_auto_setup] scope, so this is the
gate: before it, any visitor got file metadata and a freshly-minted signed URL
for any uuid, which handed out capability URLs the signing scheme exists to
withhold (issue #687 class).
Serve the DZI manifest for an image, generating it lazily if it doesn't exist yet.
Request
GET /tiles/:token/:dzi_filenamewhere dzi_filename is "<file_uuid>-<version>.dzi" (version as in
URLSigner.version/1, of the file's original) and token is the signed
per-file token from URLSigner.generate_token(file_uuid, "dzi").
Returns the XML manifest describing the image's dimensions and tile
config — Tessera's generate_manifest/3 produces it on first request,
subsequent requests serve from storage.
The version is in the path, not a query string, because the viewer derives
tile URLs from the manifest's path. Tiles are cut from one version of the
image and stored under it, so an edit never serves new tiles under an old
URL (or old ones under a new URL): a version that is not the current one is
a 404, and so is every tile request while an edit is rendering. The legacy
unversioned "<file_uuid>.dzi" resolves to the current version and is not
cached.
The token gates BOTH manifest and tile generation: without it, the
endpoint is a 404. The MediaBrowser emits manifest URLs only when
storage_tile_generation_enabled is on, so unauthenticated callers
can't trigger lazy ImageMagick work by guessing UUIDs.
Serve a single DZI tile, generating it lazily if it doesn't exist yet.
Request
GET /tiles/:token/:files_segment/:level/:tile_filenamewhere token is the signed per-file token (same one used by
serve_manifest/2), files_segment is "<file_uuid>-<version>_files"
(or the legacy "<file_uuid>_files"),
level is the integer zoom level, and tile_filename is
"<col>_<row>.<ext>". This matches the layout Tessera writes to
storage and the URL convention OpenSeadragon derives from a DZI
manifest's base URL (token in the path survives that derivation;
query-string tokens don't).
Serve a file variant by ID with signed URL token.
Request
GET /file/:file_uuid/:variant/:tokenParameters
file_uuid: UUID of the filevariant: Variant name (e.g., "original", "thumbnail", "medium")token: Signed token for authentication
Response
Success (200):
- File streamed to client with appropriate headers:
Cache-Control: public, max-age=31536000, immutable(1 year)ETag: "md5-hash"Content-Type: <mime-type>Content-Disposition: inline; filename="..."
Not Modified (304):
- Returned when request includes
If-None-Matchmatching the file ETag
Error (401):
"Invalid or expired token"Error (404):
"File or variant not found"
Downloads an edited image's unedited original.
Request
GET /api/files/:file_uuid/unedited[?t=<token>][&variant=<name>]Only for a signed-in user who may edit the file
(ImageEditing.can_edit?/2: its owner, an Owner/Admin, or a holder of the
"media" permission) — or who holds a link from unedited_url/4, made for
them within the last hour by a host that decided they may manage the file.
Anyone else, and a file with no unedited original, gets the same 404.
variant (a variant the original had, e.g. "large") is served inline,
for a preview; without it the original is an attachment. Nothing here may
be cached: the unedited bytes are what an edit (a redaction) may hide.
A link to file_uuid's unedited original (see unedited/2) that works
for the signed-in user user_uuid for an hour, whether or not
ImageEditing would let them edit the file on its own. Hand it out only
where that decision has been made — the image editor does.
context is what Phoenix.Token signs with: a conn, a LiveView socket or
an endpoint. variant: links a preview-sized variant instead. nil
without a user.