查看源代码 外部上传
本指南延续了服务器 上传指南 中开始的配置。
上传到外部云提供商(如 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 信息
- aws_access_key_id
- aws_secret_access_key
- bucket_name
- 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 兼容
大多数与 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)
})
}