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.)
|
# Ignore all user-generated content (uploads, QR codes, etc.)
|
||||||
/priv/static/user_generated/
|
/priv/static/user_generated/
|
||||||
|
/uploads/
|
||||||
|
|
||||||
# Ignore customized Docker Compose file.
|
# Ignore customized Docker Compose file.
|
||||||
docker-compose.yml
|
docker-compose.yml
|
||||||
|
|||||||
@@ -88,6 +88,10 @@ ENV LC_ALL en_US.UTF-8
|
|||||||
WORKDIR "/app"
|
WORKDIR "/app"
|
||||||
RUN chown nobody /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
|
# set runner ENV
|
||||||
ENV MIX_ENV="prod"
|
ENV MIX_ENV="prod"
|
||||||
|
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import Config
|
import Config
|
||||||
|
|
||||||
# Configure your database
|
# Configure the database
|
||||||
config :components_elixir, ComponentsElixir.Repo,
|
config :components_elixir, ComponentsElixir.Repo,
|
||||||
username: "postgres",
|
username: "postgres",
|
||||||
password: "fCnPB8VQdPkhJAD29hq6sZEY",
|
password: "fCnPB8VQdPkhJAD29hq6sZEY",
|
||||||
@@ -10,8 +10,12 @@ config :components_elixir, ComponentsElixir.Repo,
|
|||||||
show_sensitive_data_on_connection_error: true,
|
show_sensitive_data_on_connection_error: true,
|
||||||
pool_size: 10
|
pool_size: 10
|
||||||
|
|
||||||
# For development, we disable any cache and enable
|
# For development work, log all queries
|
||||||
# debugging and code reloading.
|
# 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
|
# The watchers configuration can be used to run external
|
||||||
# watchers to your application. For example, we can use it
|
# watchers to your application. For example, we can use it
|
||||||
|
|||||||
@@ -1,5 +1,9 @@
|
|||||||
import Config
|
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
|
# config/runtime.exs is executed for all environments, including
|
||||||
# during releases. It is executed after compilation and before the
|
# during releases. It is executed after compilation and before the
|
||||||
# system starts, so it is typically used to load production configuration
|
# system starts, so it is typically used to load production configuration
|
||||||
|
|||||||
@@ -25,9 +25,12 @@ services:
|
|||||||
PHX_HOST: "localhost"
|
PHX_HOST: "localhost"
|
||||||
PHX_SERVER: "true"
|
PHX_SERVER: "true"
|
||||||
PORT: "4000"
|
PORT: "4000"
|
||||||
|
UPLOADS_DIR: "/data/uploads"
|
||||||
depends_on:
|
depends_on:
|
||||||
db:
|
db:
|
||||||
condition: service_healthy
|
condition: service_healthy
|
||||||
|
volumes:
|
||||||
|
- uploaded_files:/data/uploads
|
||||||
command:
|
command:
|
||||||
[
|
[
|
||||||
"/bin/sh",
|
"/bin/sh",
|
||||||
@@ -37,3 +40,4 @@ services:
|
|||||||
|
|
||||||
volumes:
|
volumes:
|
||||||
postgres_data:
|
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 %>
|
<%= if @editing_component && @editing_component.image_filename do %>
|
||||||
<div class="mt-1 mb-2">
|
<div class="mt-1 mb-2">
|
||||||
<p class="text-sm text-base-content/70">Current image:</p>
|
<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>
|
</div>
|
||||||
<% end %>
|
<% end %>
|
||||||
<div class="mt-1">
|
<div class="mt-1">
|
||||||
@@ -680,8 +680,8 @@ defmodule ComponentsElixirWeb.ComponentsLive do
|
|||||||
<!-- Component Image -->
|
<!-- Component Image -->
|
||||||
<div class="flex-shrink-0 mr-4">
|
<div class="flex-shrink-0 mr-4">
|
||||||
<%= if component.image_filename do %>
|
<%= 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">
|
<button phx-click="show_image" phx-value-url={"/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" />
|
<img src={"/uploads/images/#{component.image_filename}"} alt={component.name} class="h-12 w-12 rounded-lg object-cover cursor-pointer" />
|
||||||
</button>
|
</button>
|
||||||
<% else %>
|
<% else %>
|
||||||
<div class="h-12 w-12 rounded-lg bg-base-200 flex items-center justify-center">
|
<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
|
# Helper functions for image upload handling
|
||||||
defp save_uploaded_image(socket, component_params) do
|
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 =
|
uploaded_files =
|
||||||
consume_uploaded_entries(socket, :image, fn %{path: path}, entry ->
|
consume_uploaded_entries(socket, :image, fn %{path: path}, entry ->
|
||||||
filename = "#{System.unique_integer([:positive])}_#{entry.client_name}"
|
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
|
# Ensure the upload directory exists
|
||||||
File.mkdir_p!(Path.dirname(dest))
|
File.mkdir_p!(upload_dir)
|
||||||
|
|
||||||
# Copy the file
|
# Copy the file
|
||||||
case File.cp(path, dest) do
|
case File.cp(path, dest) do
|
||||||
:ok -> filename
|
:ok ->
|
||||||
{:error, _reason} -> nil
|
IO.puts("=== DEBUG: File copy successful ===")
|
||||||
|
{:ok, filename}
|
||||||
|
{:error, reason} ->
|
||||||
|
IO.puts("=== DEBUG: File copy failed: #{inspect(reason)} ===")
|
||||||
|
{:postpone, {:error, reason}}
|
||||||
end
|
end
|
||||||
end)
|
end)
|
||||||
|
|
||||||
case uploaded_files do
|
IO.inspect(uploaded_files, label: "Uploaded files result")
|
||||||
[filename] when is_binary(filename) -> Map.put(component_params, "image_filename", filename)
|
|
||||||
[] -> component_params
|
result = case uploaded_files do
|
||||||
_error -> component_params
|
[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
|
end
|
||||||
|
|
||||||
|
IO.inspect(result, label: "Final component_params")
|
||||||
|
IO.puts("=== DEBUG: End save_uploaded_image ===")
|
||||||
|
result
|
||||||
end
|
end
|
||||||
|
|
||||||
defp delete_image_file(nil), do: :ok
|
defp delete_image_file(nil), do: :ok
|
||||||
defp delete_image_file(""), do: :ok
|
defp delete_image_file(""), do: :ok
|
||||||
|
|
||||||
defp delete_image_file(filename) do
|
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)
|
File.rm(path)
|
||||||
end
|
end
|
||||||
|
|
||||||
|
|||||||
@@ -24,6 +24,9 @@ defmodule ComponentsElixirWeb.Router do
|
|||||||
live "/login", LoginLive, :index
|
live "/login", LoginLive, :index
|
||||||
get "/login/authenticate", AuthController, :authenticate
|
get "/login/authenticate", AuthController, :authenticate
|
||||||
post "/logout", AuthController, :logout
|
post "/logout", AuthController, :logout
|
||||||
|
|
||||||
|
# File serving endpoint
|
||||||
|
get "/uploads/images/:filename", FileController, :show
|
||||||
end
|
end
|
||||||
|
|
||||||
scope "/", ComponentsElixirWeb do
|
scope "/", ComponentsElixirWeb do
|
||||||
|
|||||||
Reference in New Issue
Block a user