PhoenixKitWeb.FileController (phoenix_kit v2.29.1)

Copy Markdown View Source

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

authorize_file_read(file, user)

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

info(conn, map)

Get file information without serving the file.

Request

GET /api/files/:file_uuid/info

Response

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"
    }
  ]
}

require_user(user)

@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_manifest(conn, map)

Serve the DZI manifest for an image, generating it lazily if it doesn't exist yet.

Request

GET /tiles/:token/:dzi_filename

where 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_tile(conn, map)

Serve a single DZI tile, generating it lazily if it doesn't exist yet.

Request

GET /tiles/:token/:files_segment/:level/:tile_filename

where 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).

show(conn, params)

Serve a file variant by ID with signed URL token.

Request

GET /file/:file_uuid/:variant/:token

Parameters

  • file_uuid: UUID of the file
  • variant: 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-Match matching the file ETag

Error (401):

"Invalid or expired token"

Error (404):

"File or variant not found"

unedited(conn, params)

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.

unedited_url(context, file_uuid, user_uuid, opts \\ [])

@spec unedited_url(term(), String.t(), String.t() | nil, keyword()) ::
  String.t() | nil

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.