Packages

phoenix_kit

1.7.20
1.7.207 1.7.206 1.7.205 1.7.204 1.7.203 1.7.202 1.7.201 1.7.200 1.7.199 1.7.198 1.7.197 1.7.196 1.7.194 1.7.193 1.7.192 1.7.191 1.7.190 1.7.189 1.7.187 1.7.186 1.7.185 1.7.184 1.7.183 1.7.182 1.7.181 1.7.180 1.7.179 1.7.178 1.7.177 1.7.176 1.7.175 1.7.174 1.7.173 1.7.172 1.7.171 1.7.170 1.7.169 1.7.168 1.7.167 1.7.166 1.7.165 1.7.164 1.7.162 1.7.161 1.7.160 1.7.159 1.7.157 1.7.156 1.7.155 1.7.154 1.7.153 1.7.152 1.7.151 1.7.150 1.7.149 1.7.146 1.7.145 1.7.144 1.7.143 1.7.138 1.7.133 1.7.132 1.7.131 1.7.130 1.7.128 1.7.126 1.7.125 1.7.121 1.7.120 1.7.119 1.7.118 1.7.117 1.7.116 1.7.115 1.7.114 1.7.113 1.7.112 1.7.111 1.7.110 1.7.109 1.7.108 1.7.107 1.7.106 1.7.105 1.7.104 1.7.103 1.7.102 1.7.101 1.7.100 1.7.99 1.7.98 1.7.97 1.7.96 1.7.95 1.7.94 1.7.93 1.7.92 1.7.91 1.7.90 1.7.89 1.7.88 1.7.87 1.7.86 1.7.85 1.7.84 1.7.83 1.7.82 1.7.81 1.7.80 1.7.79 1.7.78 1.7.77 1.7.76 1.7.75 1.7.74 1.7.71 1.7.70 1.7.69 1.7.66 1.7.65 1.7.64 1.7.63 1.7.62 1.7.61 1.7.59 1.7.58 1.7.57 1.7.56 1.7.55 1.7.54 1.7.53 1.7.52 1.7.51 1.7.49 1.7.44 1.7.43 1.7.42 1.7.41 1.7.39 1.7.38 1.7.37 1.7.36 1.7.34 1.7.33 1.7.31 1.7.30 1.7.29 1.7.28 1.7.27 1.7.26 1.7.25 1.7.24 1.7.23 1.7.22 1.7.21 1.7.20 1.7.19 1.7.18 1.7.17 1.7.16 1.7.15 1.7.14 1.7.13 1.7.12 1.7.11 1.7.10 1.7.9 1.7.8 1.7.7 1.7.6 1.7.5 1.7.4 1.7.3 1.7.2 1.7.1 1.7.0 1.6.20 1.6.19 1.6.18 1.6.17 1.6.16 1.6.15 1.6.14 1.6.13 1.6.12 1.6.11 1.6.10 1.6.9 1.6.8 1.6.7 1.6.6 1.6.5 1.6.4 1.6.3 1.5.2 1.5.1 1.5.0 1.4.9 1.4.8 1.4.7 1.4.6 1.4.5 1.4.4 1.4.3 1.4.2 1.4.1 1.4.0 1.3.2 1.3.1 1.3.0 1.2.10 1.2.9 1.2.8 1.2.7 1.2.5 1.2.4 1.2.2 1.2.1 1.2.0 1.1.0 1.0.0

A foundation for building Elixir Phoenix apps — SaaS, social networks, ERP systems, marketplaces, and more

Current section

Files

Jump to
phoenix_kit lib phoenix_kit_web controllers file_controller.ex
Raw

lib/phoenix_kit_web/controllers/file_controller.ex

