Packages

phoenix_kit

1.7.21
1.7.208 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 live users media.ex
Raw

lib/phoenix_kit_web/live/users/media.ex

defmodule PhoenixKitWeb.Live.Users.Media do
@moduledoc """
Media management LiveView for PhoenixKit admin panel.
Provides interface for viewing and managing user media.
"""
use PhoenixKitWeb, :live_view
use Gettext, backend: PhoenixKitWeb.Gettext
require Logger
import Ecto.Query
alias PhoenixKit.Modules.Storage
alias PhoenixKit.Modules.Storage.FileInstance
alias PhoenixKit.Modules.Storage.URLSigner
alias PhoenixKit.Settings
alias PhoenixKit.Users.Auth
alias PhoenixKit.Utils.Routes
def mount(params, _session, socket) do
# Set locale for LiveView process
locale =
params["locale"] || socket.assigns[:current_locale]
# Batch load all settings needed for this page (uses cached settings for performance)
settings =
Settings.get_settings_cached(
["project_title"],
%{"project_title" => "PhoenixKit"}
)
# Check if any enabled buckets exist
enabled_buckets = Storage.list_enabled_buckets()
has_buckets = not Enum.empty?(enabled_buckets)
socket =
socket
|> maybe_allow_upload(has_buckets)
|> assign(:page_title, "Media")
|> assign(:project_title, settings["project_title"])
|> assign(:current_locale, locale)
|> assign(:url_path, Routes.path("/admin/media"))
|> assign(:show_upload, false)
|> assign(:last_uploaded_file_ids, [])
|> assign(:has_buckets, has_buckets)
{:ok, socket}
end
defp maybe_allow_upload(socket, has_buckets) do
if has_buckets do
allow_upload(socket, :media_files,
accept: ["image/*", "video/*", "application/pdf"],
max_entries: 10,
max_file_size: 100_000_000,
auto_upload: true
)
else
socket
end
end
def handle_params(params, _uri, socket) do
# Pagination setup
per_page = 50
page = String.to_integer(params["page"] || "1")
# Load existing files from database with pagination
{existing_files, total_count} = load_existing_files(page, per_page)
total_pages = ceil(total_count / per_page)
socket =
socket
|> assign(:uploaded_files, existing_files)
|> assign(:current_page, page)
|> assign(:per_page, per_page)
|> assign(:total_pages, total_pages)
|> assign(:total_count, total_count)
{:noreply, socket}
end
def handle_event("toggle_upload", _params, socket) do
{:noreply, assign(socket, :show_upload, !socket.assigns.show_upload)}
end
def handle_event("validate", _params, socket) do
# File selection event - files will auto-upload
entries = socket.assigns.uploads.media_files.entries
Logger.info("validate event: entries=#{length(entries)}")
if entries != [] do
Logger.info("validate: scheduling check_uploads_complete")
Process.send_after(self(), :check_uploads_complete, 500)
end
{:noreply, socket}
end
def handle_event("cancel_upload", %{"ref" => ref}, socket) do
{:noreply, cancel_upload(socket, :media_files, ref)}
end
def handle_info({:file_uploaded, file_id}, socket) do
# This event can be used by other modules listening to uploaded files
# For example, avatar upload systems can listen for this event
Logger.info("File uploaded with ID: #{file_id}")
{:noreply, socket}
end
def handle_info(:check_uploads_complete, socket) do
entries = socket.assigns.uploads.media_files.entries
Logger.info(
"check_uploads_complete: entries=#{length(entries)}, done?=#{inspect(Enum.map(entries, & &1.done?))}"
)
# Check if all entries are done uploading
if entries != [] && Enum.all?(entries, & &1.done?) do
Logger.info("All uploads done! Processing...")
# All done - process them
process_uploads(socket)
else
# Still uploading - check again later
Logger.info("Still uploading, checking again...")
Process.send_after(self(), :check_uploads_complete, 500)
{:noreply, socket}
end
end
defp process_uploads(socket) do
# Process uploaded files
uploaded_files =
consume_uploaded_entries(socket, :media_files, fn %{path: path}, entry ->
process_single_upload(socket, path, entry)
end)
# Reload paginated data from database to show newly uploaded files
per_page = socket.assigns.per_page || 50
page = socket.assigns.current_page || 1
{refreshed_files, total_count} = load_existing_files(page, per_page)
total_pages = ceil(total_count / per_page)
# Extract file IDs for callbacks (only from successful uploads)
file_ids = Enum.map(uploaded_files, &get_file_id/1) |> Enum.reject(&is_nil/1)
# Build flash message based on upload results
{flash_type, flash_message} = build_upload_flash_message(uploaded_files)
socket =
socket
|> assign(:uploaded_files, refreshed_files)
|> assign(:total_count, total_count)
|> assign(:total_pages, total_pages)
|> assign(:last_uploaded_file_ids, file_ids)
|> put_flash(flash_type, flash_message)
{:noreply, socket}
end
defp get_file_id({:ok, %{file_id: file_id}}), do: file_id
defp get_file_id({:postpone, _}), do: nil
defp get_file_id(_), do: nil
# Generate URLs from pre-loaded instances (no database query needed)
defp generate_urls_from_instances(instances, file_id) do
Enum.reduce(instances, %{}, fn instance, acc ->
url = URLSigner.signed_url(file_id, instance.variant_name)
Map.put(acc, instance.variant_name, url)
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" -> "pdf"
true -> "document"
end
end
# Format file size in human-readable format
defp format_file_size(bytes) when is_integer(bytes) do
cond do
bytes >= 1_000_000_000 -> "#{Float.round(bytes / 1_000_000_000, 2)} GB"
bytes >= 1_000_000 -> "#{Float.round(bytes / 1_000_000, 2)} MB"
bytes >= 1_000 -> "#{Float.round(bytes / 1_000, 2)} KB"
true -> "#{bytes} B"
end
end
# Get icon for file type
defp file_icon("image"), do: "hero-photo"
defp file_icon("video"), do: "hero-play-circle"
defp file_icon("pdf"), do: "hero-document-text"
defp file_icon("document"), do: "hero-document"
defp file_icon(_), do: "hero-document-arrow-down"
# Load existing files from database with pagination
defp load_existing_files(page, per_page) do
repo = PhoenixKit.Config.get_repo()
# Get total count
total_count = repo.aggregate(Storage.File, :count, :id)
# Calculate offset
offset = (page - 1) * per_page
# Query files ordered by most recent first with pagination
files =
from(f in Storage.File,
order_by: [desc: f.inserted_at],
limit: ^per_page,
offset: ^offset
)
|> repo.all()
# Batch load ALL file instances in ONE query instead of N queries
file_ids = Enum.map(files, & &1.id)
instances_by_file =
if file_ids != [] do
from(fi in FileInstance,
where: fi.file_id in ^file_ids
)
|> repo.all()
|> Enum.group_by(& &1.file_id)
else
%{}
end
# Convert to same format as uploaded files
existing_files =
Enum.map(files, fn file ->
# Get pre-loaded instances for this file (no DB query!)
instances = Map.get(instances_by_file, file.id, [])
urls = generate_urls_from_instances(instances, file.id)
%{
file_id: file.id,
filename: file.original_file_name || file.file_name || "Unknown",
original_filename: file.original_file_name,
file_type: file.file_type,
mime_type: file.mime_type,
size: file.size || 0,
status: file.status,
urls: urls
}
end)
{existing_files, total_count}
end
defp process_single_upload(socket, path, entry) do
# Get file info
ext = Path.extname(entry.client_name) |> String.replace_leading(".", "")
mime_type = entry.client_type || MIME.from_path(entry.client_name)
file_type = determine_file_type(mime_type)
# Get current user
current_user = socket.assigns.phoenix_kit_current_user
user_id = if current_user, do: current_user.id, else: 1
# Get file size
{:ok, stat} = Elixir.File.stat(path)
file_size = stat.size
# Calculate hash
file_hash = Auth.calculate_file_hash(path)
# Store file in storage
case Storage.store_file_in_buckets(
path,
file_type,
user_id,
file_hash,
ext,
entry.client_name
) do
{:ok, file, :duplicate} ->
build_upload_result(file, entry, file_type, mime_type, file_size, true)
{:ok, file} ->
build_upload_result(file, entry, file_type, mime_type, file_size, false)
{:error, reason} ->
Logger.error("Storage Error: #{inspect(reason)}")
{:postpone, reason}
end
end
defp build_upload_result(file, entry, file_type, mime_type, file_size, is_duplicate) do
result = %{
file_id: file.id,
filename: entry.client_name,
file_type: file_type,
mime_type: mime_type,
size: file_size,
status: file.status,
urls: %{}
}
result = if is_duplicate, do: Map.put(result, :duplicate, true), else: result
{:ok, result}
end
defp build_upload_flash_message(uploaded_files) do
error_count = Enum.count(uploaded_files, &match?({:postpone, _}, &1))
successful_uploads = Enum.reject(uploaded_files, &match?({:postpone, _}, &1))
duplicate_count =
Enum.count(successful_uploads, fn
{:ok, %{duplicate: true}} -> true
_ -> false
end)
new_count = length(successful_uploads) - duplicate_count
build_flash_from_counts(error_count, new_count, duplicate_count)
end
# Build flash message based on upload counts
defp build_flash_from_counts(error_count, new_count, duplicate_count) do
cond do
all_failed?(error_count, new_count, duplicate_count) ->
build_all_failed_message()
partial_success?(error_count, new_count) ->
build_partial_success_message(new_count, error_count)
only_duplicates?(duplicate_count, new_count) ->
build_only_duplicates_message(duplicate_count)
new_files_only?(new_count, duplicate_count) ->
build_new_files_only_message(new_count)
new_and_duplicates?(new_count, duplicate_count) ->
build_new_and_duplicates_message(new_count, duplicate_count)
true ->
{:info, "Upload processed"}
end
end
# Check if all uploads failed
defp all_failed?(error_count, new_count, duplicate_count) do
error_count > 0 && new_count == 0 && duplicate_count == 0
end
# Check if partial success (some errors, some successful)
defp partial_success?(error_count, new_count) do
error_count > 0 && new_count > 0
end
# Check if only duplicates (no new files)
defp only_duplicates?(duplicate_count, new_count) do
duplicate_count > 0 && new_count == 0
end
# Check if only new files (no duplicates)
defp new_files_only?(new_count, duplicate_count) do
new_count > 0 && duplicate_count == 0
end
# Check if both new files and duplicates
defp new_and_duplicates?(new_count, duplicate_count) do
new_count > 0 && duplicate_count > 0
end
# Flash message builders
defp build_all_failed_message do
{:error,
"Upload failed: No storage buckets configured. Please configure at least one storage bucket before uploading files."}
end
defp build_partial_success_message(new_count, error_count) do
{:warning,
"Partially successful: #{new_count} file(s) uploaded, #{error_count} failed due to missing storage buckets."}
end
defp build_only_duplicates_message(duplicate_count) do
{:info, "Already have #{duplicate_count} duplicate file(s). No new files were added."}
end
defp build_new_files_only_message(new_count) do
{:info, "Upload successful! #{new_count} new file(s) processed"}
end
defp build_new_and_duplicates_message(new_count, duplicate_count) do
{:info,
"Upload successful! #{new_count} new file(s) added. #{duplicate_count} file(s) were already uploaded."}
end
end