fix(elixir): file upload in production

- move uploads to new directory (`./uploads` per default)
This commit is contained in:
Schuwi
2025-09-16 23:16:02 +02:00
parent fa9bf74fd9
commit 76b0a97d31
8 changed files with 103 additions and 15 deletions

1
.gitignore vendored
View File

@@ -37,6 +37,7 @@ npm-debug.log
# Ignore all user-generated content (uploads, QR codes, etc.)
/priv/static/user_generated/
/uploads/
# Ignore customized Docker Compose file.
docker-compose.yml

View File

@@ -88,6 +88,10 @@ ENV LC_ALL en_US.UTF-8
WORKDIR "/app"
RUN chown nobody /app
# Create data directory for uploads
RUN mkdir -p /data/uploads/images && \
chown -R nobody:root /data/uploads
# set runner ENV
ENV MIX_ENV="prod"

View File

@@ -1,6 +1,6 @@
import Config
# Configure your database
# Configure the database
config :components_elixir, ComponentsElixir.Repo,
username: "postgres",
password: "fCnPB8VQdPkhJAD29hq6sZEY",
@@ -10,8 +10,12 @@ config :components_elixir, ComponentsElixir.Repo,
show_sensitive_data_on_connection_error: true,
pool_size: 10
# For development, we disable any cache and enable
# debugging and code reloading.
# For development work, log all queries
# config :components_elixir, ComponentsElixir.Repo, log: false
# For development, use a local uploads directory
config :components_elixir,
uploads_dir: "./uploads"
#
# The watchers configuration can be used to run external
# watchers to your application. For example, we can use it

View File

@@ -1,5 +1,9 @@
import Config
# Runtime configuration for uploads directory
config :components_elixir,
uploads_dir: System.get_env("UPLOADS_DIR", "./uploads")
# config/runtime.exs is executed for all environments, including
# during releases. It is executed after compilation and before the
# system starts, so it is typically used to load production configuration

View File

