You need to enable JavaScript to run this app.
最新活动
大模型
产品
解决方案
定价
生态与合作
支持与服务
开发者
了解我们

如何在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

火山引擎 最新活动