Packages
phoenix_kit
1.7.28
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
Current section
Files
lib/phoenix_kit_web/controllers/upload_controller.ex
defmodule PhoenixKitWeb.UploadController do
@moduledoc """
File upload controller for handling multipart uploads.
Accepts file uploads, validates them, and queues them for background processing.
"""
use PhoenixKitWeb, :controller
alias PhoenixKit.Modules.Storage
alias PhoenixKit.Modules.Storage.File, as: StorageFile
alias PhoenixKit.Modules.Storage.ProcessFileJob
@upload_config %{
# 100MB max file size
max_size: 100 * 1024 * 1024,
allowed_types:
~w(image/jpeg image/png image/webp image/gif video/mp4 video/webm video/quicktime application/pdf)
}
@doc """
Upload a file via multipart form.
## Request
POST /api/upload
## Parameters
- `file` (required): The file to upload (multipart/form-data)
- `user_id` (optional): Override user ID (admin only)
## Response
Success (200):
{
"file_id": "uuidv7-string",
"original_filename": "photo.jpg",
"file_type": "image",
"mime_type": "image/jpeg",
"size": 1234567,
"status": "processing",
"message": "Upload successful, processing variants..."
}
Error (400):
{
"error": "INVALID_FILE_TYPE",
"message": "File type not allowed"
}
Error (413):
{
"error": "FILE_TOO_LARGE",
"message": "File size exceeds maximum allowed (100MB)"
}
"""
def create(conn, params) do
with {:ok, upload} <- extract_upload(params),
:ok <- validate_file_type(upload),
:ok <- validate_file_size(upload),
{:ok, user_id} <- get_current_user_id(conn, params),
{:ok, file_id} <- process_upload(upload, user_id) do
json(conn, %{
file_id: file_id,
status: "processing",
message: "Upload successful, variants will be generated shortly"
})
else
{:error, :invalid_file_type} ->
conn
|> put_status(:bad_request)
|> json(%{error: "INVALID_FILE_TYPE", message: "File type not allowed"})
{:error, :file_too_large} ->
conn
|> put_status(:request_entity_too_large)
|> json(%{
error: "FILE_TOO_LARGE",
message: "File size exceeds maximum allowed (#{format_bytes(@upload_config.max_size)})"
})
{:error, :no_user} ->
conn
|> put_status(:unauthorized)
|> json(%{error: "UNAUTHORIZED", message: "Authentication required"})
{:error, %Ecto.Changeset{} = changeset} ->
conn
|> put_status(:bad_request)
|> json(%{
error: "VALIDATION_ERROR",
message: "Invalid upload",
details: changeset_errors(changeset)
})
{:error, reason} ->
conn
|> put_status(:internal_server_error)
|> json(%{error: "UPLOAD_FAILED", message: "Upload failed: #{inspect(reason)}"})
end
end
defp extract_upload(params) do
case params["file"] do
%Plug.Upload{} = upload -> {:ok, upload}
_ -> {:error, :no_file}
end
end
defp validate_file_type(upload) do
if upload.content_type in @upload_config.allowed_types do
:ok
else
{:error, :invalid_file_type}
end
end
defp validate_file_size(upload) do
case File.stat(upload.path) do
{:ok, stat} ->
if stat.size <= @upload_config.max_size do
:ok
else
{:error, :file_too_large}
end
{:error, reason} ->
{:error, reason}
end
end
defp get_current_user_id(conn, params) do
# Check if user is authenticated
case conn.assigns[:phoenix_kit_current_user] do
%PhoenixKit.Users.Auth.User{id: user_id} ->
{:ok, user_id}
nil ->
# Try override from params (admin only)
case params["user_id"] do
user_id when is_binary(user_id) ->
# Verify admin permission here if needed
{:ok, user_id}
_ ->
{:error, :no_user}
end
end
end
defp process_upload(upload, user_id) do
with {:ok, stat} <- File.stat(upload.path),
{:ok, file_checksum} <- safe_calculate_file_hash(upload.path) do
file_size = stat.size
# Calculate user-specific checksum for per-user duplicate detection
user_file_checksum = Storage.calculate_user_file_checksum(user_id, file_checksum)
# Check if this user already uploaded this file
case Storage.get_file_by_user_checksum(user_file_checksum) do
%StorageFile{} = existing_file ->
# File already exists for this user, delete temp upload and return existing file
File.rm(upload.path)
{:ok, existing_file.id}
nil ->
# New file for this user, proceed with upload
perform_upload(upload, user_id, file_size, file_checksum)
end
else
{:error, reason} -> {:error, reason}
end
end
defp safe_calculate_file_hash(file_path) do
case File.read(file_path) do
{:ok, data} ->
hash =
data
|> then(&:crypto.hash(:md5, &1))
|> Base.encode16(case: :lower)
{:ok, hash}
{:error, reason} ->
{:error, "Failed to read file: #{inspect(reason)}"}
end
end
defp perform_upload(upload, user_id, _file_size, file_checksum) do
file_type = determine_file_type(upload.content_type)
ext = Path.extname(upload.filename) |> String.replace_leading(".", "")
# Store in buckets with hierarchical path structure
case Storage.store_file_in_buckets(upload.path, file_type, user_id, file_checksum, ext) do
{:ok, file} ->
# Queue background job for variant generation
%{file_id: file.id, user_id: user_id, filename: upload.filename}
|> ProcessFileJob.new()
|> Oban.insert()
{:ok, file.id}
{:error, reason} ->
{:error, reason}
end
end
defp determine_file_type(mime_type) do
cond do
String.starts_with?(mime_type, "image/") -> "image"
String.starts_with?(mime_type, "video/") -> "video"
mime_type == "application/pdf" -> "document"
true -> "other"
end
end
defp format_bytes(bytes) when is_integer(bytes) do
if bytes < 1024 do
"#{bytes} B"
else
units = ["KB", "MB", "GB", "TB"]
{value, unit} = calculate_size(bytes, units)
"#{Float.round(value, 2)} #{unit}"
end
end
defp calculate_size(bytes, [_unit | rest]) when bytes >= 1024 and rest != [] do
calculate_size(bytes / 1024, rest)
end
defp calculate_size(bytes, [unit | _]), do: {bytes, unit}
defp changeset_errors(changeset) do
Ecto.Changeset.traverse_errors(changeset, fn {msg, opts} ->
Regex.replace(~r"%{(\w+)}", msg, fn _, key ->
opts |> Keyword.get(String.to_existing_atom(key), msg) |> to_string()
end)
end)
end
end