檢視原始程式碼 外部上傳

此指南從伺服器中的組態繼續,請參閱 上傳指南

上傳至外部雲端供應商,例如亞馬遜 S3、Google Cloud 等,可以使用 allow_upload/3 中的 :external 選項。

你提供一個 2 元函數,讓伺服器為每個上傳項目產生元資料,該函數會傳給用戶端指定的 JavaScript 函數。

通常當函數被呼叫時,你會產生簽名前置網址,特定於你的雲端儲存供應商,該網址會提供臨時存取權限,讓最終使用者可以直接將資料上傳到你的雲端儲存。

切割 HTTP 上傳

對於任何使用 Content-Range 標頭透過切割 HTTP 要求來支援大型檔案上傳的服務,你可以使用 Mux 的 UpChunk JS 函式庫來完成上傳檔案的所有艱辛工作。要上傳小檔案或快速入門,請改為考慮 直接上傳至 S3

你只需要將 UpChunk 執行個體連接到 LiveView UploadEntry 回呼函式,LiveView 就會處理其餘的部分。

透過將 UpChunk 的內容儲存到 assets/vendor/upchunk.js 或使用 npm 安裝來安裝 UpChunk

$ 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

:external 選項提供給 Phoenix.LiveView.allow_upload/3。它需要一個 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。對於較大的檔案上傳,請考慮使用如上所示的分塊方式。

本指南假設已設定現有的 S3 區塊貯存空間,並擁有正確的 CORS 組態,允許將檔案直接上傳至區塊貯存空間。

一個 CORS 組態範例如下:

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

您也可以將您的網域放入「allowedOrigins」中。關於如何設定 S3 區塊貯存空間 CORS 的更多資訊,請 瀏覽 AWS

為了在將檔案上傳至 S3 時套用所有檔案限制,您必須使用檔案資料執行 multipart 表單 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 結果,其中包含元資料給客戶端,以及我們未變更的 socket。

接下來,我們新增一個遺失的模組 SimpleS3Upload 來產生 S3 的預先簽章 URL。建立一個名為 simple_s3_upload.ex 的檔案。從這個名為 SimpleS3Upload 的無相依性模組取得檔案內容,它是由 Chris McCord 編寫的。

提示:如果您在 :crypto 模組或 S3 阻擋 ACL 中遇到錯誤,請閱讀上述摘要中的評論以取得解決方案。

接下來,我們新增我們的 JavaScript 客户端上傳程式。元資料必須包含 :uploader 金鑰,並指定 JavaScript 客户端上傳程式的名稱。在本例中,如上所示,名稱為 "S3"

app.js 旁的 assets/js/ 下的目錄中,新增一個檔案 uploaders.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」將會與客戶端的「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)
  })
}