Blob 存储

在您的 Nuxt 应用程序中上传、存储和提供图像、视频、音乐、文档和其他非结构化数据。

入门

通过在 nuxt.config.ts 文件中将 blob 属性添加到 hub 对象中,在您的 NuxtHub 项目中启用 Blob 存储。

nuxt.config.ts
export default defineNuxtConfig({
  hub: {
    blob: true
  }
})
此选项将在开发中使用 Cloudflare 平台代理,并在您 部署它 时自动为您的项目创建一个 Cloudflare R2 存储桶。
Nuxt DevTools Blob
NuxtHub Admin Blob

hubBlob()

返回一组用于操作 Blob 存储的方法的服务器可组合函数。

list()

返回 Blob 的分页列表(仅元数据)。

server/api/files.get.ts
export default eventHandler(async () => {
  const { blobs } = await hubBlob().list({ limit: 10 })

  return blobs
})

参数

options
对象

列表选项。

返回值

返回 BlobListResult

serve()

返回 Blob 的数据并设置 Content-TypeContent-LengthETag 标头。

export default eventHandler(async (event) => {
  const { pathname } = getRouterParams(event)

  return hubBlob().serve(event, pathname)
})
为了防止 XSS 攻击,请确保控制您提供的 Blob 的内容类型。

您还可以设置 Content-Security-Policy 标头以添加额外的安全层。

server/api/images/[...pathname].get.ts
export default eventHandler(async (event) => {
  const { pathname } = getRouterParams(event)

  setHeader(event, 'Content-Security-Policy', 'default-src \'none\';')
  return hubBlob().serve(event, pathname)
})

参数

event
H3Event

处理程序的事件,需要设置标头。

pathname
字符串

要提供的 Blob 的名称。

返回值

返回 Blob 的原始数据并设置 Content-TypeContent-Length 标头。

返回 Blob 的元数据。

const metadata = await hubBlob().head(pathname)

参数

pathname
字符串

要提供的 Blob 的名称。

返回值

返回 BlobObject

get()

返回 Blob 主体。

const blob = await hubBlob().get(pathname)

参数

pathname
字符串

要提供的 Blob 的名称。

返回值

返回 Blob 或如果未找到则返回 null

put()

将 Blob 上传到存储。

server/api/files.post.ts
export default eventHandler(async (event) => {
  const form = await readFormData(event)
  const file = form.get('file') as File

  if (!file || !file.size) {
    throw createError({ statusCode: 400, message: 'No file provided' })
  }

  ensureBlob(file, {
    maxSize: '1MB',
    types: ['image']
  })

  return hubBlob().put(file.name, file, {
    addRandomSuffix: false,
    prefix: 'images'
  })
})

查看 Vue 端的示例

pages/upload.vue
<script setup lang="ts">
async function uploadImage (e: Event) {
  const form = e.target as HTMLFormElement

  await $fetch('/api/files', {
    method: 'POST',
    body: new FormData(form)
  }).catch((err) => alert('Failed to upload image:\n'+ err.data?.message))

  form.reset()
}
</script>

<template>
  <form @submit.prevent="uploadImage">
    <label>Upload an image: <input type="file" name="image"></label>
    <button type="submit">
      Upload
    </button>
  </form>
</template>

参数

pathname
字符串

要提供的 Blob 的名称。

body
字符串 | ReadableStream<any> | ArrayBuffer | ArrayBufferView | Blob

Blob 的数据。

options
对象

放置选项。任何其他提供的字段都将存储在 Blob 的元数据中。

返回值

返回 BlobObject

del()

使用其路径名删除 Blob。

server/api/files/[...pathname].delete.ts
export default eventHandler(async (event) => {
  const { pathname } = getRouterParams(event)

  await hubBlob().del(pathname)

  return sendNoContent(event)
})

您还可以通过提供路径名数组来一次删除多个 Blob。

await hubBlob().del(['images/1.jpg', 'images/2.jpg'])
您也可以使用 delete() 方法作为 del() 的别名。

参数

pathname
字符串

要提供的 Blob 的名称。

返回值

不返回值。

handleUpload()

