Skip to content

Commit c2d0171

Browse files
authored
feat: implement multipart storage on top of unstorage drivers (#656)
1 parent 77d0254 commit c2d0171

File tree

17 files changed

+1850
-941
lines changed

17 files changed

+1850
-941
lines changed

docs/content/docs/2.features/blob.md

Lines changed: 128 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -390,7 +390,7 @@ Returns a [`BlobObject`](#blobobject) or an array of [`BlobObject`](#blobobject)
390390

391391
Throws an error if `file` doesn't meet the requirements.
392392

393-
<!-- ### `handleMultipartUpload()`
393+
### `handleMultipartUpload()`
394394

395395
Handle the request to support multipart upload.
396396

@@ -437,7 +437,10 @@ See [`useMultipartUpload()`](#usemultipartupload) on usage details.
437437
### `createMultipartUpload()`
438438

439439
::note
440-
We suggest to use [`handleMultipartUpload()`](#handlemultipartupload) method to handle the multipart upload request.
440+
We suggest using [`handleMultipartUpload()`](#handlemultipartupload) method to handle the multipart upload request.
441+
:br
442+
:br
443+
If you want to handle multipart uploads manually using this utility, keep in mind that you cannot use this utility for Vercel Blob due to payload size limit of Vercel functions. Consider using [Vercel Blob Client SDK](https://vercel.com/docs/vercel-blob/client-upload).
441444
::
442445

443446
Start a new multipart upload.
@@ -484,7 +487,7 @@ Returns a `BlobMultipartUpload`
484487
### `resumeMultipartUpload()`
485488

486489
::note
487-
We suggest to use [`handleMultipartUpload()`](#handlemultipartupload) method to handle the multipart upload request.
490+
We suggest using [`handleMultipartUpload()`](#handlemultipartupload) method to handle the multipart upload request.
488491
::
489492

490493
Continue processing of unfinished multipart upload.
@@ -595,7 +598,7 @@ Returns a `BlobMultipartUpload`
595598
::field{name="event" type="H3Event" required}
596599
The event to handle.
597600
::
598-
:: -->
601+
::
599602

600603
## `ensureBlob()`
601604

@@ -697,6 +700,10 @@ const data = await completed
697700

698701
Application composable that creates a multipart upload helper.
699702

703+
::important
704+
When you configure to use Vercel Blob, this utility will automatically use [Vercel Blob Client SDK](https://vercel.com/docs/vercel-blob/client-upload) to upload the file.
705+
::
706+
700707
```ts [utils/multipart-upload.ts]
701708
export const mpu = useMultipartUpload('/api/files/multipart')
702709
```
@@ -739,6 +746,122 @@ const { completed, progress, abort } = mpu(file)
739746
const data = await completed
740747
```
741748

749+
## Storage Providers
750+
751+
NuxtHub supports multiple storage providers for blob storage. In development mode, NuxtHub automatically configures the filesystem (`fs`) driver for local development.
752+
753+
### Filesystem (fs)
754+
755+
The filesystem driver stores blobs locally on your development machine.
756+
757+
```ts [nuxt.config.ts]
758+
export default defineNuxtConfig({
759+
nitro: {
760+
storage: {
761+
BLOB: {
762+
driver: 'fs',
763+
base: './.data/blob'
764+
}
765+
}
766+
}
767+
})
768+
```
769+
770+
### Vercel Blob
771+
772+
For production deployments on Vercel, use the Vercel Blob driver.
773+
774+
```ts [nuxt.config.ts]
775+
export default defineNuxtConfig({
776+
nitro: {
777+
storage: {
778+
BLOB: {
779+
driver: 'vercel-blob',
780+
access: 'public'
781+
}
782+
}
783+
}
784+
})
785+
```
786+
787+
### Cloudflare R2
788+
789+
For Cloudflare deployments, you can use Cloudflare R2 with either bindings (recommended) or the S3-compatible driver.
790+
791+
#### Using R2 Bindings (Recommended)
792+
793+
When deploying to Cloudflare Workers, use R2 bindings for optimal performance and integration.
794+
795+
```ts [nuxt.config.ts]
796+
export default defineNuxtConfig({
797+
nitro: {
798+
storage: {
799+
BLOB: {
800+
driver: 'cloudflare-r2',
801+
binding: 'BLOB'
802+
}
803+
}
804+
}
805+
})
806+
```
807+
808+
Make sure to configure the R2 binding in your `wrangler.toml`:
809+
810+
```toml [wrangler.toml]
811+
[[r2_buckets]]
812+
binding = "BLOB"
813+
bucket_name = "my-bucket"
814+
```
815+
816+
#### Using S3-Compatible Driver
817+
818+
Alternatively, you can use the S3-compatible driver with Cloudflare R2. This is useful for deploying your project in different environments while still using Cloudflare R2.
819+
820+
```ts [nuxt.config.ts]
821+
export default defineNuxtConfig({
822+
nitro: {
823+
storage: {
824+
BLOB: {
825+
driver: 's3',
826+
accessKeyId: process.env.CLOUDFLARE_R2_ACCESS_KEY_ID,
827+
secretAccessKey: process.env.CLOUDFLARE_R2_SECRET_ACCESS_KEY,
828+
region: 'auto',
829+
endpoint: `https://${process.env.CLOUDFLARE_ACCOUNT_ID}.r2.cloudflarestorage.com`,
830+
bucket: process.env.CLOUDFLARE_R2_BUCKET_NAME
831+
}
832+
}
833+
}
834+
})
835+
```
836+
837+
### Amazon S3
838+
839+
For AWS S3 storage, use the S3 driver.
840+
841+
```ts [nuxt.config.ts]
842+
export default defineNuxtConfig({
843+
nitro: {
844+
storage: {
845+
BLOB: {
846+
driver: 's3',
847+
accessKeyId: process.env.AWS_ACCESS_KEY_ID,
848+
secretAccessKey: process.env.AWS_SECRET_ACCESS_KEY,
849+
region: process.env.AWS_REGION,
850+
bucket: process.env.AWS_S3_BUCKET
851+
}
852+
}
853+
}
854+
})
855+
```
856+
857+
::callout{to="https://unstorage.unjs.io/drivers"}
858+
For additional storage providers and configuration options, see the unstorage documentation.
859+
::
860+
861+
::note
862+
Other unstorage drivers do not support multipart upload. If you want to upload large files, consider using one of the above providers.
863+
::
864+
742865
## Types
743866

744867
### `BlobObject`
@@ -752,6 +875,7 @@ interface BlobObject {
752875
uploadedAt: Date
753876
httpMetadata: Record<string, string>
754877
customMetadata: Record<string, string>
878+
url: string | undefined
755879
}
756880
```
757881

package.json

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -54,6 +54,8 @@
5454
"@cloudflare/workers-types": "^4.20250913.0",
5555
"@nuxt/devtools-kit": "^2.6.3",
5656
"@uploadthing/mime-types": "^0.3.6",
57+
"@vercel/blob": "^1.1.1",
58+
"aws4fetch": "^1.0.20",
5759
"db0": "^0.3.2",
5860
"defu": "^6.1.4",
5961
"destr": "^2.0.5",

playground/app/pages/blob.vue

Lines changed: 5 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -40,7 +40,7 @@ async function loadMore() {
4040
4141
async function addFile() {
4242
if (!newFilesValue.value.length) {
43-
toast.add({ title: 'Missing files.', color: 'red' })
43+
toast.add({ title: 'Missing files.', color: 'error' })
4444
return
4545
}
4646
loading.value = true
@@ -54,7 +54,7 @@ async function addFile() {
5454
newFilesValue.value = []
5555
} catch (err: any) {
5656
const title = err.data?.data?.issues?.map((issue: any) => issue.message).join('\n') || err.message
57-
toast.add({ title, color: 'red' })
57+
toast.add({ title, color: 'error' })
5858
}
5959
loading.value = false
6060
}
@@ -76,17 +76,13 @@ async function uploadFiles(files: File[]) {
7676
})(smallFiles)
7777
}
7878
79-
// TODO: multipart upload
8079
// upload big files
8180
const uploadLarge = useMultipartUpload('/api/blob/multipart', {
8281
concurrent: 2,
8382
prefix: String(prefix.value || '')
8483
})
8584
8685
for (const file of bigFiles) {
87-
toast.add({ title: 'Multipart upload is not supported yet.', color: 'warning' })
88-
continue
89-
9086
const { completed, progress, abort } = uploadLarge(file)
9187
9288
const uploadingToast = toast.add({
@@ -118,7 +114,7 @@ async function uploadFiles(files: File[]) {
118114
} else {
119115
toast.add({
120116
title: `Failed to upload ${file.name}.`,
121-
color: 'red'
117+
color: 'error'
122118
})
123119
}
124120
}
@@ -148,7 +144,7 @@ async function deleteFile(pathname: string) {
148144
toast.add({ title: `File "${pathname}" deleted.` })
149145
} catch (err: any) {
150146
const title = err.data?.data?.issues?.map((issue: any) => issue.message).join('\n') || err.message
151-
toast.add({ title, color: 'red' })
147+
toast.add({ title, color: 'error' })
152148
}
153149
}
154150
</script>
@@ -163,7 +159,7 @@ async function deleteFile(pathname: string) {
163159
disabled
164160
class="flex-1"
165161
autocomplete="off"
166-
:ui="{ wrapper: 'flex-1' }"
162+
:ui="{ root: 'flex-1' }"
167163
/>
168164
<input
169165
ref="uploadRef"

playground/server/api/cached.ts

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
const test = defineCachedFunction((_event) => {
2+
return 'test'
3+
}, {
4+
getKey: () => 'test'
5+
})
6+
7+
export default cachedEventHandler(async (event) => {
8+
return {
9+
now: Date.now(),
10+
test: test(event)
11+
}
12+
}, {
13+
maxAge: 10,
14+
swr: true
15+
})

0 commit comments

Comments
 (0)