@@ -25,9 +25,12 @@ services:
PHX_HOST: "localhost"
PHX_SERVER: "true"
PORT: "4000"
UPLOADS_DIR: "/data/uploads"
depends_on:
db:
condition: service_healthy
volumes:
- uploaded_files:/data/uploads
command:
[
"/bin/sh",
@@ -37,3 +40,4 @@ services:
volumes:
postgres_data:
uploaded_files:

View File

@@ -0,0 +1,40 @@
defmodule ComponentsElixirWeb.FileController do
use ComponentsElixirWeb, :controller
def show(conn, %{"filename" => filename}) do
# Security: only allow alphanumeric, dashes, underscores, and dots
if String.match?(filename, ~r/^[a-zA-Z0-9_\-\.]+$/) do
uploads_dir = Application.get_env(:components_elixir, :uploads_dir)
file_path = Path.join([uploads_dir, "images", filename])
if File.exists?(file_path) do
# Get the file's MIME type
mime_type = get_mime_type(filename)
conn
|> put_resp_content_type(mime_type)
|> put_resp_header("cache-control", "public, max-age=86400") # Cache for 1 day
|> send_file(200, file_path)
else
conn
|> put_status(:not_found)
|> text("File not found")
end
else
conn
|> put_status(:bad_request)
|> text("Invalid filename")
end
end
defp get_mime_type(filename) do
case Path.extname(filename) |> String.downcase() do
".jpg" -> "image/jpeg"
".jpeg" -> "image/jpeg"
".png" -> "image/png"
".gif" -> "image/gif"
".webp" -> "image/webp"
_ -> "application/octet-stream"
end
end
end

View File

@@ -614,7 +614,7 @@ defmodule ComponentsElixirWeb.ComponentsLive do
<%= if @editing_component && @editing_component.image_filename do %>
<div class="mt-1 mb-2">
<p class="text-sm text-base-content/70">Current image:</p>
<img src={"/user_generated/uploads/images/#{@editing_component.image_filename}"} alt="Current component" class="h-20 w-20 object-cover rounded-lg" />
<img src={"/uploads/images/#{@editing_component.image_filename}"} alt="Current component" class="h-20 w-20 object-cover rounded-lg" />
</div>
<% end %>
<div class="mt-1">
@@ -680,8 +680,8 @@ defmodule ComponentsElixirWeb.ComponentsLive do
<!-- Component Image -->
<div class="flex-shrink-0 mr-4">
<%= if component.image_filename do %>
<button phx-click="show_image" phx-value-url={"/user_generated/uploads/images/#{component.image_filename}"} class="hover:opacity-75 transition-opacity">
<img src={"/user_generated/uploads/images/#{component.image_filename}"} alt={component.name} class="h-12 w-12 rounded-lg object-cover cursor-pointer" />
<button phx-click="show_image" phx-value-url={"/uploads/images/#{component.image_filename}"} class="hover:opacity-75 transition-opacity">
<img src={"/uploads/images/#{component.image_filename}"} alt={component.name} class="h-12 w-12 rounded-lg object-cover cursor-pointer" />
</button>
<% else %>
<div class="h-12 w-12 rounded-lg bg-base-200 flex items-center justify-center">
@@ -867,33 +867,61 @@ defmodule ComponentsElixirWeb.ComponentsLive do
# Helper functions for image upload handling
defp save_uploaded_image(socket, component_params) do
IO.puts("=== DEBUG: Starting save_uploaded_image ===")
IO.inspect(socket.assigns.uploads.image.entries, label: "Upload entries")
uploaded_files =
consume_uploaded_entries(socket, :image, fn %{path: path}, entry ->
filename = "#{System.unique_integer([:positive])}_#{entry.client_name}"
dest = Path.join(["priv", "static", "user_generated", "uploads", "images", filename])
uploads_dir = Application.get_env(:components_elixir, :uploads_dir)
upload_dir = Path.join([uploads_dir, "images"])
dest = Path.join(upload_dir, filename)
IO.puts("=== DEBUG: Processing upload ===")
IO.puts("Filename: #{filename}")
IO.puts("Upload dir: #{upload_dir}")
IO.puts("Destination: #{dest}")
# Ensure the upload directory exists
File.mkdir_p!(Path.dirname(dest))
File.mkdir_p!(upload_dir)
# Copy the file
case File.cp(path, dest) do
:ok -> filename
{:error, _reason} -> nil
:ok ->
IO.puts("=== DEBUG: File copy successful ===")
{:ok, filename}
{:error, reason} ->
IO.puts("=== DEBUG: File copy failed: #{inspect(reason)} ===")
{:postpone, {:error, reason}}
end
end)
case uploaded_files do
[filename] when is_binary(filename) -> Map.put(component_params, "image_filename", filename)
[] -> component_params
_error -> component_params
IO.inspect(uploaded_files, label: "Uploaded files result")
result = case uploaded_files do
[filename] when is_binary(filename) ->
IO.puts("=== DEBUG: Adding filename to params: #{filename} ===")
Map.put(component_params, "image_filename", filename)
[] ->
IO.puts("=== DEBUG: No files uploaded ===")
component_params
_error ->
IO.puts("=== DEBUG: Upload error ===")
IO.inspect(uploaded_files, label: "Unexpected upload result")
component_params
end
IO.inspect(result, label: "Final component_params")
IO.puts("=== DEBUG: End save_uploaded_image ===")
result
end
defp delete_image_file(nil), do: :ok
defp delete_image_file(""), do: :ok
defp delete_image_file(filename) do
path = Path.join(["priv", "static", "user_generated", "uploads", "images", filename])
uploads_dir = Application.get_env(:components_elixir, :uploads_dir)
path = Path.join([uploads_dir, "images", filename])
File.rm(path)
end

View File

@@ -24,6 +24,9 @@ defmodule ComponentsElixirWeb.Router do
live "/login", LoginLive, :index
get "/login/authenticate", AuthController, :authenticate
post "/logout", AuthController, :logout
# File serving endpoint
get "/uploads/images/:filename", FileController, :show
end
scope "/", ComponentsElixirWeb do