这是一个“一体化”功能,用于通过检查其大小和类型来验证 Blob 并将其上传到存储。

此服务器实用程序旨在与 useUpload() Vue 可组合函数一起使用。

它可以用于处理 API 路由中的文件上传。

export default eventHandler(async (event) => {
  return hubBlob().handleUpload(event, {
    formKey: 'files', // read file or files form the `formKey` field of request body (body should be a `FormData` object)
    multiple: true, // when `true`, the `formKey` field will be an array of `Blob` objects
    ensure: {
      contentType: ['image/jpeg', 'images/png'], // allowed types of the file
    },
    put: {
      addRandomSuffix: true
    }
  })
})

参数

formKey
字符串

要从中读取文件的表单键。默认值为 'files'

multiple
布尔值

当为 true 时,formKey 字段将是一个 Blob 对象数组。

ensure
BlobEnsureOptions

有关更多详细信息,请参阅 ensureBlob() 选项。

put
BlobPutOptions

有关更多详细信息,请参阅 put() 选项。

返回值

返回 BlobObject 或如果 multipletrue 则返回 BlobObject 数组。

如果 file 不满足要求,则抛出错误。

handleMultipartUpload()

处理请求以支持分段上传。

server/api/files/multipart/[action]/[...pathname].ts
export default eventHandler(async (event) => {
  return await hubBlob().handleMultipartUpload(event)
})
确保您的路由包含 [action][...pathname] 参数。

在客户端,您可以使用 useMultipartUpload() 可组合函数以分段上传文件。

<script setup lang="ts">
async function uploadFile(file: File) {
  const upload = useMultipartUpload('/api/files/multipart')

  const { progress, completed, abort } = upload(file)
}
</script>
有关用法详细信息,请参阅 useMultipartUpload()

参数

contentType
字符串

Blob 的内容类型。

contentLength
字符串

Blob 的内容长度。

addRandomSuffix
布尔值

如果为 true,则将向 Blob 的名称添加随机后缀。默认为 false

createMultipartUpload()

我们建议使用 handleMultipartUpload() 方法来处理分段上传请求。

启动新的分段上传。

server/api/files/multipart/[...pathname].post.ts
export default eventHandler(async (event) => {
  const { pathname } = getRouterParams(event)

  const mpu = await hubBlob().createMultipartUpload(pathname)

  return {
    uploadId: mpu.uploadId,
    pathname: mpu.pathname,
  }
})

参数

pathname
字符串

要提供的 Blob 的名称。

options
对象

放置选项。任何其他提供的字段都将存储在 Blob 的元数据中。

返回值

返回 BlobMultipartUpload

resumeMultipartUpload()

我们建议使用 handleMultipartUpload() 方法来处理分段上传请求。

继续处理未完成的分段上传。

要上传分段上传的一部分,您可以使用 uploadPart() 方法

server/api/files/multipart/[...pathname].put.ts
export default eventHandler(async (event) => {
  const { pathname } = getRouterParams(event)
  const { uploadId, partNumber } = getQuery(event)

  const stream = getRequestWebStream(event)!
  const body = await streamToArrayBuffer(stream, contentLength)

  const mpu = hubBlob().resumeMultipartUpload(pathname, uploadId)
  return await mpu.uploadPart(partNumber, body)
})

通过调用 complete() 方法完成上传

server/api/files/multipart/complete.post.ts
export default eventHandler(async (event) => {
  const { pathname, uploadId } = getQuery(event)
  const parts = await readBody(event)

  const mpu = hubBlob().resumeMultipartUpload(pathname, uploadId)
  return await mpu.complete(parts)
})

如果您要取消上传,则需要调用 abort() 方法

server/api/files/multipart/[...pathname].delete.ts
export default eventHandler(async (event) => {
  const { pathname } = getRouterParams(event)
  const { uploadId } = getQuery(event)

  const mpu = hubBlob().resumeMultipartUpload(pathname, uploadId)
  await mpu.abort()

  return sendNoContent(event)
})

客户端使用上述路由进行分段上传的简单示例

