diff --git a/docs/content/docs/2.storage/3.blob.md b/docs/content/docs/2.storage/3.blob.md
index 8b226610..7f234fcf 100644
--- a/docs/content/docs/2.storage/3.blob.md
+++ b/docs/content/docs/2.storage/3.blob.md
@@ -131,6 +131,32 @@ export default eventHandler(async (event) => {
})
```
+See an example on the Vue side:
+
+```vue [pages/upload.vue]
+
+
+
+
+
+```
+
#### Params
::field-group
@@ -248,6 +274,212 @@ Returns a [`BlobObject`](#blobobject) or an array of [`BlobObject`](#blobobject)
Throws an error if `file` doesn't meet the requirements.
+### `handleMultipartUpload()`
+
+Handle the request to support multipart upload.
+
+```ts [server/api/files/multipart/[action\\]/[...pathname\\].ts]
+export default eventHandler(async (event) => {
+ return await hubBlob().handleMultipartUpload(event)
+})
+```
+
+::important
+Make sure your route includes `[action]` and `[...pathname]` params.
+::
+
+On the client side, you can use the `useMultipartUpload()` composable to upload a file in parts.
+
+```vue
+
+```
+
+::note{to="#usemultipartupload"}
+See [`useMultipartUpload()`](#usemultipartupload) on usage details.
+::
+
+#### Params
+
+::field-group
+ ::field{name="contentType" type="string"}
+ The content type of the blob.
+ ::
+ ::field{name="contentLength" type="string"}
+ The content length of the blob.
+ ::
+ ::field{name="addRandomSuffix" type="boolean"}
+ If `true`, a random suffix will be added to the blob's name. Defaults to `false`.
+ ::
+::
+
+### `createMultipartUpload()`
+
+::note
+We suggest to use [`handleMultipartUpload()`](#handlemultipartupload) method to handle the multipart upload request.
+::
+
+Start a new multipart upload.
+
+```ts [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,
+ }
+})
+```
+
+#### Params
+
+::field-group
+ ::field{name="pathname" type="String"}
+ The name of the blob to serve.
+ ::
+ ::field{name="options" type="Object"}
+ The put options. Any other provided field will be stored in the blob's metadata.
+ ::field{name="contentType" type="String"}
+ The content type of the blob. If not given, it will be inferred from the Blob or the file extension.
+ ::
+ ::field{name="contentLength" type="String"}
+ The content length of the blob.
+ ::
+ ::field{name="addRandomSuffix" type="Boolean"}
+ If `true`, a random suffix will be added to the blob's name. Defaults to `true`.
+ ::
+ ::
+::
+
+#### Return
+
+Returns a `BlobMultipartUpload`
+
+### `resumeMultipartUpload()`
+
+::note
+We suggest to use [`handleMultipartUpload()`](#handlemultipartupload) method to handle the multipart upload request.
+::
+
+Continue processing of unfinished multipart upload.
+
+To upload a part of the multipart upload, you can use the `uploadPart()` method:
+
+```ts [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 the upload by calling `complete()` method:
+
+```ts [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)
+})
+```
+
+If you want to cancel the upload, you need to call `abort()` method:
+
+```ts [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)
+})
+```
+
+A simple example of multipart upload in client with above routes:
+
+```ts [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 },
+ },
+ )
+}
+```
+
+#### Params
+
+::field-group
+ ::field{name="pathname" type="String"}
+ The name of the blob to serve.
+ ::
+ ::field{name="uploadId" type="String"}
+ The upload ID of the multipart upload.
+ ::
+::
+
+#### Return
+
+Returns a `BlobMultipartUpload`
+
+
+#### Params
+
+::field-group
+ ::field{name="event" type="H3Event" required}
+ The event to handle.
+ ::
+::
+
+
## `ensureBlob()`
`ensureBlob()` is a handy util to validate a `Blob` by checking its size and type:
@@ -284,6 +516,10 @@ Throws an error if `file` doesn't meet the requirements.
## Composables
+::note
+The following composables are meant to be used in the Vue side of your application (not the `server/` directory).
+::
+
### `useUpload()`
`useUpload` is to handle file uploads in your Nuxt application.
@@ -327,6 +563,59 @@ async function onFileSelect({ target }: Event) {
::
::
+#### Return
+
+Return a `MultipartUpload` function that can be used to upload a file in parts.
+
+```ts
+const { completed, progress, abort } = upload(file)
+const data = await completed
+```
+
+### `useMultipartUpload()`
+
+Application composable that creates a multipart upload helper.
+
+```ts [utils/multipart-upload.ts]
+export const mpu = useMultipartUpload('/api/files/multipart')
+```
+
+#### Params
+
+::field-group
+ ::field{name="baseURL" type="string"}
+ The base URL of the multipart upload API handled by [`handleMultipartUpload()`](#handlemultipartupload).
+ ::
+ ::field{name="options"}
+ The options for the multipart upload helper.
+ ::field{name="partSize" type="number"}
+ The size of each part of the file to be uploaded. Defaults to `10MB`.
+ ::
+ ::field{name="concurrent" type="number"}
+ The maximum number of concurrent uploads. Defaults to `1`.
+ ::
+ ::field{name="maxRetry" type="number"}
+ The maximum number of retry attempts for the whole upload. Defaults to `3`.
+ ::
+ ::field{name="prefix" type="string"}
+ The prefix to use for the blob pathname.
+ ::
+ ::field{name="fetchOptions" type="Omit"}
+ Override the ofetch options.
+ The `query` and `headers` will be merged with the options provided by the uploader.
+ ::
+ ::
+::
+
+#### Return
+
+Return a `MultipartUpload` function that can be used to upload a file in parts.
+
+```ts
+const { completed, progress, abort } = mpu(file)
+const data = await completed
+```
+
## Types
### `BlobObject`
@@ -340,6 +629,40 @@ interface BlobObject {
}
```
+### `BlobMultipartUpload`
+
+```ts
+export interface BlobMultipartUpload {
+ pathname: string
+ uploadId: string
+ uploadPart(
+ partNumber: number,
+ value: string | ReadableStream | ArrayBuffer | ArrayBufferView | Blob
+ ): Promise
+ abort(): Promise
+ complete(uploadedParts: BlobUploadedPart[]): Promise
+}
+```
+
+### `BlobUploadedPart`
+
+```ts
+export interface BlobUploadedPart {
+ partNumber: number;
+ etag: string;
+}
+```
+
+### `MultipartUploader`
+
+```ts
+export type MultipartUploader = (file: File) => {
+ completed: Promise | undefined>
+ progress: Readonly[>
+ abort: () => Promise
+}
+```
+
### `BlobListResult`
```ts
@@ -351,7 +674,6 @@ interface BlobListResult {
}
```
-
## Examples
### List blobs with pagination
diff --git a/playground/pages/blob.vue b/playground/pages/blob.vue
index 5e284146..840425f6 100644
--- a/playground/pages/blob.vue
+++ b/playground/pages/blob.vue
@@ -1,5 +1,6 @@
]