查看源代码 外部上传

本指南延续了服务器 上传指南 中开始的配置。

上传到外部云提供商(如 Amazon S3、Google Cloud 等)可以通过在 allow_upload/3 中使用 :external 选项来实现。

您提供一个 2 元函数来允许服务器为每个上传条目生成元数据,该元数据传递给客户端上用户指定的 JavaScript 函数。

通常,当您的函数被调用时,您将生成一个预签名 URL,该 URL 特定于您的云存储提供商,它将提供临时访问权限,以便最终用户直接将数据上传到您的云存储。

分块 HTTP 上传

对于任何支持通过具有 Content-Range 标头的分块 HTTP 请求上传大文件的服务,您可以使用 Mux 的 UpChunk JS 库来完成上传文件的所有繁重工作。对于小文件上传或快速入门,请考虑 直接上传到 S3

您只需要将 UpChunk 实例连接到 LiveView UploadEntry 回调,LiveView 将处理其余部分。

安装 UpChunk,方法是将 其内容 保存到 assets/vendor/upchunk.js,或使用 npm 安装。

$ npm install --prefix assets --save @mux/upchunk

Phoenix.LiveView.mount/3 上配置您的上传程序

def mount(_params, _session, socket) do
  {:ok,
   socket
   |> assign(:uploaded_files, [])
   |> allow_upload(:avatar, accept: :any, max_entries: 3, external: &presign_upload/2)}
end

Phoenix.LiveView.allow_upload/3 提供 :external 选项。它需要一个 2 元函数,该函数生成一个签名 URL,客户端将在该 URL 上推送上传条目的字节。此函数必须返回 {:ok, meta, socket}{:error, meta, socket},其中 meta 必须是映射。

例如,如果您使用提供 start_session 函数的上下文,您可能会编写类似以下内容

defp presign_upload(entry, socket) do
  {:ok, %{"Location" => link}} =
    SomeTube.start_session(%{
      "uploadType" => "resumable",
      "x-upload-content-length" => entry.client_size
    })

  {:ok, %{uploader: "UpChunk", entrypoint: link}, socket}
end

最后,在客户端,我们使用 UpChunk 从服务器生成的临时 URL 创建一个上传,并将它的事件侦听器附加到条目的回调

import * as UpChunk from "@mux/upchunk"

let Uploaders = {}

Uploaders.UpChunk = function(entries, onViewError){
  entries.forEach(entry => {
    // create the upload session with UpChunk
    let { file, meta: { entrypoint } } = entry
    let upload = UpChunk.createUpload({ endpoint: entrypoint, file })

    // stop uploading in the event of a view error
    onViewError(() => upload.pause())

    // upload error triggers LiveView error
    upload.on("error", (e) => entry.error(e.detail.message))

    // notify progress events to LiveView
    upload.on("progress", (e) => {
      if(e.detail < 100){ entry.progress(e.detail) }
    })

    // success completes the UploadEntry
    upload.on("success", () => entry.progress(100))
  })
}

// Don't forget to assign Uploaders to the liveSocket
let liveSocket = new LiveSocket("/live", Socket, {
  uploaders: Uploaders,
  params: {_csrf_token: csrfToken}
})

直接到 S3

根据 S3 常见问题解答,可以上传到 S3 的最大单个 PUT 对象为 5 GB。对于更大的文件上传,请考虑使用上面显示的分块。

本指南假设已设置具有正确 CORS 配置的现有 S3 存储桶,该配置允许直接上传到存储桶。

CORS 配置示例为

[
    {
        "AllowedHeaders": [ "*" ],
        "AllowedMethods": [ "PUT", "POST" ],
        "AllowedOrigins": [ "*" ],
        "ExposeHeaders": []
    }
]

您可以将您的域名放在“allowedOrigins”中。有关为 S3 存储桶配置 CORS 的更多信息,请参阅 AWS 上的信息

为了在上传到 S3 时强制执行所有文件约束,有必要执行具有文件数据的 multipart form POST。在继续之前,您应该准备以下 S3 信息

  1. aws_access_key_id
  2. aws_secret_access_key
  3. bucket_name
  4. region

我们首先实现 LiveView 部分

def mount(_params, _session, socket) do
  {:ok,
    socket
    |> assign(:uploaded_files, [])
    |> allow_upload(:avatar, accept: :any, max_entries: 3, external: &presign_upload/2)}
end