utils/multipart-upload.ts
async function uploadLargeFile(file: File) {
  const chunkSize = 10 * 1024 * 1024 // 10MB

  const count = Math.ceil(file.size / chunkSize)
  const { pathname, uploadId } = await $fetch(
    `/api/files/multipart/${file.name}`,
    { method: 'POST' },
  )

  const uploaded = []

  for (let i = 0; i < count; i++) {
    const start = i * chunkSize
    const end = Math.min(start + chunkSize, file.size)
    const partNumber = i + 1
    const chunk = file.slice(start, end)

    const part = await $fetch(
      `/api/files/multipart/${pathname}`,
      {
        method: 'PUT',
        query: { uploadId, partNumber },
        body: chunk,
      },
    )

    uploaded.push(part)
  }

  return await $fetch(
    '/api/files/multipart/complete',
    {
      method: 'POST',
      query: { pathname, uploadId },
      body: { parts: uploaded },
    },
  )
}

参数

pathname
字符串

要提供的 Blob 的名称。

uploadId
字符串

分段上传的上传 ID。

返回值

返回 BlobMultipartUpload

参数

event必填
H3Event

要处理的事件。

createCredentials()

创建临时访问凭据,这些凭据可以选择性地限定为前缀或对象。

用于从客户端创建预签名 URL 以将文件上传到 R2 (查看示例)。

此方法仅在生产环境中或在带有 --remote 标志的开发环境中可用。
// Create credentials with default permission & scope (admin-read-write)
const credentials = await hubBlob().createCredentials()

// Limit the scope to a specific object & permission
const credentials = await hubBlob().createCredentials({
  permission: 'object-read-write',
  pathnames: ['only-this-file.png']
})

详细了解 创建预签名 URL 以将文件上传到 R2

参数

options
对象

创建凭据的选项。

返回值

返回包含以下属性的对象

{
  accountId: string
  bucketName: string
  accessKeyId: string
  secretAccessKey: string
  sessionToken: string
}

ensureBlob()

ensureBlob() 是一个方便的实用程序,用于通过检查其大小和类型来验证 Blob

// Will throw an error if the file is not an image or is larger than 1MB
ensureBlob(file, { maxSize: '1MB', types: ['image' ]})

参数

file必填
Blob

要验证的文件。

options必填
对象

请注意,至少应提供 maxSizetypes

返回值

不返回值。

如果 file 不满足要求,则抛出错误。

Vue 可组合函数

以下可组合函数旨在用于应用程序的 Vue 端(不是 server/ 目录)。

useUpload()

useUpload 用于处理 Nuxt 应用程序中的文件上传。

<script setup lang="ts">
const upload = useUpload('/api/blob', { method: 'PUT' })

async function onFileSelect({ target }: Event) {
  const uploadedFiles = await upload(target as HTMLInputElement)

  // file uploaded successfully
}
</script>

<template>
  <input
    accept="jpeg, png"
    type="file"
    name="file"
    multiple
    @change="onFileSelect"
  >
</template>

参数

apiBase必填
字符串

上传 API 的基本 URL。

options必填
对象

可选地,您可以将 Fetch 选项传递给请求。详细了解 Fetch API 此处

返回值

返回一个 MultipartUpload 函数,该函数可用于以分段上传文件。

const { completed, progress, abort } = upload(file)
const data = await completed

useMultipartUpload()

应用程序可组合函数,用于创建分段上传助手。

utils/multipart-upload.ts
export const mpu = useMultipartUpload('/api/files/multipart')

参数

baseURL
字符串

handleMultipartUpload() 处理的分段上传 API 的基本 URL。

options

分段上传助手的选项。

返回值

返回一个 MultipartUpload 函数,该函数可用于以分段上传文件。

const { completed, progress, abort } = mpu(file)
const data = await completed

类型

BlobObject

interface BlobObject {
  pathname: string
  contentType: string | undefined
  size: number
  httpEtag: string
  uploadedAt: Date
  httpMetadata: Record<string, string>
  customMetadata: Record<string, string>
}

BlobMultipartUpload

