PhoenixKitWeb.FileController (phoenix_kit v2.0.0)

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.

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>.dzi" 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 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>_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, map)

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"