fix(elixir): file upload in production
- move uploads to new directory (`./uploads` per default)
This commit is contained in:
1
.gitignore
vendored
1
.gitignore
vendored
@@ -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
|
||||
|
||||
@@ -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"
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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:
|
||||
|
||||
40
lib/components_elixir_web/controllers/file_controller.ex
Normal file
40
lib/components_elixir_web/controllers/file_controller.ex
Normal 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
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user