export interface BlobMultipartUpload {
  pathname: string
  uploadId: string
  uploadPart(
    partNumber: number,
    value: string | ReadableStream<any> | ArrayBuffer | ArrayBufferView | Blob
  ): Promise<BlobUploadedPart>
  abort(): Promise<void>
  complete(uploadedParts: BlobUploadedPart[]): Promise<BlobObject>
}

BlobUploadedPart

export interface BlobUploadedPart {
  partNumber: number;
  etag: string;
}

MultipartUploader

export type MultipartUploader = (file: File) => {
  completed: Promise<SerializeObject<BlobObject> | undefined>
  progress: Readonly<Ref<number>>
  abort: () => Promise<void>
}

BlobListResult

interface BlobListResult {
  blobs: BlobObject[]
  hasMore: boolean
  cursor?: string
  folders?: string[]
}

示例

使用分页列出 Blob

export default eventHandler(async (event) => {
  const { limit, cursor } = await getQuery(event)

  return hubBlob().list({
    limit: limit ? Number.parseInt(limit) : 10,
    cursor: cursor ? cursor : undefined
  })
})

创建预签名 URL 以将文件上传到 R2

预签名 URL 可用于从客户端将文件上传到 R2,而无需使用 API 密钥。

NuxtHub presigned URLs to upload files to R2

由于我们使用 aws4fetch 来签名请求,并使用 zod 来验证请求,因此我们需要安装这些包

终端
npx nypm i aws4fetch zod

首先,我们需要创建一个 API 路由,它将向客户端返回一个预签名的 URL。

server/api/blob/sign/[...pathname].get.ts
import { z } from 'zod'
import { AwsClient } from 'aws4fetch'

export default eventHandler(async (event) => {
  const { pathname } = await getValidatedRouterParams(event, z.object({
    pathname: z.string().min(1)
  }).parse)
  // Create credentials with the right permission & scope
  const blob = hubBlob()
  const { accountId, bucketName, ...credentials } = await blob.createCredentials({
    permission: 'object-read-write',
    pathnames: [pathname]
  })
  // Create the presigned URL
  const client = new AwsClient(credentials)
  const endpoint = new URL(
    pathname,
    `https://${bucketName}.${accountId}.r2.cloudflarestorage.com`
  )
  const { url } = await client.sign(endpoint, {
    method: 'PUT',
    aws: { signQuery: true }
  })
  // Return the presigned URL to the client
  return { url }
})
确保在服务器端对请求进行身份验证,以防止任何人将文件上传到您的 R2 存储桶。查看 nuxt-auth-utils 作为可能的解决方案之一。

接下来,我们需要创建一个 Vue 页面,使用预签名的 URL 将文件上传到我们的 R2 存储桶。

pages/upload.vue
<script setup lang="ts">
async function uploadWithPresignedUrl(file: File) {
  const { url } = await $fetch(`/api/blob/sign/${file.name}`)
  await $fetch(url, {
    method: 'PUT',
    body: file
  })
}
</script>

<template>
  <input type="file" @change="uploadWithPresignedUrl($event.target.files[0])">
</template>

在此阶段,您将收到 CORS 错误,因为我们没有在 R2 存储桶上设置 CORS。

要在我们的 R2 存储桶上设置 CORS

  • 使用 npx nuxthub manage 在 NuxtHub 管理员中打开项目
  • 转到 **Blob 选项卡**(确保您位于正确的环境:生产或预览)
  • 点击右上角的 Cloudflare 图标
  • 在 Cloudflare 中,转到 R2 存储桶的 设置 选项卡
  • 滚动到 **CORS 策略**
  • 点击 编辑 CORS 策略
  • 按照以下示例,使用您的来源更新允许的来源
[
  {
    "AllowedOrigins": [
      "https://127.0.0.1:3000",
      "https://my-app.nuxt.dev"
    ],
    "AllowedMethods": [
      "GET",
      "PUT"
    ],
    "AllowedHeaders": [
      "*"
    ]
  }
]
  • 保存更改

就是这样!您现在可以使用预签名的 URL 将文件上传到 R2。

阅读更多关于 Cloudflare 的预签名的 URL 的信息 官方文档