defmodule PhoenixKitWeb.FileController do
@moduledoc """
File serving controller with signed URL support.
Handles secure file retrieval with token-based authentication and cache headers.
"""
use PhoenixKitWeb, :controller
alias PhoenixKit.Modules.Storage
alias PhoenixKit.Modules.Storage.{Manager, ProcessFileJob, URLSigner}
alias PhoenixKit.Utils.Routes
@doc """
Serve a file variant by ID with signed URL token.
## Request
GET /file/:file_id/:variant/:token
## Parameters
- `file_id`: 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` (1 year)
- `ETag: "md5-hash"`
- `Content-Type: <mime-type>`
- `Content-Disposition: inline; filename="..."`
Error (401):
"Invalid or expired token"
Error (404):
"File or variant not found"
"""
def show(conn, %{"file_id" => file_id, "variant" => variant, "token" => token}) do
with {:ok, file} <- get_file(file_id),
:ok <- verify_token(file_id, variant, token),
{:ok, instance} <- get_file_instance(file_id, variant),
{:ok, temp_path} <- retrieve_file_variant(instance) do
# Set cache headers
conn =
conn
|> put_resp_header("cache-control", "public, max-age=31536000, immutable")
|> put_resp_header("etag", ~s("#{instance.checksum}"))
|> put_resp_header(
"content-disposition",
~s(inline; filename="#{file.original_file_name}")
)
# Set content type
conn = put_resp_content_type(conn, instance.mime_type)
# Stream file to client, then clean up temp file
conn = send_file(conn, 200, temp_path)
# Clean up temp file after send (send_file is synchronous in Plug)
File.rm(temp_path)
conn
else
{:error, :invalid_token} ->
conn
|> put_status(:unauthorized)
|> text("Invalid or expired token")
{:error, :not_found} ->
conn
|> put_status(:not_found)
|> text("File or variant not found")
{:error, reason} ->
conn
|> put_status(:internal_server_error)
|> text("Error retrieving file: #{inspect(reason)}")
end
end
@doc """
Get file information without serving the file.
## Request
GET /api/files/:file_id/info
## Response
Success (200):
{
"file_id": "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"
}
]
}
"""
def info(conn, %{"file_id" => file_id}) do
case get_file(file_id) do
{:ok, file} ->
instances = Storage.list_file_instances(file_id)
variant_urls =
Enum.map(instances, fn instance ->
token = URLSigner.generate_token(file_id, instance.variant_name)
file_path = "/file/#{file_id}/#{instance.variant_name}/#{token}"
url = Routes.path(file_path)
%{
variant_name: instance.variant_name,
mime_type: instance.mime_type,
size: instance.size,
width: instance.width,
height: instance.height,
url: url
}
end)
json(conn, %{
file_id: file.id,
original_filename: file.original_file_name,
mime_type: file.mime_type,
file_type: file.file_type,
size: file.size,
status: file.status,
variants: variant_urls
})
{:error, :not_found} ->
conn
|> put_status(:not_found)
|> json(%{error: "FILE_NOT_FOUND", message: "File not found"})
end
end
defp get_file(file_id) do
case Storage.get_file(file_id) do
nil -> {:error, :not_found}
file -> {:ok, file}
end
end
defp get_file_instance(file_id, variant) do
case Storage.get_file_instance_by_name(file_id, variant) do
nil ->
# Variant doesn't exist, try to get the original to queue generation
case Storage.get_file_instance_by_name(file_id, "original") do
nil ->
{:error, :not_found}
original_instance ->
# Queue the variant for generation if not already requested
queue_missing_variant(file_id, variant, original_instance)
# Return the original for now
{:ok, original_instance}
end
instance ->
{:ok, instance}
end
end
defp queue_missing_variant(file_id, _variant, original_instance) do
# Queue background job to generate the missing variant
Task.start(fn ->
case Storage.get_file(file_id) do
nil ->
:error
file ->
%{file_id: file_id, user_id: file.user_id, filename: original_instance.file_name}
|> ProcessFileJob.new()
|> Oban.insert()
end
end)
end
defp verify_token(file_id, variant, token) do
if URLSigner.verify_token(file_id, variant, token) do
:ok
else
{:error, :invalid_token}
end
end
defp retrieve_file_variant(instance) do
# Create temp path with unique suffix to prevent race conditions
# when multiple requests for the same file arrive simultaneously
temp_dir = System.tmp_dir!()
ext = Path.extname(instance.file_name)
random_suffix = :crypto.strong_rand_bytes(4) |> Base.encode16(case: :lower)
temp_path = Path.join(temp_dir, "phoenix_kit_#{instance.id}_#{random_suffix}#{ext}")
# Retrieve from storage
case Manager.retrieve_file(instance.file_name,
destination_path: temp_path
) do
{:ok, _} ->
{:ok, temp_path}
{:error, reason} ->
{:error, reason}
end
end
end