Packages
phoenix_kit
1.7.159
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/modules/storage/web/settings.ex
defmodule PhoenixKitWeb.Live.Modules.Storage.Settings do
@moduledoc """
Storage settings management LiveView for PhoenixKit.
Provides configuration interface for the distributed file storage system.
"""
use PhoenixKitWeb, :live_view
use Gettext, backend: PhoenixKitWeb.Gettext
require Logger
import Ecto.Query
alias PhoenixKit.Modules.Storage
alias PhoenixKit.Settings
alias PhoenixKit.System.Dependencies
alias PhoenixKit.Utils.Routes
def mount(_params, _session, socket) do
# Get current path for navigation
current_path = get_current_path(socket, %{})
# Get project title from settings
project_title = Settings.get_project_title()
# Load buckets
buckets = Storage.list_buckets()
# Load file counts per bucket (unique files, not instances)
bucket_file_counts = get_bucket_file_counts(buckets)
# Load storage settings from database (using basic function to avoid cache issues)
redundancy_copies = Settings.get_setting("storage_redundancy_copies", "1")
auto_generate_variants = Settings.get_setting("storage_auto_generate_variants", "true")
default_bucket_uuid = Settings.get_setting("storage_default_bucket_uuid", nil)
max_upload_size_mb = Settings.get_setting("storage_max_upload_size_mb", "500")
tile_generation_enabled = Settings.get_setting("storage_tile_generation_enabled", "false")
# Calculate maximum redundancy based on available buckets
active_buckets = Enum.count(buckets, & &1.enabled)
max_redundancy = if active_buckets > 0, do: active_buckets, else: 1
# Keep user's current redundancy setting unchanged
current_redundancy = String.to_integer(redundancy_copies)
# Store form values for batch updates
form_redundancy = current_redundancy
form_auto_generate_variants = auto_generate_variants == "true"
form_tile_generation_enabled = tile_generation_enabled == "true"
current_max_upload_size_mb = String.to_integer(max_upload_size_mb)
# Check system dependencies
imagemagick_status = Dependencies.check_imagemagick_cached()
ffmpeg_status = Dependencies.check_ffmpeg_cached()
socket =
socket
|> assign(:current_path, current_path)
|> assign(:page_title, "Media Settings")
|> assign(:project_title, project_title)
|> assign(:buckets, buckets)
|> assign(:bucket_file_counts, bucket_file_counts)
|> assign(:redundancy_copies, current_redundancy)
|> assign(:auto_generate_variants, auto_generate_variants == "true")
|> assign(:tile_generation_enabled, tile_generation_enabled == "true")
|> assign(:default_bucket_uuid, default_bucket_uuid)
|> assign(:active_buckets_count, active_buckets)
|> assign(:max_redundancy, max_redundancy)
|> assign(:form_redundancy, form_redundancy)
|> assign(:form_auto_generate_variants, form_auto_generate_variants)
|> assign(:form_tile_generation_enabled, form_tile_generation_enabled)
|> assign(:max_upload_size_mb, current_max_upload_size_mb)
|> assign(:form_max_upload_size_mb, current_max_upload_size_mb)
|> assign(:imagemagick_status, imagemagick_status)
|> assign(:ffmpeg_status, ffmpeg_status)
{:ok, socket}
end
def handle_event("update_redundancy", %{"redundancy_copies" => copies}, socket) do
requested_copies = String.to_integer(copies)
max_redundancy = socket.assigns.max_redundancy
if requested_copies > max_redundancy do
socket =
socket
|> put_flash(
:error,
"Cannot set redundancy to #{requested_copies} copies. Only #{max_redundancy} active bucket(s) available."
)
{:noreply, socket}
else
case Settings.update_setting("storage_redundancy_copies", copies) do
{:ok, _setting} ->
# Settings.update_setting already handles cache invalidation
socket =
socket
|> assign(:redundancy_copies, requested_copies)
|> put_flash(
:info,
"Redundancy settings updated to #{requested_copies} #{if requested_copies == 1, do: "copy", else: "copies"}"
)
{:noreply, socket}
{:error, _changeset} ->
socket = put_flash(socket, :error, "Failed to update redundancy settings")
{:noreply, socket}
end
end
end
def handle_event("update_form_redundancy", %{"form_redundancy" => copies}, socket) do
# Handle both string and integer inputs
form_redundancy =
cond do
is_integer(copies) -> copies
is_binary(copies) -> String.to_integer(copies)
# fallback
true -> 1
end
socket =
socket
|> assign(:form_redundancy, form_redundancy)
{:noreply, socket}
end
def handle_event("update_form_variants", %{"form_auto_generate_variants" => value}, socket) do
form_auto_generate_variants = value == "true"
socket =
socket
|> assign(:form_auto_generate_variants, form_auto_generate_variants)
{:noreply, socket}
end
def handle_event("toggle_form_tile_generation", _params, socket) do
new_value = not socket.assigns.form_tile_generation_enabled
{:noreply, assign(socket, :form_tile_generation_enabled, new_value)}
end
def handle_event("toggle_form_variants", _params, socket) do
new_value = not socket.assigns.form_auto_generate_variants
socket =
socket
|> assign(:form_auto_generate_variants, new_value)
{:noreply, socket}
end
def handle_event("update_storage_form", params, socket) do
form_redundancy =
case params["form_redundancy"] do
nil -> socket.assigns.form_redundancy
val when is_integer(val) -> val
val when is_binary(val) -> parse_integer(val, socket.assigns.form_redundancy)
_ -> socket.assigns.form_redundancy
end
form_max_upload_size_mb =
case params["form_max_upload_size_mb"] do
nil ->
socket.assigns.form_max_upload_size_mb
val when is_integer(val) ->
max(val, 1)
val when is_binary(val) ->
max(parse_integer(val, socket.assigns.form_max_upload_size_mb), 1)
_ ->
socket.assigns.form_max_upload_size_mb
end
socket =
socket
|> assign(:form_redundancy, form_redundancy)
|> assign(:form_max_upload_size_mb, form_max_upload_size_mb)
{:noreply, socket}
end
def handle_event("apply_storage_settings", _params, socket) do
# Get current form values
new_redundancy = socket.assigns.form_redundancy
new_variants = if socket.assigns.form_auto_generate_variants, do: "true", else: "false"
new_tile_generation =
if socket.assigns.form_tile_generation_enabled, do: "true", else: "false"
new_max_upload_size_mb = socket.assigns.form_max_upload_size_mb
# Validate redundancy doesn't exceed available buckets
max_redundancy = socket.assigns.max_redundancy
if new_redundancy > max_redundancy do
socket =
socket
|> put_flash(
:error,
"Cannot set redundancy to #{new_redundancy} copies. Only #{max_redundancy} active bucket(s) available."
)
{:noreply, socket}
else
# Update all settings
redundancy_result =
Settings.update_setting("storage_redundancy_copies", to_string(new_redundancy))
variants_result = Settings.update_setting("storage_auto_generate_variants", new_variants)
Settings.update_setting("storage_tile_generation_enabled", new_tile_generation)
Settings.update_setting(
"storage_max_upload_size_mb",
to_string(new_max_upload_size_mb)
)
case {redundancy_result, variants_result} do
{{:ok, _}, {:ok, _}} ->
# Verify the settings were saved correctly by reading them back
saved_redundancy = Settings.get_setting("storage_redundancy_copies", "1")
saved_variants = Settings.get_setting("storage_auto_generate_variants", "true")
saved_tile_generation = Settings.get_setting("storage_tile_generation_enabled", "false")
saved_max_upload = Settings.get_setting("storage_max_upload_size_mb", "500")
socket =
socket
|> assign(:redundancy_copies, String.to_integer(saved_redundancy))
|> assign(:auto_generate_variants, saved_variants == "true")
|> assign(:tile_generation_enabled, saved_tile_generation == "true")
|> assign(:form_redundancy, String.to_integer(saved_redundancy))
|> assign(:form_auto_generate_variants, saved_variants == "true")
|> assign(:form_tile_generation_enabled, saved_tile_generation == "true")
|> assign(:max_upload_size_mb, String.to_integer(saved_max_upload))
|> assign(:form_max_upload_size_mb, String.to_integer(saved_max_upload))
|> put_flash(:info, "Storage settings updated successfully")
{:noreply, socket}
{{:error, _}, {:ok, _}} ->
socket = put_flash(socket, :error, "Failed to update redundancy settings")
{:noreply, socket}
{{:ok, _}, {:error, _}} ->
socket = put_flash(socket, :error, "Failed to update variant settings")
{:noreply, socket}
{{:error, _}, {:error, _}} ->
socket = put_flash(socket, :error, "Failed to update storage settings")
{:noreply, socket}
end
end
end
def handle_event("toggle_variants", _params, socket) do
new_value = if socket.assigns.auto_generate_variants, do: "false", else: "true"
case Settings.update_setting("storage_auto_generate_variants", new_value) do
{:ok, _setting} ->
# Settings.update_setting already handles cache invalidation
socket =
socket
|> assign(:auto_generate_variants, new_value == "true")
|> put_flash(
:info,
"Auto-variant generation #{if new_value == "true", do: "enabled", else: "disabled"}"
)
{:noreply, socket}
{:error, _changeset} ->
socket = put_flash(socket, :error, "Failed to update variant settings")
{:noreply, socket}
end
end
def handle_event("update_default_bucket", %{"bucket_uuid" => bucket_uuid}, socket) do
new_value = if bucket_uuid == "", do: nil, else: bucket_uuid
case Settings.update_setting("storage_default_bucket_uuid", new_value) do
{:ok, _setting} ->
socket =
socket
|> assign(:default_bucket_uuid, new_value)
|> put_flash(:info, "Default bucket updated")
{:noreply, socket}
{:error, _changeset} ->
socket = put_flash(socket, :error, "Failed to update default bucket")
{:noreply, socket}
end
end
def handle_event("toggle_bucket", %{"id" => bucket_uuid}, socket) do
case Storage.get_bucket(bucket_uuid) do
nil ->
{:noreply, put_flash(socket, :error, "Bucket not found")}
bucket ->
new_enabled = !bucket.enabled
case Storage.update_bucket(bucket, %{enabled: new_enabled}) do
{:ok, _bucket} ->
action = if new_enabled, do: "enabled", else: "disabled"
socket = reload_settings_data(socket)
{:noreply, put_flash(socket, :info, "Bucket #{action} successfully")}
{:error, _changeset} ->
{:noreply, put_flash(socket, :error, "Failed to update bucket")}
end
end
end
def handle_event("delete_bucket", %{"id" => bucket_uuid}, socket) do
bucket = Storage.get_bucket(bucket_uuid)
case Storage.delete_bucket(bucket) do
{:ok, _bucket} ->
# Reload buckets and recalculate max redundancy
buckets = Storage.list_buckets()
active_buckets_count = Enum.count(buckets, & &1.enabled)
max_redundancy = max(1, active_buckets_count)
socket =
socket
|> assign(:buckets, buckets)
|> assign(:active_buckets_count, active_buckets_count)
|> assign(:max_redundancy, max_redundancy)
|> put_flash(:info, "Bucket deleted successfully")
{:noreply, socket}
{:error, _changeset} ->
socket = put_flash(socket, :error, "Failed to delete bucket")
{:noreply, socket}
end
end
def handle_event("repair_storage_module", _params, socket) do
case Storage.repair_storage_module() do
{:ok, repairs} ->
repair_summary = format_repairs(repairs)
socket =
socket
|> reload_settings_data()
|> put_flash(:info, "Storage module repaired: #{repair_summary}")
{:noreply, socket}
{:error, reason} ->
socket = put_flash(socket, :error, "Failed to repair: #{inspect(reason)}")
{:noreply, socket}
end
end
defp get_current_path(_socket, _session) do
# For Storage settings page
Routes.path("/admin/settings/media")
end
# Helper function to get full path for a bucket
defp get_bucket_full_path(bucket) do
case bucket.provider do
"local" ->
bucket.endpoint || "No path configured"
provider when provider in ["s3", "b2", "r2"] ->
path_parts = [
provider <> ":",
if(bucket.bucket_name, do: bucket.bucket_name, else: "no-bucket"),
if(bucket.endpoint, do: bucket.endpoint, else: "/")
]
Enum.join(path_parts, "")
_ ->
"#{bucket.provider}: unknown configuration"
end
end
# Get count of unique files stored on each bucket
defp get_bucket_file_counts(buckets) do
repo = PhoenixKit.Config.get_repo()
Enum.reduce(buckets, %{}, fn bucket, acc ->
# Count distinct files that have at least one instance located on this bucket
# We count files, not instances or locations
count =
repo.one(
from f in Storage.File,
join: fi in Storage.FileInstance,
on: fi.file_uuid == f.uuid,
join: fl in Storage.FileLocation,
on: fl.file_instance_uuid == fi.uuid,
where: fl.bucket_uuid == ^bucket.uuid and fl.status == "active",
select: count(f.uuid, :distinct)
)
Map.put(acc, bucket.uuid, count || 0)
end)
rescue
_ -> %{}
end
defp format_repairs(repairs) do
Enum.map_join(repairs, ", ", fn
{:bucket_created, name} -> "created bucket '#{name}'"
{:dimensions_reset, count} -> "reset #{count} dimensions"
{:settings_reset, count} -> "reset #{count} settings"
end)
end
defp reload_settings_data(socket) do
# Reload buckets
buckets = Storage.list_buckets()
bucket_file_counts = get_bucket_file_counts(buckets)
# Reload storage settings
redundancy_copies = Settings.get_setting("storage_redundancy_copies", "1")
auto_generate_variants = Settings.get_setting("storage_auto_generate_variants", "true")
default_bucket_uuid = Settings.get_setting("storage_default_bucket_uuid", nil)
max_upload_size_mb = Settings.get_setting("storage_max_upload_size_mb", "500")
# Recalculate max redundancy
active_buckets_count = Enum.count(buckets, & &1.enabled)
max_redundancy = if active_buckets_count > 0, do: active_buckets_count, else: 1
current_redundancy = String.to_integer(redundancy_copies)
current_max_upload_size_mb = String.to_integer(max_upload_size_mb)
socket
|> assign(:buckets, buckets)
|> assign(:bucket_file_counts, bucket_file_counts)
|> assign(:redundancy_copies, current_redundancy)
|> assign(:auto_generate_variants, auto_generate_variants == "true")
|> assign(:default_bucket_uuid, default_bucket_uuid)
|> assign(:active_buckets_count, active_buckets_count)
|> assign(:max_redundancy, max_redundancy)
|> assign(:form_redundancy, current_redundancy)
|> assign(:form_auto_generate_variants, auto_generate_variants == "true")
|> assign(:max_upload_size_mb, current_max_upload_size_mb)
|> assign(:form_max_upload_size_mb, current_max_upload_size_mb)
end
defp parse_integer(val, fallback) do
case Integer.parse(val) do
{int, _} -> int
:error -> fallback
end
end
end