defp presign_upload(entry, socket) do
  uploads = socket.assigns.uploads
  bucket = "phx-upload-example"
  key = "public/#{entry.client_name}"

  config = %{
    region: "us-east-1",
    access_key_id: System.fetch_env!("AWS_ACCESS_KEY_ID"),
    secret_access_key: System.fetch_env!("AWS_SECRET_ACCESS_KEY")
  }

  {:ok, fields} =
    SimpleS3Upload.sign_form_upload(config, bucket,
      key: key,
      content_type: entry.client_type,
      max_file_size: uploads[entry.upload_config].max_file_size,
      expires_in: :timer.hours(1)
    )

  meta = %{uploader: "S3", key: key, url: "http://#{bucket}.s3-#{config.region}.amazonaws.com", fields: fields}
  {:ok, meta, socket}
end

在这里,我们实现了一个 presign_upload/2 函数,我们将其作为捕获的匿名函数传递给 :external。它为上传生成一个预签名 URL,并返回我们的 :ok 结果,以及用于客户端的元数据有效负载,以及我们未更改的套接字。

接下来,我们将添加一个缺失的模块 SimpleS3Upload 来为 S3 生成预签名 URL。创建一个名为 simple_s3_upload.ex 的文件。从 Chris McCord 编写的这个名为 SimpleS3Upload 的零依赖模块中获取文件内容。

提示:如果您遇到 :crypto 模块或 S3 阻止 ACL 的错误,请阅读上面 gist 中的注释以获取解决方案。

接下来,我们添加我们的 JavaScript 客户端上传程序。元数据 *必须* 包含 :uploader 键,指定 JavaScript 客户端上传程序的名称。在本例中,它是 "S3",如上所示。

在以下目录 assets/js/ 中添加一个新文件 uploaders.js,该目录位于 app.js 旁边。此 S3 客户端上传程序的内容

let Uploaders = {}

Uploaders.S3 = function(entries, onViewError){
  entries.forEach(entry => {
    let formData = new FormData()
    let {url, fields} = entry.meta
    Object.entries(fields).forEach(([key, val]) => formData.append(key, val))
    formData.append("file", entry.file)
    let xhr = new XMLHttpRequest()
    onViewError(() => xhr.abort())
    xhr.onload = () => xhr.status === 204 ? entry.progress(100) : entry.error()
    xhr.onerror = () => entry.error()
    xhr.upload.addEventListener("progress", (event) => {
      if(event.lengthComputable){
        let percent = Math.round((event.loaded / event.total) * 100)
        if(percent < 100){ entry.progress(percent) }
      }
    })

    xhr.open("POST", url, true)
    xhr.send(formData)
  })
}

export default Uploaders;

我们定义一个 Uploaders.S3 函数,该函数接收我们的条目。然后它对每个条目执行 AJAX 请求,使用 entry.progress()entry.error() 函数将上传事件报告回 LiveView。上传程序的名称必须与我们在 LiveView 中的 :uploader 元数据中返回的名称匹配。

最后,转到 app.js,并将 uploaders: Uploaders 键添加到 LiveSocket 构造函数中,以告诉 phoenix 在哪里找到外部元数据中返回的上传程序。

// for uploading to S3
import Uploaders from "./uploaders"

let liveSocket = new LiveSocket("/live",
   Socket, {
     params: {_csrf_token: csrfToken},
     uploaders: Uploaders
  }
)

现在,从服务器返回的“S3”将与客户端中的匹配。要调试尝试上传时的客户端 JavaScript,您可以检查您的浏览器并查看控制台或网络选项卡以查看错误日志。

直接到 S3 兼容

本节假设您已在项目中正确安装和配置了 ExAwsExAws.S3,并且可以在页面中执行示例而不会出现错误。

大多数与 S3 兼容的平台(如 Cloudflare R2)在上传文件时不支持 POST,因此我们需要使用 PUT 以及签名 URL 而不是签名 POST,并将文件直接发送到服务。为此,我们需要更改 presign_upload/2 函数以及执行上传的 Uploaders.S3

新的 presign_upload/2

def presign_upload(entry, socket) do
  config = ExAws.Config.new(:s3)
  bucket = "bucket"
  key = "public/#{entry.client_name}"

  {:ok, url} =
    ExAws.S3.presigned_url(config, :put, bucket, key,
      expires_in: 3600,
      query_params: [{"Content-Type", entry.client_type}]
    )
   {:ok, %{uploader: "S3", key: key, url: url}, socket}
end

新的 Uploaders.S3

Uploaders.S3 = function (entries, onViewError) {
  entries.forEach(entry => {
    let xhr = new XMLHttpRequest()
    onViewError(() => xhr.abort())
    xhr.onload = () => xhr.status === 200 ? entry.progress(100) : entry.error()
    xhr.onerror = () => entry.error()

    xhr.upload.addEventListener("progress", (event) => {
      if(event.lengthComputable){
        let percent = Math.round((event.loaded / event.total) * 100)
        if(percent < 100){ entry.progress(percent) }
      }
    })

    let url = entry.meta.url
    xhr.open("PUT", url, true)
    xhr.send(entry.file)
  })
}