Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 4 additions & 4 deletions lib/modules/storage/spec.md
Original file line number Diff line number Diff line change
Expand Up @@ -279,7 +279,7 @@ The V18 migration will seed one default local storage bucket:
The V18 migration will add 3 new settings:

```elixir
%{key: "storage_redundancy_copies", value: "2"} # Store files on 2 buckets
%{key: "storage_redundancy_copies", value: "1"} # Store files on 2 buckets
%{key: "storage_auto_generate_variants", value: "true"} # Auto-generate thumbnails/resizes
%{key: "storage_default_bucket_id", value: nil} # No default bucket (use selection algorithm)
```
Expand Down Expand Up @@ -311,7 +311,7 @@ All instances stored **next to original** in same directory:
```

### Redundancy
- Setting: `storage_redundancy_copies` (integer, 1-5, default: 2)
- Setting: `storage_redundancy_copies` (integer, 1-5, default: 1)
- Each file + all variants replicated across N buckets
- Example: redundancy = 2, file stored on 2 different buckets

Expand Down Expand Up @@ -385,14 +385,14 @@ token = :crypto.hash(:md5, data <> secret)
New settings added in V18 migration:

```elixir
storage_redundancy_copies: "2" # How many bucket copies (1-5)
storage_redundancy_copies: "1" # How many bucket copies (1-5)
storage_auto_generate_variants: "true" # Auto-generate thumbnails/resizes
storage_default_bucket_id: nil # Default bucket for uploads (optional)
```

**Access in code:**
```elixir
PhoenixKit.Settings.get_setting("storage_redundancy_copies", "2")
PhoenixKit.Settings.get_setting("storage_redundancy_copies", "1")
```

---
Expand Down
47 changes: 38 additions & 9 deletions lib/phoenix_kit/install/oban_config.ex
Original file line number Diff line number Diff line change
Expand Up @@ -101,8 +101,8 @@ defmodule PhoenixKit.Install.ObanConfig do
Igniter.update_file(igniter, "config/config.exs", fn source ->
content = Rewrite.Source.get(source, :content)

# Check if Oban config already exists
if String.contains?(content, "config :#{app_name}, Oban") do
# Check if Oban config already exists (with more robust detection)
if oban_config_already_exists?(content, app_name) do
source
else
# Find insertion point before import_config statements
Expand All @@ -129,23 +129,52 @@ defmodule PhoenixKit.Install.ObanConfig do
end
end

# Get repo module from PhoenixKit config or use placeholder
defp get_repo_module(_igniter) do
# Get repo module from PhoenixKit config or detect from app
defp get_repo_module(igniter) do
config_path = "config/config.exs"
app_name = IgniterHelpers.get_parent_app_name(igniter)

if File.exists?(config_path) do
content = File.read!(config_path)

# Look for existing PhoenixKit repo config
# First try: Look for existing PhoenixKit repo config
case Regex.run(~r/config :phoenix_kit,\s+repo:\s+([A-Za-z0-9_.]+)/, content) do
[_, repo] -> repo
_ -> "MyApp.Repo"
[_, repo] ->
repo

_ ->
# Second try: Look for ecto_repos in app config
app_module = Macro.camelize(to_string(app_name))

case Regex.run(~r/config :#{app_name}.*?ecto_repos:\s*\[([A-Za-z0-9_.]+)\]/s, content) do
[_, repo] -> repo
_ -> "#{app_module}.Repo"
end
end
else
"MyApp.Repo"
app_module = Macro.camelize(to_string(app_name))
"#{app_module}.Repo"
end
rescue
_ -> "MyApp.Repo"
_ ->
app_name = IgniterHelpers.get_parent_app_name(igniter)
app_module = Macro.camelize(to_string(app_name))
"#{app_module}.Repo"
end

# Check if Oban config already exists in the file
defp oban_config_already_exists?(content, app_name) do
lines = String.split(content, "\n")

Enum.any?(lines, fn line ->
trimmed = String.trim(line)

# Not a comment and contains config for Oban
# Also check for variations with spaces
!String.starts_with?(trimmed, "#") and
(String.contains?(line, "config :#{app_name}, Oban") or
Regex.match?(~r/config\s+:#{app_name},\s+Oban/, line))
end)
end

# Find the location to insert config before import_config statements
Expand Down
4 changes: 2 additions & 2 deletions lib/phoenix_kit/migrations/postgres/v20.ex
Original file line number Diff line number Diff line change
Expand Up @@ -44,7 +44,7 @@ defmodule PhoenixKit.Migrations.Postgres.V20 do

## Settings

- `storage_redundancy_copies`: How many bucket copies (default: 2)
- `storage_redundancy_copies`: How many bucket copies (default: 1)
- `storage_auto_generate_variants`: Auto-generate thumbnails/resizes (default: true)
- `storage_default_bucket_id`: Default bucket for uploads (optional)
"""
Expand Down Expand Up @@ -217,7 +217,7 @@ defmodule PhoenixKit.Migrations.Postgres.V20 do
seed_default_bucket(prefix)

# Add storage settings
insert_setting(prefix, "storage_redundancy_copies", "2")
insert_setting(prefix, "storage_redundancy_copies", "1")
insert_setting(prefix, "storage_auto_generate_variants", "true")
insert_setting(prefix, "storage_default_bucket_id", nil)

Expand Down
17 changes: 17 additions & 0 deletions lib/phoenix_kit/storage.ex
Original file line number Diff line number Diff line change
Expand Up @@ -1462,6 +1462,23 @@ defmodule PhoenixKit.Storage do
end
end

@doc """
Creates file location records for a file instance across specified buckets.

## Parameters

- `file_instance_id` - The file instance UUID
- `bucket_ids` - List of bucket UUIDs where the file is stored
- `file_path` - The storage path of the file

## Returns

- `:ok` - Locations created successfully
"""
def create_file_locations_for_instance(file_instance_id, bucket_ids, file_path) do
create_file_locations(file_instance_id, bucket_ids, file_path)
end

defp generate_temp_path do
temp_dir = System.tmp_dir!()
random_name = :crypto.strong_rand_bytes(8) |> Base.encode16(case: :lower)
Expand Down
2 changes: 1 addition & 1 deletion lib/phoenix_kit/storage/manager.ex
Original file line number Diff line number Diff line change
Expand Up @@ -245,7 +245,7 @@ defmodule PhoenixKit.Storage.Manager do
end

defp get_redundancy_copies do
PhoenixKit.Settings.get_setting("storage_redundancy_copies", "2")
PhoenixKit.Settings.get_setting("storage_redundancy_copies", "1")
|> String.to_integer()
|> max(1)
|> min(5)
Expand Down
12 changes: 10 additions & 2 deletions lib/phoenix_kit/storage/variant_generator.ex
Original file line number Diff line number Diff line change
Expand Up @@ -115,7 +115,7 @@ defmodule PhoenixKit.Storage.VariantGenerator do
{:ok, variant_path} <-
process_variant(original_path, variant_path, file.mime_type, dimension),
{:ok, file_stats} <- get_variant_file_stats(variant_path),
{:ok, _storage_info} <-
{:ok, storage_info} <-
store_variant_file(variant_path, variant_name, variant_storage_path, file.id),
{:ok, instance} <-
create_variant_instance(
Expand All @@ -126,8 +126,16 @@ defmodule PhoenixKit.Storage.VariantGenerator do
variant_ext,
file_stats
) do
# Create file location records for this variant instance
_ =
Storage.create_file_locations_for_instance(
instance.id,
storage_info.bucket_ids,
variant_storage_path
)

cleanup_temp_files([original_path, variant_path])
Logger.info("Variant #{variant_name} created successfully in database")
Logger.info("Variant #{variant_name} created successfully in database with locations")
{:ok, instance}
else
{:error, reason} = error ->
Expand Down
34 changes: 32 additions & 2 deletions lib/phoenix_kit_web/live/settings/storage.ex
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,8 @@ defmodule PhoenixKitWeb.Live.Settings.Storage do

require Logger

import Ecto.Query

alias PhoenixKit.Settings
alias PhoenixKit.System.Dependencies
alias PhoenixKit.Utils.Routes
Expand All @@ -28,8 +30,11 @@ defmodule PhoenixKitWeb.Live.Settings.Storage do
# Load buckets
buckets = PhoenixKit.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", "2")
redundancy_copies = Settings.get_setting("storage_redundancy_copies", "1")
auto_generate_variants = Settings.get_setting("storage_auto_generate_variants", "true")
default_bucket_id = Settings.get_setting("storage_default_bucket_id", nil)

Expand All @@ -54,6 +59,7 @@ defmodule PhoenixKitWeb.Live.Settings.Storage do
|> assign(:page_title, "Storage 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(:default_bucket_id, default_bucket_id)
Expand Down Expand Up @@ -188,7 +194,7 @@ defmodule PhoenixKitWeb.Live.Settings.Storage do
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", "2")
saved_redundancy = Settings.get_setting("storage_redundancy_copies", "1")
saved_variants = Settings.get_setting("storage_auto_generate_variants", "true")

socket =
Expand Down Expand Up @@ -305,4 +311,28 @@ defmodule PhoenixKitWeb.Live.Settings.Storage do
"#{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 PhoenixKit.Storage.File,
join: fi in PhoenixKit.Storage.FileInstance,
on: fi.file_id == f.id,
join: fl in PhoenixKit.Storage.FileLocation,
on: fl.file_instance_id == fi.id,
where: fl.bucket_id == ^bucket.id and fl.status == "active",
select: count(f.id, :distinct)
)

Map.put(acc, bucket.id, count || 0)
end)
rescue
_ -> %{}
end
end
15 changes: 5 additions & 10 deletions lib/phoenix_kit_web/live/settings/storage.html.heex
Original file line number Diff line number Diff line change
Expand Up @@ -131,7 +131,7 @@
<th>Name</th>
<th>Provider</th>
<th>Priority</th>
<th>Usage</th>
<th>Files</th>
<th>Status</th>
<th class="text-right">Actions</th>
</tr>
Expand Down Expand Up @@ -179,10 +179,10 @@
<% end %>
</td>

<%!-- Usage --%>
<%!-- Files Count --%>
<td>
<span class="text-sm">
Calculating...
<span class="text-sm font-semibold">
{Map.get(@bucket_file_counts, bucket.id, 0)} files
</span>
</td>

Expand Down Expand Up @@ -344,15 +344,10 @@
<span class="label-text ml-2">
<span class="font-semibold">Auto-Generate Variants</span>
<span class="block text-xs opacity-70">
Automatically create different sizes when files are uploaded
Automatically resize images and videos to configured dimensions
</span>
</span>
</label>
<label class="label">
<span class="label-text-alt">
When enabled, images and videos will automatically be resized to configured dimensions
</span>
</label>
</div>
</.form>

Expand Down
26 changes: 16 additions & 10 deletions lib/phoenix_kit_web/live/users/media.html.heex
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,15 @@
<%= if @show_upload do %>
<div class="card bg-base-100 shadow-xl mb-8 animate-in fade-in duration-200">
<div class="card-body">
<h2 class="card-title text-xl mb-4">Upload Media</h2>
<div class="flex items-center gap-3 mb-4">
<h2 class="card-title text-xl">Upload Media</h2>
<button
phx-click="toggle_upload"
class="btn btn-sm btn-error gap-2"
>
<.icon name="hero-x-mark" class="w-4 h-4" /> Cancel
</button>
</div>
<%= if @has_buckets do %>
<p class="text-sm text-base-content/70 mb-4">
Upload images, videos, or PDFs. Files are automatically processed and optimized.
Expand Down Expand Up @@ -57,16 +65,14 @@
<div class="flex justify-between items-center mb-4">
<div class="flex items-center gap-3">
<h2 class="card-title text-xl">Uploaded Files ({@total_count})</h2>
<button
phx-click="toggle_upload"
class={["btn btn-sm gap-2", (@show_upload && "btn-error") || "btn-primary"]}
>
<%= if @show_upload do %>
<.icon name="hero-x-mark" class="w-4 h-4" /> Cancel
<% else %>
<%= unless @show_upload do %>
<button
phx-click="toggle_upload"
class="btn btn-sm btn-primary gap-2"
>
<.icon name="hero-plus" class="w-4 h-4" /> Add Media
<% end %>
</button>
</button>
<% end %>
</div>
<.pagination_info
page={@current_page}
Expand Down
Loading