如何在Phoenix中实现已上传文件的浏览器下载功能
嘿,我之前用 Arc 和 Arc.Ecto 做过类似的本地文件下载功能,给你一步步拆解实现方案,应该能直接落地:
步骤1:调整Arc存储配置,指定独立的下载文件目录
首先你需要确保上传的文件被存储到一个独立的、专门用于下载的文件夹里(如果已经上传到其他目录,也可以后续在下载时复制到目标目录,但更推荐一开始就配置好)。
打开你的 Arc 存储模块(比如 lib/your_app/arc/storage.ex),修改 storage_dir 函数,指定独立的下载目录:
defmodule YourApp.Arc.Storage do use Arc.Storage.Local @impl true def storage_dir(_version, {_file, _scope}) do # 这里设置独立的下载文件夹,比如项目根目录下的 priv/downloads "priv/downloads" end end
如果想全局配置,也可以在 config/config.exs 里直接写:
config :arc, storage: Arc.Storage.Local, storage_dir: "priv/downloads"
这样所有上传的文件都会自动存到 priv/downloads 这个独立文件夹,方便后续处理下载。
步骤2:添加下载路由
在你的 Phoenix 路由文件(lib/your_app_web/router.ex)里添加一个 GET 路由,用于处理文件下载请求:
scope "/", YourAppWeb do pipe_through :browser # 如果需要登录验证,改成 pipe_through [:browser, :authenticate_user] # 这里的 :filename 是你要下载的文件名,也可以根据业务改成带资源ID的路径,比如 /downloads/:id get "/downloads/:filename", DownloadController, :show end
步骤3:实现下载控制器动作
创建一个 DownloadController(或者在现有控制器里新增动作),编写下载逻辑:
defmodule YourAppWeb.DownloadController do use YourAppWeb, :controller def show(conn, %{"filename" => filename}) do # 构建文件的完整路径 file_root = Application.app_dir(:your_app, "priv/downloads") file_path = Path.join(file_root, filename) # 安全校验:防止路径遍历攻击,确保文件确实在指定的下载目录内 if Path.absname(file_path) |> String.starts_with?(Path.absname(file_root)) do # 设置响应头,让浏览器触发下载弹窗而非直接打开文件 conn |> put_resp_content_type(Plug.MIME.path(file_path)) |> put_resp_header("content-disposition", "attachment; filename=\"#{filename}\"") |> send_file(200, file_path) else conn |> put_status(:not_found) |> text("File not found") end end end
这里的核心是 send_file/3 函数,它会直接将文件内容发送给客户端;加上 content-disposition 头可以强制浏览器弹出下载对话框,而不是直接打开文件。
步骤4:在前端添加下载按钮
现在你可以在前端模板里添加下载按钮,指向刚才配置的路由:
<!-- 假设你有一个附件记录,包含 filename 字段 --> <a href="/downloads/<%= @attachment.filename %>" class="btn btn-primary"> 下载文件 </a>
如果是批量文件列表,循环渲染即可:
<%= for attachment <- @attachments do %> <div class="attachment-item"> <%= attachment.filename %> <a href="/downloads/<%= attachment.filename %>" download>下载</a> </div> <% end %>
额外注意事项
- 权限控制:如果只有特定用户能下载特定文件,建议不要直接用文件名作为路由参数,而是通过资源ID关联到用户,在控制器里校验当前用户的访问权限,避免恶意用户猜测文件名下载他人文件。
- 文件重名处理:Arc 默认会给重名文件添加随机后缀,所以要确保从数据库读取的是 Arc 生成的完整文件名(比如你的附件表的
file字段存储的是 Arc 返回的最终文件名)。 - 大文件优化:如果需要下载大文件,可以考虑用
send_chunked/2分块发送,避免占用过多内存。
这样一套流程下来,用户点击下载按钮就能触发浏览器的下载动作,而且所有供下载的文件都存储在独立的文件夹里,完全符合你的需求。
内容的提问来源于stack exchange,提问作者Badops




