From 30e83793e3e9272162989ec92ee901715454beef Mon Sep 17 00:00:00 2001 From: Farnabaz Date: Wed, 3 Sep 2025 16:53:27 +0200 Subject: [PATCH 01/15] feat: implement multipart storage on top of unstorage drivers --- package.json | 1 + playground/app/pages/blob.vue | 4 - pnpm-lock.yaml | 273 ++++++++++--- .../app/composables/useMultipartUpload.ts | 2 +- .../blob/server/helpers/blob-cloudflare.ts | 59 +++ src/runtime/blob/server/helpers/blob-fs.ts | 131 +++++++ .../blob/server/helpers/blob-vercel.ts | 78 ++++ .../blob/server/helpers/multipart-storage.ts | 64 ++++ src/runtime/blob/server/utils/blob.ts | 361 +++++++++--------- 9 files changed, 734 insertions(+), 239 deletions(-) create mode 100644 src/runtime/blob/server/helpers/blob-cloudflare.ts create mode 100644 src/runtime/blob/server/helpers/blob-fs.ts create mode 100644 src/runtime/blob/server/helpers/blob-vercel.ts create mode 100644 src/runtime/blob/server/helpers/multipart-storage.ts diff --git a/package.json b/package.json index 4f6fc6d9..0497370c 100644 --- a/package.json +++ b/package.json @@ -53,6 +53,7 @@ "@cloudflare/workers-types": "^4.20250810.0", "@nuxt/devtools-kit": "^2.4.0", "@uploadthing/mime-types": "^0.3.5", + "@vercel/blob": "^1.1.1", "confbox": "^0.2.2", "db0": "^0.3.2", "defu": "^6.1.4", diff --git a/playground/app/pages/blob.vue b/playground/app/pages/blob.vue index 0943378a..6a5c52b3 100644 --- a/playground/app/pages/blob.vue +++ b/playground/app/pages/blob.vue @@ -76,7 +76,6 @@ async function uploadFiles(files: File[]) { })(smallFiles) } - // TODO: multipart upload // upload big files const uploadLarge = useMultipartUpload('/api/blob/multipart', { concurrent: 2, @@ -84,9 +83,6 @@ async function uploadFiles(files: File[]) { }) for (const file of bigFiles) { - toast.add({ title: 'Multipart upload is not supported yet.', color: 'warning' }) - continue - const { completed, progress, abort } = uploadLarge(file) const uploadingToast = toast.add({ diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 1fc967ef..2ebbf577 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -17,6 +17,9 @@ importers: '@uploadthing/mime-types': specifier: ^0.3.5 version: 0.3.5 + '@vercel/blob': + specifier: ^1.1.1 + version: 1.1.1 confbox: specifier: ^0.2.2 version: 0.2.2 @@ -55,7 +58,7 @@ importers: version: 0.1.3 unstorage: specifier: ^1.16.1 - version: 1.16.1(@netlify/blobs@9.1.2)(aws4fetch@1.0.20)(db0@0.3.2(@electric-sql/pglite@0.3.7)(better-sqlite3@12.2.0)(drizzle-orm@0.44.4(@cloudflare/workers-types@4.20250812.0)(@electric-sql/pglite@0.3.7)(@opentelemetry/api@1.9.0)(better-sqlite3@12.2.0)(postgres@3.4.7)))(ioredis@5.7.0) + version: 1.16.1(@netlify/blobs@9.1.2)(@vercel/blob@1.1.1)(aws4fetch@1.0.20)(db0@0.3.2(@electric-sql/pglite@0.3.7)(better-sqlite3@12.2.0)(drizzle-orm@0.44.4(@cloudflare/workers-types@4.20250812.0)(@electric-sql/pglite@0.3.7)(@opentelemetry/api@1.9.0)(better-sqlite3@12.2.0)(postgres@3.4.7)))(ioredis@5.7.0) zod: specifier: ^3.25.34 version: 3.25.76 @@ -92,7 +95,7 @@ importers: version: 9.33.0(jiti@2.5.1) nuxt: specifier: ^4.0.3 - version: 4.0.3(@electric-sql/pglite@0.3.7)(@netlify/blobs@9.1.2)(@parcel/watcher@2.5.1)(@types/node@22.17.1)(@vue/compiler-sfc@3.5.18)(aws4fetch@1.0.20)(better-sqlite3@12.2.0)(db0@0.3.2(@electric-sql/pglite@0.3.7)(better-sqlite3@12.2.0)(drizzle-orm@0.44.4(@cloudflare/workers-types@4.20250812.0)(@electric-sql/pglite@0.3.7)(@opentelemetry/api@1.9.0)(better-sqlite3@12.2.0)(postgres@3.4.7)))(drizzle-orm@0.44.4(@cloudflare/workers-types@4.20250812.0)(@electric-sql/pglite@0.3.7)(@opentelemetry/api@1.9.0)(better-sqlite3@12.2.0)(postgres@3.4.7))(eslint@9.33.0(jiti@2.5.1))(ioredis@5.7.0)(lightningcss@1.30.1)(magicast@0.3.5)(optionator@0.9.4)(rollup@4.46.2)(terser@5.43.1)(typescript@5.9.2)(vite@7.1.2(@types/node@22.17.1)(jiti@2.5.1)(lightningcss@1.30.1)(terser@5.43.1)(yaml@2.8.1))(vue-tsc@3.0.5(typescript@5.9.2))(yaml@2.8.1) + version: 4.0.3(@electric-sql/pglite@0.3.7)(@netlify/blobs@9.1.2)(@parcel/watcher@2.5.1)(@types/node@22.17.1)(@vercel/blob@1.1.1)(@vue/compiler-sfc@3.5.18)(aws4fetch@1.0.20)(better-sqlite3@12.2.0)(db0@0.3.2(@electric-sql/pglite@0.3.7)(better-sqlite3@12.2.0)(drizzle-orm@0.44.4(@cloudflare/workers-types@4.20250812.0)(@electric-sql/pglite@0.3.7)(@opentelemetry/api@1.9.0)(better-sqlite3@12.2.0)(postgres@3.4.7)))(drizzle-orm@0.44.4(@cloudflare/workers-types@4.20250812.0)(@electric-sql/pglite@0.3.7)(@opentelemetry/api@1.9.0)(better-sqlite3@12.2.0)(postgres@3.4.7))(eslint@9.33.0(jiti@2.5.1))(ioredis@5.7.0)(lightningcss@1.30.1)(magicast@0.3.5)(optionator@0.9.4)(rollup@4.46.2)(terser@5.43.1)(typescript@5.9.2)(vite@7.1.2(@types/node@22.17.1)(jiti@2.5.1)(lightningcss@1.30.1)(terser@5.43.1)(yaml@2.8.1))(vue-tsc@3.0.5(typescript@5.9.2))(yaml@2.8.1) typescript: specifier: ~5.9.2 version: 5.9.2 @@ -131,13 +134,13 @@ importers: version: 3.6.3(@electric-sql/pglite@0.3.7)(better-sqlite3@12.2.0)(drizzle-orm@0.44.4(@cloudflare/workers-types@4.20250812.0)(@electric-sql/pglite@0.3.7)(@opentelemetry/api@1.9.0)(better-sqlite3@12.2.0)(postgres@3.4.7))(magicast@0.3.5)(vue-component-type-helpers@3.0.5) '@nuxt/image': specifier: ^1.10.0 - version: 1.11.0(@netlify/blobs@9.1.2)(aws4fetch@1.0.20)(db0@0.3.2(@electric-sql/pglite@0.3.7)(better-sqlite3@12.2.0)(drizzle-orm@0.44.4(@cloudflare/workers-types@4.20250812.0)(@electric-sql/pglite@0.3.7)(@opentelemetry/api@1.9.0)(better-sqlite3@12.2.0)(postgres@3.4.7)))(ioredis@5.7.0)(magicast@0.3.5) + version: 1.11.0(@netlify/blobs@9.1.2)(@vercel/blob@1.1.1)(aws4fetch@1.0.20)(db0@0.3.2(@electric-sql/pglite@0.3.7)(better-sqlite3@12.2.0)(drizzle-orm@0.44.4(@cloudflare/workers-types@4.20250812.0)(@electric-sql/pglite@0.3.7)(@opentelemetry/api@1.9.0)(better-sqlite3@12.2.0)(postgres@3.4.7)))(ioredis@5.7.0)(magicast@0.3.5) '@nuxt/scripts': specifier: ^0.11.6 - version: 0.11.10(@netlify/blobs@9.1.2)(@unhead/vue@2.0.14(vue@3.5.18(typescript@5.9.2)))(aws4fetch@1.0.20)(db0@0.3.2(@electric-sql/pglite@0.3.7)(better-sqlite3@12.2.0)(drizzle-orm@0.44.4(@cloudflare/workers-types@4.20250812.0)(@electric-sql/pglite@0.3.7)(@opentelemetry/api@1.9.0)(better-sqlite3@12.2.0)(postgres@3.4.7)))(ioredis@5.7.0)(magicast@0.3.5)(typescript@5.9.2)(vue@3.5.18(typescript@5.9.2)) + version: 0.11.10(@netlify/blobs@9.1.2)(@unhead/vue@2.0.14(vue@3.5.18(typescript@5.9.2)))(@vercel/blob@1.1.1)(aws4fetch@1.0.20)(db0@0.3.2(@electric-sql/pglite@0.3.7)(better-sqlite3@12.2.0)(drizzle-orm@0.44.4(@cloudflare/workers-types@4.20250812.0)(@electric-sql/pglite@0.3.7)(@opentelemetry/api@1.9.0)(better-sqlite3@12.2.0)(postgres@3.4.7)))(ioredis@5.7.0)(magicast@0.3.5)(typescript@5.9.2)(vue@3.5.18(typescript@5.9.2)) '@nuxt/ui-pro': specifier: ^3.2.0 - version: 3.3.0(@babel/parser@7.28.0)(@netlify/blobs@9.1.2)(aws4fetch@1.0.20)(db0@0.3.2(@electric-sql/pglite@0.3.7)(better-sqlite3@12.2.0)(drizzle-orm@0.44.4(@cloudflare/workers-types@4.20250812.0)(@electric-sql/pglite@0.3.7)(@opentelemetry/api@1.9.0)(better-sqlite3@12.2.0)(postgres@3.4.7)))(ioredis@5.7.0)(magicast@0.3.5)(react@19.1.1)(typescript@5.9.2)(valibot@1.1.0(typescript@5.9.2))(vite@7.1.2(@types/node@22.17.1)(jiti@2.5.1)(lightningcss@1.30.1)(terser@5.43.1)(yaml@2.8.1))(vue-router@4.5.1(vue@3.5.18(typescript@5.9.2)))(vue@3.5.18(typescript@5.9.2))(zod@3.25.76) + version: 3.3.0(@babel/parser@7.28.0)(@netlify/blobs@9.1.2)(@vercel/blob@1.1.1)(aws4fetch@1.0.20)(db0@0.3.2(@electric-sql/pglite@0.3.7)(better-sqlite3@12.2.0)(drizzle-orm@0.44.4(@cloudflare/workers-types@4.20250812.0)(@electric-sql/pglite@0.3.7)(@opentelemetry/api@1.9.0)(better-sqlite3@12.2.0)(postgres@3.4.7)))(ioredis@5.7.0)(magicast@0.3.5)(react@19.1.1)(typescript@5.9.2)(valibot@1.1.0(typescript@5.9.2))(vite@7.1.2(@types/node@22.17.1)(jiti@2.5.1)(lightningcss@1.30.1)(terser@5.43.1)(yaml@2.8.1))(vue-router@4.5.1(vue@3.5.18(typescript@5.9.2)))(vue@3.5.18(typescript@5.9.2))(zod@3.25.76) '@nuxtjs/plausible': specifier: ^1.2.0 version: 1.2.0(magicast@0.3.5) @@ -155,16 +158,13 @@ importers: version: 13.6.0(vue@3.5.18(typescript@5.9.2)) '@vueuse/nuxt': specifier: ^13.1.0 - version: 13.6.0(magicast@0.3.5)(nuxt@3.18.1(@electric-sql/pglite@0.3.7)(@netlify/blobs@9.1.2)(@parcel/watcher@2.5.1)(@types/node@22.17.1)(@vue/compiler-sfc@3.5.18)(aws4fetch@1.0.20)(better-sqlite3@12.2.0)(db0@0.3.2(@electric-sql/pglite@0.3.7)(better-sqlite3@12.2.0)(drizzle-orm@0.44.4(@cloudflare/workers-types@4.20250812.0)(@electric-sql/pglite@0.3.7)(@opentelemetry/api@1.9.0)(better-sqlite3@12.2.0)(postgres@3.4.7)))(drizzle-orm@0.44.4(@cloudflare/workers-types@4.20250812.0)(@electric-sql/pglite@0.3.7)(@opentelemetry/api@1.9.0)(better-sqlite3@12.2.0)(postgres@3.4.7))(eslint@9.33.0(jiti@2.5.1))(ioredis@5.7.0)(lightningcss@1.30.1)(magicast@0.3.5)(optionator@0.9.4)(rollup@4.46.2)(terser@5.43.1)(typescript@5.9.2)(vite@7.1.2(@types/node@22.17.1)(jiti@2.5.1)(lightningcss@1.30.1)(terser@5.43.1)(yaml@2.8.1))(vue-tsc@3.0.5(typescript@5.9.2))(yaml@2.8.1))(vue@3.5.18(typescript@5.9.2)) - better-sqlite3: - specifier: ^12.2.0 - version: 12.2.0 + version: 13.6.0(magicast@0.3.5)(nuxt@3.18.1(@electric-sql/pglite@0.3.7)(@netlify/blobs@9.1.2)(@parcel/watcher@2.5.1)(@types/node@22.17.1)(@vercel/blob@1.1.1)(@vue/compiler-sfc@3.5.18)(aws4fetch@1.0.20)(better-sqlite3@12.2.0)(db0@0.3.2(@electric-sql/pglite@0.3.7)(better-sqlite3@12.2.0)(drizzle-orm@0.44.4(@cloudflare/workers-types@4.20250812.0)(@electric-sql/pglite@0.3.7)(@opentelemetry/api@1.9.0)(better-sqlite3@12.2.0)(postgres@3.4.7)))(drizzle-orm@0.44.4(@cloudflare/workers-types@4.20250812.0)(@electric-sql/pglite@0.3.7)(@opentelemetry/api@1.9.0)(better-sqlite3@12.2.0)(postgres@3.4.7))(eslint@9.33.0(jiti@2.5.1))(ioredis@5.7.0)(lightningcss@1.30.1)(magicast@0.3.5)(optionator@0.9.4)(rollup@4.46.2)(terser@5.43.1)(typescript@5.9.2)(vite@7.1.2(@types/node@22.17.1)(jiti@2.5.1)(lightningcss@1.30.1)(terser@5.43.1)(yaml@2.8.1))(vue-tsc@3.0.5(typescript@5.9.2))(yaml@2.8.1))(vue@3.5.18(typescript@5.9.2)) feed: specifier: ^4.2.2 version: 4.2.2 nuxt: specifier: ^3.16.2 - version: 3.18.1(@electric-sql/pglite@0.3.7)(@netlify/blobs@9.1.2)(@parcel/watcher@2.5.1)(@types/node@22.17.1)(@vue/compiler-sfc@3.5.18)(aws4fetch@1.0.20)(better-sqlite3@12.2.0)(db0@0.3.2(@electric-sql/pglite@0.3.7)(better-sqlite3@12.2.0)(drizzle-orm@0.44.4(@cloudflare/workers-types@4.20250812.0)(@electric-sql/pglite@0.3.7)(@opentelemetry/api@1.9.0)(better-sqlite3@12.2.0)(postgres@3.4.7)))(drizzle-orm@0.44.4(@cloudflare/workers-types@4.20250812.0)(@electric-sql/pglite@0.3.7)(@opentelemetry/api@1.9.0)(better-sqlite3@12.2.0)(postgres@3.4.7))(eslint@9.33.0(jiti@2.5.1))(ioredis@5.7.0)(lightningcss@1.30.1)(magicast@0.3.5)(optionator@0.9.4)(rollup@4.46.2)(terser@5.43.1)(typescript@5.9.2)(vite@7.1.2(@types/node@22.17.1)(jiti@2.5.1)(lightningcss@1.30.1)(terser@5.43.1)(yaml@2.8.1))(vue-tsc@3.0.5(typescript@5.9.2))(yaml@2.8.1) + version: 3.18.1(@electric-sql/pglite@0.3.7)(@netlify/blobs@9.1.2)(@parcel/watcher@2.5.1)(@types/node@22.17.1)(@vercel/blob@1.1.1)(@vue/compiler-sfc@3.5.18)(aws4fetch@1.0.20)(better-sqlite3@12.2.0)(db0@0.3.2(@electric-sql/pglite@0.3.7)(better-sqlite3@12.2.0)(drizzle-orm@0.44.4(@cloudflare/workers-types@4.20250812.0)(@electric-sql/pglite@0.3.7)(@opentelemetry/api@1.9.0)(better-sqlite3@12.2.0)(postgres@3.4.7)))(drizzle-orm@0.44.4(@cloudflare/workers-types@4.20250812.0)(@electric-sql/pglite@0.3.7)(@opentelemetry/api@1.9.0)(better-sqlite3@12.2.0)(postgres@3.4.7))(eslint@9.33.0(jiti@2.5.1))(ioredis@5.7.0)(lightningcss@1.30.1)(magicast@0.3.5)(optionator@0.9.4)(rollup@4.46.2)(terser@5.43.1)(typescript@5.9.2)(vite@7.1.2(@types/node@22.17.1)(jiti@2.5.1)(lightningcss@1.30.1)(terser@5.43.1)(yaml@2.8.1))(vue-tsc@3.0.5(typescript@5.9.2))(yaml@2.8.1) nuxt-cloudflare-analytics: specifier: ^1.0.8 version: 1.0.8(magicast@0.3.5) @@ -173,7 +173,7 @@ importers: version: 0.1.3(magicast@0.3.5) nuxt-og-image: specifier: ^5.1.2 - version: 5.1.9(@unhead/vue@2.0.14(vue@3.5.18(typescript@5.9.2)))(magicast@0.3.5)(unstorage@1.16.1(@netlify/blobs@9.1.2)(aws4fetch@1.0.20)(db0@0.3.2(@electric-sql/pglite@0.3.7)(better-sqlite3@12.2.0)(drizzle-orm@0.44.4(@cloudflare/workers-types@4.20250812.0)(@electric-sql/pglite@0.3.7)(@opentelemetry/api@1.9.0)(better-sqlite3@12.2.0)(postgres@3.4.7)))(ioredis@5.7.0))(vite@7.1.2(@types/node@22.17.1)(jiti@2.5.1)(lightningcss@1.30.1)(terser@5.43.1)(yaml@2.8.1))(vue@3.5.18(typescript@5.9.2)) + version: 5.1.9(@unhead/vue@2.0.14(vue@3.5.18(typescript@5.9.2)))(magicast@0.3.5)(unstorage@1.16.1(@netlify/blobs@9.1.2)(@vercel/blob@1.1.1)(aws4fetch@1.0.20)(db0@0.3.2(@electric-sql/pglite@0.3.7)(better-sqlite3@12.2.0)(drizzle-orm@0.44.4(@cloudflare/workers-types@4.20250812.0)(@electric-sql/pglite@0.3.7)(@opentelemetry/api@1.9.0)(better-sqlite3@12.2.0)(postgres@3.4.7)))(ioredis@5.7.0))(vite@7.1.2(@types/node@22.17.1)(jiti@2.5.1)(lightningcss@1.30.1)(terser@5.43.1)(yaml@2.8.1))(vue@3.5.18(typescript@5.9.2)) playground: dependencies: @@ -191,7 +191,7 @@ importers: version: 1.7.0(magicast@0.3.5) '@nuxt/ui': specifier: ^3.2.0 - version: 3.3.0(@babel/parser@7.28.0)(@netlify/blobs@9.1.2)(aws4fetch@1.0.20)(change-case@5.4.4)(db0@0.3.2(@electric-sql/pglite@0.3.7)(better-sqlite3@12.2.0)(drizzle-orm@0.44.4(@cloudflare/workers-types@4.20250812.0)(@electric-sql/pglite@0.3.7)(@opentelemetry/api@1.9.0)(better-sqlite3@12.2.0)(postgres@3.4.7)))(embla-carousel@8.6.0)(ioredis@5.7.0)(jwt-decode@4.0.0)(magicast@0.3.5)(typescript@5.9.2)(valibot@1.1.0(typescript@5.9.2))(vite@7.1.2(@types/node@22.17.1)(jiti@2.5.1)(lightningcss@1.30.1)(terser@5.43.1)(yaml@2.8.1))(vue-router@4.5.1(vue@3.5.18(typescript@5.9.2)))(vue@3.5.18(typescript@5.9.2))(zod@3.25.76) + version: 3.3.0(@babel/parser@7.28.0)(@netlify/blobs@9.1.2)(@vercel/blob@1.1.1)(aws4fetch@1.0.20)(change-case@5.4.4)(db0@0.3.2(@electric-sql/pglite@0.3.7)(better-sqlite3@12.2.0)(drizzle-orm@0.44.4(@cloudflare/workers-types@4.20250812.0)(@electric-sql/pglite@0.3.7)(@opentelemetry/api@1.9.0)(better-sqlite3@12.2.0)(postgres@3.4.7)))(embla-carousel@8.6.0)(ioredis@5.7.0)(jwt-decode@4.0.0)(magicast@0.3.5)(typescript@5.9.2)(valibot@1.1.0(typescript@5.9.2))(vite@7.1.2(@types/node@22.17.1)(jiti@2.5.1)(lightningcss@1.30.1)(terser@5.43.1)(yaml@2.8.1))(vue-router@4.5.1(vue@3.5.18(typescript@5.9.2)))(vue@3.5.18(typescript@5.9.2))(zod@3.25.76) '@nuxthub/core': specifier: workspace:* version: link:.. @@ -203,7 +203,7 @@ importers: version: 13.6.0(vue@3.5.18(typescript@5.9.2)) '@vueuse/nuxt': specifier: ^13.2.0 - version: 13.6.0(magicast@0.3.5)(nuxt@3.18.1(@electric-sql/pglite@0.3.7)(@netlify/blobs@9.1.2)(@parcel/watcher@2.5.1)(@types/node@22.17.1)(@vue/compiler-sfc@3.5.18)(aws4fetch@1.0.20)(better-sqlite3@12.2.0)(db0@0.3.2(@electric-sql/pglite@0.3.7)(better-sqlite3@12.2.0)(drizzle-orm@0.44.4(@cloudflare/workers-types@4.20250812.0)(@electric-sql/pglite@0.3.7)(@opentelemetry/api@1.9.0)(better-sqlite3@12.2.0)(postgres@3.4.7)))(drizzle-orm@0.44.4(@cloudflare/workers-types@4.20250812.0)(@electric-sql/pglite@0.3.7)(@opentelemetry/api@1.9.0)(better-sqlite3@12.2.0)(postgres@3.4.7))(eslint@9.33.0(jiti@2.5.1))(ioredis@5.7.0)(lightningcss@1.30.1)(magicast@0.3.5)(optionator@0.9.4)(rollup@4.46.2)(terser@5.43.1)(typescript@5.9.2)(vite@7.1.2(@types/node@22.17.1)(jiti@2.5.1)(lightningcss@1.30.1)(terser@5.43.1)(yaml@2.8.1))(vue-tsc@3.0.5(typescript@5.9.2))(yaml@2.8.1))(vue@3.5.18(typescript@5.9.2)) + version: 13.6.0(magicast@0.3.5)(nuxt@3.18.1(@electric-sql/pglite@0.3.7)(@netlify/blobs@9.1.2)(@parcel/watcher@2.5.1)(@types/node@22.17.1)(@vercel/blob@1.1.1)(@vue/compiler-sfc@3.5.18)(aws4fetch@1.0.20)(better-sqlite3@12.2.0)(db0@0.3.2(@electric-sql/pglite@0.3.7)(better-sqlite3@12.2.0)(drizzle-orm@0.44.4(@cloudflare/workers-types@4.20250812.0)(@electric-sql/pglite@0.3.7)(@opentelemetry/api@1.9.0)(better-sqlite3@12.2.0)(postgres@3.4.7)))(drizzle-orm@0.44.4(@cloudflare/workers-types@4.20250812.0)(@electric-sql/pglite@0.3.7)(@opentelemetry/api@1.9.0)(better-sqlite3@12.2.0)(postgres@3.4.7))(eslint@9.33.0(jiti@2.5.1))(ioredis@5.7.0)(lightningcss@1.30.1)(magicast@0.3.5)(optionator@0.9.4)(rollup@4.46.2)(terser@5.43.1)(typescript@5.9.2)(vite@7.1.2(@types/node@22.17.1)(jiti@2.5.1)(lightningcss@1.30.1)(terser@5.43.1)(yaml@2.8.1))(vue-tsc@3.0.5(typescript@5.9.2))(yaml@2.8.1))(vue@3.5.18(typescript@5.9.2)) ai: specifier: ^4.3.15 version: 4.3.19(react@19.1.1)(zod@3.25.76) @@ -215,7 +215,7 @@ importers: version: 0.44.4(@cloudflare/workers-types@4.20250812.0)(@electric-sql/pglite@0.3.7)(@opentelemetry/api@1.9.0)(better-sqlite3@12.2.0)(postgres@3.4.7) nuxt: specifier: ^3.17.3 - version: 3.18.1(@electric-sql/pglite@0.3.7)(@netlify/blobs@9.1.2)(@parcel/watcher@2.5.1)(@types/node@22.17.1)(@vue/compiler-sfc@3.5.18)(aws4fetch@1.0.20)(better-sqlite3@12.2.0)(db0@0.3.2(@electric-sql/pglite@0.3.7)(better-sqlite3@12.2.0)(drizzle-orm@0.44.4(@cloudflare/workers-types@4.20250812.0)(@electric-sql/pglite@0.3.7)(@opentelemetry/api@1.9.0)(better-sqlite3@12.2.0)(postgres@3.4.7)))(drizzle-orm@0.44.4(@cloudflare/workers-types@4.20250812.0)(@electric-sql/pglite@0.3.7)(@opentelemetry/api@1.9.0)(better-sqlite3@12.2.0)(postgres@3.4.7))(eslint@9.33.0(jiti@2.5.1))(ioredis@5.7.0)(lightningcss@1.30.1)(magicast@0.3.5)(optionator@0.9.4)(rollup@4.46.2)(terser@5.43.1)(typescript@5.9.2)(vite@7.1.2(@types/node@22.17.1)(jiti@2.5.1)(lightningcss@1.30.1)(terser@5.43.1)(yaml@2.8.1))(vue-tsc@3.0.5(typescript@5.9.2))(yaml@2.8.1) + version: 3.18.1(@electric-sql/pglite@0.3.7)(@netlify/blobs@9.1.2)(@parcel/watcher@2.5.1)(@types/node@22.17.1)(@vercel/blob@1.1.1)(@vue/compiler-sfc@3.5.18)(aws4fetch@1.0.20)(better-sqlite3@12.2.0)(db0@0.3.2(@electric-sql/pglite@0.3.7)(better-sqlite3@12.2.0)(drizzle-orm@0.44.4(@cloudflare/workers-types@4.20250812.0)(@electric-sql/pglite@0.3.7)(@opentelemetry/api@1.9.0)(better-sqlite3@12.2.0)(postgres@3.4.7)))(drizzle-orm@0.44.4(@cloudflare/workers-types@4.20250812.0)(@electric-sql/pglite@0.3.7)(@opentelemetry/api@1.9.0)(better-sqlite3@12.2.0)(postgres@3.4.7))(eslint@9.33.0(jiti@2.5.1))(ioredis@5.7.0)(lightningcss@1.30.1)(magicast@0.3.5)(optionator@0.9.4)(rollup@4.46.2)(terser@5.43.1)(typescript@5.9.2)(vite@7.1.2(@types/node@22.17.1)(jiti@2.5.1)(lightningcss@1.30.1)(terser@5.43.1)(yaml@2.8.1))(vue-tsc@3.0.5(typescript@5.9.2))(yaml@2.8.1) nuxt-auth-utils: specifier: ^0.5.20 version: 0.5.23(magicast@0.3.5) @@ -237,7 +237,7 @@ importers: devDependencies: '@nuxt/devtools': specifier: latest - version: 2.6.2(vite@7.1.2(@types/node@22.17.1)(jiti@2.5.1)(lightningcss@1.30.1)(terser@5.43.1)(yaml@2.8.1))(vue@3.5.18(typescript@5.9.2)) + version: 2.6.3(vite@7.1.2(@types/node@22.17.1)(jiti@2.5.1)(lightningcss@1.30.1)(terser@5.43.1)(yaml@2.8.1))(vue@3.5.18(typescript@5.9.2)) packages: @@ -1030,6 +1030,10 @@ packages: resolution: {integrity: sha512-OIHZrb2ImZ7XG85HXOONLcJWGosv7sIvM2ifAPQVhg9Lv7qdmMBNVaai4QTdyuaqbKM5eO6sLSQOYI7wEQeCJQ==} engines: {node: '>=14'} + '@fastify/busboy@2.1.1': + resolution: {integrity: sha512-vBZP4NlzfOlerQTnba4aqZoMhE/a9HY7HRqoOPaETQcSQuWEIyZMHGfVu6w9wGtGK5fED5qRs2DteVCjOH60sA==} + engines: {node: '>=14'} + '@fastify/busboy@3.1.1': resolution: {integrity: sha512-5DGmA8FTdB2XbDeEwc/5ZXBl6UbBAyBOOLlPuBnZ/N1SwdH9Ii+cOX3tBROlDgcTXxjOYnLMVoKk9+FXAw0CJw==} @@ -1351,16 +1355,31 @@ packages: peerDependencies: vite: '>=6.0' + '@nuxt/devtools-kit@2.6.3': + resolution: {integrity: sha512-cDmai3Ws6AbJlYy1p4CCwc718cfbqtAjXe6oEc6q03zoJnvX1PsvKUfmU+yuowfqTSR6DZRmH4SjCBWuMjgaKQ==} + peerDependencies: + vite: '>=6.0' + '@nuxt/devtools-wizard@2.6.2': resolution: {integrity: sha512-s1eYYKi2eZu2ZUPQrf22C0SceWs5/C3c3uow/DVunD304Um/Tj062xM9E4p1B9L8yjaq8t0Gtyu/YvZdo/reyg==} hasBin: true + '@nuxt/devtools-wizard@2.6.3': + resolution: {integrity: sha512-FWXPkuJ1RUp+9nWP5Vvk29cJPNtm4OO38bgr9G8vGbqcRznzgaSODH/92c8sm2dKR7AF+9MAYLL+BexOWOkljQ==} + hasBin: true + '@nuxt/devtools@2.6.2': resolution: {integrity: sha512-pqcSDPv1I+8fxa6FvhAxVrfcN/sXYLOBe9scTLbRQOVLTO0pHzryayho678qNKiwWGgj/rcjEDr6IZCgwqOCfA==} hasBin: true peerDependencies: vite: '>=6.0' + '@nuxt/devtools@2.6.3': + resolution: {integrity: sha512-n+8we7pr0tNl6w+KfbFDXZsYpWIYL4vG/daIdRF66lQ6fLyQy/CcxDAx8+JNu3Ew96RjuBtWRSbCCv454L5p0Q==} + hasBin: true + peerDependencies: + vite: '>=6.0' + '@nuxt/eslint-config@1.8.0': resolution: {integrity: sha512-SaS+s+1qnNENcVzkpmHPMuKwFeZTp7QR/UM3kZfqx60mWtWVB2iHNwR0a0DeJBZM84tebKzicrzEP/1J+otWBw==} peerDependencies: @@ -2758,6 +2777,10 @@ packages: '@uploadthing/mime-types@0.3.5': resolution: {integrity: sha512-iYOmod80XXOSe4NVvaUG9FsS91YGPUaJMTBj52Nwu0G2aTzEN6Xcl0mG1rWqXJ4NUH8MzjVqg+tQND5TPkJWhg==} + '@vercel/blob@1.1.1': + resolution: {integrity: sha512-heiJGj2qt5qTv6yiShH9f6KRAoZGj+lz61GQ+lBRL4lhvUmKI9A51KYlQTnsUd9ymdFlKHBlvmPeG+yGz2Qsbg==} + engines: {node: '>=16.14'} + '@vercel/nft@0.29.4': resolution: {integrity: sha512-6lLqMNX3TuycBPABycx7A9F1bHQR7kiQln6abjFbPrf5C/05qHM9M5E4PeTE59c7z8g6vHnx1Ioihb2AQl7BTA==} engines: {node: '>=18'} @@ -3111,6 +3134,9 @@ packages: resolution: {integrity: sha512-72XOdbzQCMKERvFrxAykatn2pu7osPNq/sNUzwcHdWzwPvOsNpPqkawfDXVvQbA2RT+ivtsMNjYdojTUZitt1A==} engines: {node: '>=20.18.0'} + async-retry@1.3.3: + resolution: {integrity: sha512-wfr/jstw9xNi/0teMHrRW7dsz3Lt5ARhYNZ2ewpadnhaIp5mbALhOAP+EAdsC7t4Z6wqsDVv9+W6gm1Dk9mEyw==} + async-sema@3.1.1: resolution: {integrity: sha512-tLRNUXati5MFePdAk8dw7Qt7DpxPB60ofAgn8WRhW6a2rcimZnYBP9oxHiv0OHy+Wz7kPMG+t4LGdt31+4EmGg==} @@ -4754,6 +4780,10 @@ packages: resolution: {integrity: sha512-ZMERYes6pDydyuGidse7OsHxtbI7WVeUEozgR/g7rd0xUimYNlvZRE/K2MgZTjWy725IfelLeVcEM97mmtRGXw==} engines: {node: '>=8'} + is-buffer@2.0.5: + resolution: {integrity: sha512-i2R6zNFDwgEHJyQUtJEk0XFi1i0dPFn/oqjK3/vPCcDeJvW5NQ83V8QbicfF1SupOaB0h8ntgBC2YiE7dfyctQ==} + engines: {node: '>=4'} + is-builtin-module@3.2.1: resolution: {integrity: sha512-BSLE3HnV2syZ0FK0iMA/yUGplUeMmNz4AW5fnTunbCIqZi4vG3WjJT9FHMy5D69xmAYBHXQhJdALdpwVxV501A==} engines: {node: '>=6'} @@ -4806,6 +4836,9 @@ packages: is-module@1.0.0: resolution: {integrity: sha512-51ypPSPCoTEIN9dy5Oy+h4pShgJmPCygKfyRCISBI+JoWT/2oJvK8QPxmwv7b/p239jXrm9M1mlQbyKJ5A152g==} + is-node-process@1.2.0: + resolution: {integrity: sha512-Vg4o6/fqPxIjtxgUH5QLJhwZ7gW5diGCVlXpuUfELC62CuxM1iHcRe51f2W1FDy04Ai4KJkagKjx3XaqyfRKXw==} + is-number@7.0.0: resolution: {integrity: sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==} engines: {node: '>=0.12.0'} @@ -5085,6 +5118,10 @@ packages: resolution: {integrity: sha512-WunYko2W1NcdfAFpuLUoucsgULmgDBRkdxHxWQ7mK0cQqwPiy8E1enjuRBrhLtZkB5iScJ1XIPdhVEFK8aOLSg==} engines: {node: '>=14'} + local-pkg@1.1.2: + resolution: {integrity: sha512-arhlxbFRmoQHl33a0Zkle/YWlmNwoyt6QNZEIJcqNbdrsix5Lvc4HyyI3EnwxTYlZYc32EbYrQ8SzEZ7dqgg9A==} + engines: {node: '>=14'} + locate-path@6.0.0: resolution: {integrity: sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw==} engines: {node: '>=10'} @@ -5879,6 +5916,9 @@ packages: pkg-types@2.2.0: resolution: {integrity: sha512-2SM/GZGAEkPp3KWORxQZns4M+WSeXbC2HEvmOIJe3Cmiv6ieAJvdVhDldtHqM5J1Y7MrR1XhkBT/rMlhh9FdqQ==} + pkg-types@2.3.0: + resolution: {integrity: sha512-SIqCzDRg0s9npO5XQ3tNZioRY1uK06lA41ynBC1YmFTmnY6FjUjVt6s4LoADmwoig1qqD0oK8h1p/8mlMx8Oig==} + playwright-core@1.54.2: resolution: {integrity: sha512-n5r4HFbMmWsB4twG7tJLDN9gmBUeSPcsBZiWSE4DnYz9mJMAFqr2ID7+eGC9kpEnxExJ1epttwR59LEWCk8mtA==} engines: {node: '>=18'} @@ -6166,6 +6206,9 @@ packages: quansync@0.2.10: resolution: {integrity: sha512-t41VRkMYbkHyCYmOvx/6URnN80H7k4X0lLdBMGsz+maAwrJQYB1djpV6vHrQIBE0WBSGqhtEHrK9U3DWWH8v7A==} + quansync@0.2.11: + resolution: {integrity: sha512-AifT7QEbW9Nri4tAwR5M/uzpBuqfZf+zwaEM/QkzEjj7NBuFD2rBuy0K3dE+8wltbezDV7JMA0WfnCPYRSYbXA==} + queue-microtask@1.2.3: resolution: {integrity: sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==} @@ -6334,6 +6377,10 @@ packages: restructure@3.0.2: resolution: {integrity: sha512-gSfoiOEA0VPE6Tukkrr7I0RBdE0s7H1eFCDBk05l1KIQT1UIKNc5JZy6jdyW6eYH3aR3g5b3PuL77rq0hvwtAw==} + retry@0.13.1: + resolution: {integrity: sha512-XQBQ3I8W1Cge0Seh+6gjj03LbmRFWuoszgK9ooCpwYIrhhoO80pfq4cUkU5DkknwfOfFteRwlZ56PYOGYyFWdg==} + engines: {node: '>= 4'} + reusify@1.1.0: resolution: {integrity: sha512-g6QUff04oZpHs0eG5p83rFLhHeV00ug/Yf9nZM6fLeUrPguBTkTQOdpAWWspMh55TZfVQDPaN3NQJfbVRAxdIw==} engines: {iojs: '>=1.0.0', node: '>=0.10.0'} @@ -6900,6 +6947,10 @@ packages: undici-types@6.21.0: resolution: {integrity: sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ==} + undici@5.29.0: + resolution: {integrity: sha512-raqeBD6NQK4SkWhQzeYKd1KmIG6dllBOTt55Rmkt4HtI9mwdWtJljnrXjAFUBLTSN67HWrOIZ3EPF4kjUw80Bg==} + engines: {node: '>=14.0'} + undici@7.13.0: resolution: {integrity: sha512-l+zSMssRqrzDcb3fjMkjjLGmuiiK2pMIcV++mJaAc9vhjSGpvM7h43QgP+OAMb1GImHmbPyG2tBXeuyG5iY4gA==} engines: {node: '>=20.18.1'} @@ -8172,6 +8223,8 @@ snapshots: '@fastify/accept-negotiator@1.1.0': optional: true + '@fastify/busboy@2.1.1': {} + '@fastify/busboy@3.1.1': {} '@floating-ui/core@1.7.3': @@ -8629,6 +8682,14 @@ snapshots: transitivePeerDependencies: - magicast + '@nuxt/devtools-kit@2.6.3(magicast@0.3.5)(vite@7.1.2(@types/node@22.17.1)(jiti@2.5.1)(lightningcss@1.30.1)(terser@5.43.1)(yaml@2.8.1))': + dependencies: + '@nuxt/kit': 3.18.1(magicast@0.3.5) + execa: 8.0.1 + vite: 7.1.2(@types/node@22.17.1)(jiti@2.5.1)(lightningcss@1.30.1)(terser@5.43.1)(yaml@2.8.1) + transitivePeerDependencies: + - magicast + '@nuxt/devtools-wizard@2.6.2': dependencies: consola: 3.4.2 @@ -8640,6 +8701,17 @@ snapshots: prompts: 2.4.2 semver: 7.7.2 + '@nuxt/devtools-wizard@2.6.3': + dependencies: + consola: 3.4.2 + diff: 8.0.2 + execa: 8.0.1 + magicast: 0.3.5 + pathe: 2.0.3 + pkg-types: 2.3.0 + prompts: 2.4.2 + semver: 7.7.2 + '@nuxt/devtools@2.6.2(vite@7.1.2(@types/node@22.17.1)(jiti@2.5.1)(lightningcss@1.30.1)(terser@5.43.1)(yaml@2.8.1))(vue@3.5.18(typescript@5.9.2))': dependencies: '@nuxt/devtools-kit': 2.6.2(magicast@0.3.5)(vite@7.1.2(@types/node@22.17.1)(jiti@2.5.1)(lightningcss@1.30.1)(terser@5.43.1)(yaml@2.8.1)) @@ -8681,6 +8753,47 @@ snapshots: - utf-8-validate - vue + '@nuxt/devtools@2.6.3(vite@7.1.2(@types/node@22.17.1)(jiti@2.5.1)(lightningcss@1.30.1)(terser@5.43.1)(yaml@2.8.1))(vue@3.5.18(typescript@5.9.2))': + dependencies: + '@nuxt/devtools-kit': 2.6.3(magicast@0.3.5)(vite@7.1.2(@types/node@22.17.1)(jiti@2.5.1)(lightningcss@1.30.1)(terser@5.43.1)(yaml@2.8.1)) + '@nuxt/devtools-wizard': 2.6.3 + '@nuxt/kit': 3.18.1(magicast@0.3.5) + '@vue/devtools-core': 7.7.7(vite@7.1.2(@types/node@22.17.1)(jiti@2.5.1)(lightningcss@1.30.1)(terser@5.43.1)(yaml@2.8.1))(vue@3.5.18(typescript@5.9.2)) + '@vue/devtools-kit': 7.7.7 + birpc: 2.5.0 + consola: 3.4.2 + destr: 2.0.5 + error-stack-parser-es: 1.0.5 + execa: 8.0.1 + fast-npm-meta: 0.4.6 + get-port-please: 3.2.0 + hookable: 5.5.3 + image-meta: 0.2.1 + is-installed-globally: 1.0.0 + launch-editor: 2.11.1 + local-pkg: 1.1.2 + magicast: 0.3.5 + nypm: 0.6.1 + ohash: 2.0.11 + pathe: 2.0.3 + perfect-debounce: 1.0.0 + pkg-types: 2.3.0 + semver: 7.7.2 + simple-git: 3.28.0 + sirv: 3.0.1 + structured-clone-es: 1.0.0 + tinyglobby: 0.2.14 + vite: 7.1.2(@types/node@22.17.1)(jiti@2.5.1)(lightningcss@1.30.1)(terser@5.43.1)(yaml@2.8.1) + vite-plugin-inspect: 11.3.2(@nuxt/kit@3.18.1(magicast@0.3.5))(vite@7.1.2(@types/node@22.17.1)(jiti@2.5.1)(lightningcss@1.30.1)(terser@5.43.1)(yaml@2.8.1)) + vite-plugin-vue-tracer: 1.0.0(vite@7.1.2(@types/node@22.17.1)(jiti@2.5.1)(lightningcss@1.30.1)(terser@5.43.1)(yaml@2.8.1))(vue@3.5.18(typescript@5.9.2)) + which: 5.0.0 + ws: 8.18.3 + transitivePeerDependencies: + - bufferutil + - supports-color + - utf-8-validate + - vue + '@nuxt/eslint-config@1.8.0(@typescript-eslint/utils@8.39.1(eslint@9.33.0(jiti@2.5.1))(typescript@5.9.2))(@vue/compiler-sfc@3.5.18)(eslint@9.33.0(jiti@2.5.1))(typescript@5.9.2)': dependencies: '@antfu/install-pkg': 1.1.0 @@ -8721,7 +8834,7 @@ snapshots: - supports-color - typescript - '@nuxt/fonts@0.11.4(@netlify/blobs@9.1.2)(aws4fetch@1.0.20)(db0@0.3.2(@electric-sql/pglite@0.3.7)(better-sqlite3@12.2.0)(drizzle-orm@0.44.4(@cloudflare/workers-types@4.20250812.0)(@electric-sql/pglite@0.3.7)(@opentelemetry/api@1.9.0)(better-sqlite3@12.2.0)(postgres@3.4.7)))(ioredis@5.7.0)(magicast@0.3.5)(vite@7.1.2(@types/node@22.17.1)(jiti@2.5.1)(lightningcss@1.30.1)(terser@5.43.1)(yaml@2.8.1))': + '@nuxt/fonts@0.11.4(@netlify/blobs@9.1.2)(@vercel/blob@1.1.1)(aws4fetch@1.0.20)(db0@0.3.2(@electric-sql/pglite@0.3.7)(better-sqlite3@12.2.0)(drizzle-orm@0.44.4(@cloudflare/workers-types@4.20250812.0)(@electric-sql/pglite@0.3.7)(@opentelemetry/api@1.9.0)(better-sqlite3@12.2.0)(postgres@3.4.7)))(ioredis@5.7.0)(magicast@0.3.5)(vite@7.1.2(@types/node@22.17.1)(jiti@2.5.1)(lightningcss@1.30.1)(terser@5.43.1)(yaml@2.8.1))': dependencies: '@nuxt/devtools-kit': 2.6.2(magicast@0.3.5)(vite@7.1.2(@types/node@22.17.1)(jiti@2.5.1)(lightningcss@1.30.1)(terser@5.43.1)(yaml@2.8.1)) '@nuxt/kit': 3.18.1(magicast@0.3.5) @@ -8742,7 +8855,7 @@ snapshots: ufo: 1.6.1 unifont: 0.4.1 unplugin: 2.3.5 - unstorage: 1.16.1(@netlify/blobs@9.1.2)(aws4fetch@1.0.20)(db0@0.3.2(@electric-sql/pglite@0.3.7)(better-sqlite3@12.2.0)(drizzle-orm@0.44.4(@cloudflare/workers-types@4.20250812.0)(@electric-sql/pglite@0.3.7)(@opentelemetry/api@1.9.0)(better-sqlite3@12.2.0)(postgres@3.4.7)))(ioredis@5.7.0) + unstorage: 1.16.1(@netlify/blobs@9.1.2)(@vercel/blob@1.1.1)(aws4fetch@1.0.20)(db0@0.3.2(@electric-sql/pglite@0.3.7)(better-sqlite3@12.2.0)(drizzle-orm@0.44.4(@cloudflare/workers-types@4.20250812.0)(@electric-sql/pglite@0.3.7)(@opentelemetry/api@1.9.0)(better-sqlite3@12.2.0)(postgres@3.4.7)))(ioredis@5.7.0) transitivePeerDependencies: - '@azure/app-configuration' - '@azure/cosmos' @@ -8788,7 +8901,7 @@ snapshots: - vite - vue - '@nuxt/image@1.11.0(@netlify/blobs@9.1.2)(aws4fetch@1.0.20)(db0@0.3.2(@electric-sql/pglite@0.3.7)(better-sqlite3@12.2.0)(drizzle-orm@0.44.4(@cloudflare/workers-types@4.20250812.0)(@electric-sql/pglite@0.3.7)(@opentelemetry/api@1.9.0)(better-sqlite3@12.2.0)(postgres@3.4.7)))(ioredis@5.7.0)(magicast@0.3.5)': + '@nuxt/image@1.11.0(@netlify/blobs@9.1.2)(@vercel/blob@1.1.1)(aws4fetch@1.0.20)(db0@0.3.2(@electric-sql/pglite@0.3.7)(better-sqlite3@12.2.0)(drizzle-orm@0.44.4(@cloudflare/workers-types@4.20250812.0)(@electric-sql/pglite@0.3.7)(@opentelemetry/api@1.9.0)(better-sqlite3@12.2.0)(postgres@3.4.7)))(ioredis@5.7.0)(magicast@0.3.5)': dependencies: '@nuxt/kit': 3.18.1(magicast@0.3.5) consola: 3.4.2 @@ -8801,7 +8914,7 @@ snapshots: std-env: 3.9.0 ufo: 1.6.1 optionalDependencies: - ipx: 2.1.1(@netlify/blobs@9.1.2)(aws4fetch@1.0.20)(db0@0.3.2(@electric-sql/pglite@0.3.7)(better-sqlite3@12.2.0)(drizzle-orm@0.44.4(@cloudflare/workers-types@4.20250812.0)(@electric-sql/pglite@0.3.7)(@opentelemetry/api@1.9.0)(better-sqlite3@12.2.0)(postgres@3.4.7)))(ioredis@5.7.0) + ipx: 2.1.1(@netlify/blobs@9.1.2)(@vercel/blob@1.1.1)(aws4fetch@1.0.20)(db0@0.3.2(@electric-sql/pglite@0.3.7)(better-sqlite3@12.2.0)(drizzle-orm@0.44.4(@cloudflare/workers-types@4.20250812.0)(@electric-sql/pglite@0.3.7)(@opentelemetry/api@1.9.0)(better-sqlite3@12.2.0)(postgres@3.4.7)))(ioredis@5.7.0) transitivePeerDependencies: - '@azure/app-configuration' - '@azure/cosmos' @@ -8918,7 +9031,7 @@ snapshots: std-env: 3.9.0 ufo: 1.6.1 - '@nuxt/scripts@0.11.10(@netlify/blobs@9.1.2)(@unhead/vue@2.0.14(vue@3.5.18(typescript@5.9.2)))(aws4fetch@1.0.20)(db0@0.3.2(@electric-sql/pglite@0.3.7)(better-sqlite3@12.2.0)(drizzle-orm@0.44.4(@cloudflare/workers-types@4.20250812.0)(@electric-sql/pglite@0.3.7)(@opentelemetry/api@1.9.0)(better-sqlite3@12.2.0)(postgres@3.4.7)))(ioredis@5.7.0)(magicast@0.3.5)(typescript@5.9.2)(vue@3.5.18(typescript@5.9.2))': + '@nuxt/scripts@0.11.10(@netlify/blobs@9.1.2)(@unhead/vue@2.0.14(vue@3.5.18(typescript@5.9.2)))(@vercel/blob@1.1.1)(aws4fetch@1.0.20)(db0@0.3.2(@electric-sql/pglite@0.3.7)(better-sqlite3@12.2.0)(drizzle-orm@0.44.4(@cloudflare/workers-types@4.20250812.0)(@electric-sql/pglite@0.3.7)(@opentelemetry/api@1.9.0)(better-sqlite3@12.2.0)(postgres@3.4.7)))(ioredis@5.7.0)(magicast@0.3.5)(typescript@5.9.2)(vue@3.5.18(typescript@5.9.2))': dependencies: '@nuxt/kit': 4.0.3(magicast@0.3.5) '@unhead/vue': 2.0.14(vue@3.5.18(typescript@5.9.2)) @@ -8935,7 +9048,7 @@ snapshots: std-env: 3.9.0 ufo: 1.6.1 unplugin: 2.3.5 - unstorage: 1.16.1(@netlify/blobs@9.1.2)(aws4fetch@1.0.20)(db0@0.3.2(@electric-sql/pglite@0.3.7)(better-sqlite3@12.2.0)(drizzle-orm@0.44.4(@cloudflare/workers-types@4.20250812.0)(@electric-sql/pglite@0.3.7)(@opentelemetry/api@1.9.0)(better-sqlite3@12.2.0)(postgres@3.4.7)))(ioredis@5.7.0) + unstorage: 1.16.1(@netlify/blobs@9.1.2)(@vercel/blob@1.1.1)(aws4fetch@1.0.20)(db0@0.3.2(@electric-sql/pglite@0.3.7)(better-sqlite3@12.2.0)(drizzle-orm@0.44.4(@cloudflare/workers-types@4.20250812.0)(@electric-sql/pglite@0.3.7)(@opentelemetry/api@1.9.0)(better-sqlite3@12.2.0)(postgres@3.4.7)))(ioredis@5.7.0) valibot: 1.1.0(typescript@5.9.2) transitivePeerDependencies: - '@azure/app-configuration' @@ -9010,12 +9123,12 @@ snapshots: - magicast - typescript - '@nuxt/ui-pro@3.3.0(@babel/parser@7.28.0)(@netlify/blobs@9.1.2)(aws4fetch@1.0.20)(db0@0.3.2(@electric-sql/pglite@0.3.7)(better-sqlite3@12.2.0)(drizzle-orm@0.44.4(@cloudflare/workers-types@4.20250812.0)(@electric-sql/pglite@0.3.7)(@opentelemetry/api@1.9.0)(better-sqlite3@12.2.0)(postgres@3.4.7)))(ioredis@5.7.0)(magicast@0.3.5)(react@19.1.1)(typescript@5.9.2)(valibot@1.1.0(typescript@5.9.2))(vite@7.1.2(@types/node@22.17.1)(jiti@2.5.1)(lightningcss@1.30.1)(terser@5.43.1)(yaml@2.8.1))(vue-router@4.5.1(vue@3.5.18(typescript@5.9.2)))(vue@3.5.18(typescript@5.9.2))(zod@3.25.76)': + '@nuxt/ui-pro@3.3.0(@babel/parser@7.28.0)(@netlify/blobs@9.1.2)(@vercel/blob@1.1.1)(aws4fetch@1.0.20)(db0@0.3.2(@electric-sql/pglite@0.3.7)(better-sqlite3@12.2.0)(drizzle-orm@0.44.4(@cloudflare/workers-types@4.20250812.0)(@electric-sql/pglite@0.3.7)(@opentelemetry/api@1.9.0)(better-sqlite3@12.2.0)(postgres@3.4.7)))(ioredis@5.7.0)(magicast@0.3.5)(react@19.1.1)(typescript@5.9.2)(valibot@1.1.0(typescript@5.9.2))(vite@7.1.2(@types/node@22.17.1)(jiti@2.5.1)(lightningcss@1.30.1)(terser@5.43.1)(yaml@2.8.1))(vue-router@4.5.1(vue@3.5.18(typescript@5.9.2)))(vue@3.5.18(typescript@5.9.2))(zod@3.25.76)': dependencies: '@ai-sdk/vue': 1.2.12(vue@3.5.18(typescript@5.9.2))(zod@3.25.76) '@nuxt/kit': 4.0.3(magicast@0.3.5) '@nuxt/schema': 4.0.3 - '@nuxt/ui': 3.3.0(@babel/parser@7.28.0)(@netlify/blobs@9.1.2)(aws4fetch@1.0.20)(change-case@5.4.4)(db0@0.3.2(@electric-sql/pglite@0.3.7)(better-sqlite3@12.2.0)(drizzle-orm@0.44.4(@cloudflare/workers-types@4.20250812.0)(@electric-sql/pglite@0.3.7)(@opentelemetry/api@1.9.0)(better-sqlite3@12.2.0)(postgres@3.4.7)))(embla-carousel@8.6.0)(ioredis@5.7.0)(jwt-decode@4.0.0)(magicast@0.3.5)(typescript@5.9.2)(valibot@1.1.0(typescript@5.9.2))(vite@7.1.2(@types/node@22.17.1)(jiti@2.5.1)(lightningcss@1.30.1)(terser@5.43.1)(yaml@2.8.1))(vue-router@4.5.1(vue@3.5.18(typescript@5.9.2)))(vue@3.5.18(typescript@5.9.2))(zod@3.25.76) + '@nuxt/ui': 3.3.0(@babel/parser@7.28.0)(@netlify/blobs@9.1.2)(@vercel/blob@1.1.1)(aws4fetch@1.0.20)(change-case@5.4.4)(db0@0.3.2(@electric-sql/pglite@0.3.7)(better-sqlite3@12.2.0)(drizzle-orm@0.44.4(@cloudflare/workers-types@4.20250812.0)(@electric-sql/pglite@0.3.7)(@opentelemetry/api@1.9.0)(better-sqlite3@12.2.0)(postgres@3.4.7)))(embla-carousel@8.6.0)(ioredis@5.7.0)(jwt-decode@4.0.0)(magicast@0.3.5)(typescript@5.9.2)(valibot@1.1.0(typescript@5.9.2))(vite@7.1.2(@types/node@22.17.1)(jiti@2.5.1)(lightningcss@1.30.1)(terser@5.43.1)(yaml@2.8.1))(vue-router@4.5.1(vue@3.5.18(typescript@5.9.2)))(vue@3.5.18(typescript@5.9.2))(zod@3.25.76) '@standard-schema/spec': 1.0.0 '@vueuse/core': 13.6.0(vue@3.5.18(typescript@5.9.2)) consola: 3.4.2 @@ -9079,12 +9192,12 @@ snapshots: - vue - vue-router - '@nuxt/ui@3.3.0(@babel/parser@7.28.0)(@netlify/blobs@9.1.2)(aws4fetch@1.0.20)(change-case@5.4.4)(db0@0.3.2(@electric-sql/pglite@0.3.7)(better-sqlite3@12.2.0)(drizzle-orm@0.44.4(@cloudflare/workers-types@4.20250812.0)(@electric-sql/pglite@0.3.7)(@opentelemetry/api@1.9.0)(better-sqlite3@12.2.0)(postgres@3.4.7)))(embla-carousel@8.6.0)(ioredis@5.7.0)(jwt-decode@4.0.0)(magicast@0.3.5)(typescript@5.9.2)(valibot@1.1.0(typescript@5.9.2))(vite@7.1.2(@types/node@22.17.1)(jiti@2.5.1)(lightningcss@1.30.1)(terser@5.43.1)(yaml@2.8.1))(vue-router@4.5.1(vue@3.5.18(typescript@5.9.2)))(vue@3.5.18(typescript@5.9.2))(zod@3.25.76)': + '@nuxt/ui@3.3.0(@babel/parser@7.28.0)(@netlify/blobs@9.1.2)(@vercel/blob@1.1.1)(aws4fetch@1.0.20)(change-case@5.4.4)(db0@0.3.2(@electric-sql/pglite@0.3.7)(better-sqlite3@12.2.0)(drizzle-orm@0.44.4(@cloudflare/workers-types@4.20250812.0)(@electric-sql/pglite@0.3.7)(@opentelemetry/api@1.9.0)(better-sqlite3@12.2.0)(postgres@3.4.7)))(embla-carousel@8.6.0)(ioredis@5.7.0)(jwt-decode@4.0.0)(magicast@0.3.5)(typescript@5.9.2)(valibot@1.1.0(typescript@5.9.2))(vite@7.1.2(@types/node@22.17.1)(jiti@2.5.1)(lightningcss@1.30.1)(terser@5.43.1)(yaml@2.8.1))(vue-router@4.5.1(vue@3.5.18(typescript@5.9.2)))(vue@3.5.18(typescript@5.9.2))(zod@3.25.76)': dependencies: '@iconify/vue': 5.0.0(vue@3.5.18(typescript@5.9.2)) '@internationalized/date': 3.8.2 '@internationalized/number': 3.6.4 - '@nuxt/fonts': 0.11.4(@netlify/blobs@9.1.2)(aws4fetch@1.0.20)(db0@0.3.2(@electric-sql/pglite@0.3.7)(better-sqlite3@12.2.0)(drizzle-orm@0.44.4(@cloudflare/workers-types@4.20250812.0)(@electric-sql/pglite@0.3.7)(@opentelemetry/api@1.9.0)(better-sqlite3@12.2.0)(postgres@3.4.7)))(ioredis@5.7.0)(magicast@0.3.5)(vite@7.1.2(@types/node@22.17.1)(jiti@2.5.1)(lightningcss@1.30.1)(terser@5.43.1)(yaml@2.8.1)) + '@nuxt/fonts': 0.11.4(@netlify/blobs@9.1.2)(@vercel/blob@1.1.1)(aws4fetch@1.0.20)(db0@0.3.2(@electric-sql/pglite@0.3.7)(better-sqlite3@12.2.0)(drizzle-orm@0.44.4(@cloudflare/workers-types@4.20250812.0)(@electric-sql/pglite@0.3.7)(@opentelemetry/api@1.9.0)(better-sqlite3@12.2.0)(postgres@3.4.7)))(ioredis@5.7.0)(magicast@0.3.5)(vite@7.1.2(@types/node@22.17.1)(jiti@2.5.1)(lightningcss@1.30.1)(terser@5.43.1)(yaml@2.8.1)) '@nuxt/icon': 1.15.0(magicast@0.3.5)(vite@7.1.2(@types/node@22.17.1)(jiti@2.5.1)(lightningcss@1.30.1)(terser@5.43.1)(yaml@2.8.1))(vue@3.5.18(typescript@5.9.2)) '@nuxt/kit': 4.0.3(magicast@0.3.5) '@nuxt/schema': 4.0.3 @@ -10461,6 +10574,14 @@ snapshots: '@uploadthing/mime-types@0.3.5': {} + '@vercel/blob@1.1.1': + dependencies: + async-retry: 1.3.3 + is-buffer: 2.0.5 + is-node-process: 1.2.0 + throttleit: 2.1.0 + undici: 5.29.0 + '@vercel/nft@0.29.4(rollup@4.46.2)': dependencies: '@mapbox/node-pre-gyp': 2.0.0 @@ -10732,13 +10853,13 @@ snapshots: '@vueuse/metadata@13.6.0': {} - '@vueuse/nuxt@13.6.0(magicast@0.3.5)(nuxt@3.18.1(@electric-sql/pglite@0.3.7)(@netlify/blobs@9.1.2)(@parcel/watcher@2.5.1)(@types/node@22.17.1)(@vue/compiler-sfc@3.5.18)(aws4fetch@1.0.20)(better-sqlite3@12.2.0)(db0@0.3.2(@electric-sql/pglite@0.3.7)(better-sqlite3@12.2.0)(drizzle-orm@0.44.4(@cloudflare/workers-types@4.20250812.0)(@electric-sql/pglite@0.3.7)(@opentelemetry/api@1.9.0)(better-sqlite3@12.2.0)(postgres@3.4.7)))(drizzle-orm@0.44.4(@cloudflare/workers-types@4.20250812.0)(@electric-sql/pglite@0.3.7)(@opentelemetry/api@1.9.0)(better-sqlite3@12.2.0)(postgres@3.4.7))(eslint@9.33.0(jiti@2.5.1))(ioredis@5.7.0)(lightningcss@1.30.1)(magicast@0.3.5)(optionator@0.9.4)(rollup@4.46.2)(terser@5.43.1)(typescript@5.9.2)(vite@7.1.2(@types/node@22.17.1)(jiti@2.5.1)(lightningcss@1.30.1)(terser@5.43.1)(yaml@2.8.1))(vue-tsc@3.0.5(typescript@5.9.2))(yaml@2.8.1))(vue@3.5.18(typescript@5.9.2))': + '@vueuse/nuxt@13.6.0(magicast@0.3.5)(nuxt@3.18.1(@electric-sql/pglite@0.3.7)(@netlify/blobs@9.1.2)(@parcel/watcher@2.5.1)(@types/node@22.17.1)(@vercel/blob@1.1.1)(@vue/compiler-sfc@3.5.18)(aws4fetch@1.0.20)(better-sqlite3@12.2.0)(db0@0.3.2(@electric-sql/pglite@0.3.7)(better-sqlite3@12.2.0)(drizzle-orm@0.44.4(@cloudflare/workers-types@4.20250812.0)(@electric-sql/pglite@0.3.7)(@opentelemetry/api@1.9.0)(better-sqlite3@12.2.0)(postgres@3.4.7)))(drizzle-orm@0.44.4(@cloudflare/workers-types@4.20250812.0)(@electric-sql/pglite@0.3.7)(@opentelemetry/api@1.9.0)(better-sqlite3@12.2.0)(postgres@3.4.7))(eslint@9.33.0(jiti@2.5.1))(ioredis@5.7.0)(lightningcss@1.30.1)(magicast@0.3.5)(optionator@0.9.4)(rollup@4.46.2)(terser@5.43.1)(typescript@5.9.2)(vite@7.1.2(@types/node@22.17.1)(jiti@2.5.1)(lightningcss@1.30.1)(terser@5.43.1)(yaml@2.8.1))(vue-tsc@3.0.5(typescript@5.9.2))(yaml@2.8.1))(vue@3.5.18(typescript@5.9.2))': dependencies: '@nuxt/kit': 4.0.3(magicast@0.3.5) '@vueuse/core': 13.6.0(vue@3.5.18(typescript@5.9.2)) '@vueuse/metadata': 13.6.0 local-pkg: 1.1.1 - nuxt: 3.18.1(@electric-sql/pglite@0.3.7)(@netlify/blobs@9.1.2)(@parcel/watcher@2.5.1)(@types/node@22.17.1)(@vue/compiler-sfc@3.5.18)(aws4fetch@1.0.20)(better-sqlite3@12.2.0)(db0@0.3.2(@electric-sql/pglite@0.3.7)(better-sqlite3@12.2.0)(drizzle-orm@0.44.4(@cloudflare/workers-types@4.20250812.0)(@electric-sql/pglite@0.3.7)(@opentelemetry/api@1.9.0)(better-sqlite3@12.2.0)(postgres@3.4.7)))(drizzle-orm@0.44.4(@cloudflare/workers-types@4.20250812.0)(@electric-sql/pglite@0.3.7)(@opentelemetry/api@1.9.0)(better-sqlite3@12.2.0)(postgres@3.4.7))(eslint@9.33.0(jiti@2.5.1))(ioredis@5.7.0)(lightningcss@1.30.1)(magicast@0.3.5)(optionator@0.9.4)(rollup@4.46.2)(terser@5.43.1)(typescript@5.9.2)(vite@7.1.2(@types/node@22.17.1)(jiti@2.5.1)(lightningcss@1.30.1)(terser@5.43.1)(yaml@2.8.1))(vue-tsc@3.0.5(typescript@5.9.2))(yaml@2.8.1) + nuxt: 3.18.1(@electric-sql/pglite@0.3.7)(@netlify/blobs@9.1.2)(@parcel/watcher@2.5.1)(@types/node@22.17.1)(@vercel/blob@1.1.1)(@vue/compiler-sfc@3.5.18)(aws4fetch@1.0.20)(better-sqlite3@12.2.0)(db0@0.3.2(@electric-sql/pglite@0.3.7)(better-sqlite3@12.2.0)(drizzle-orm@0.44.4(@cloudflare/workers-types@4.20250812.0)(@electric-sql/pglite@0.3.7)(@opentelemetry/api@1.9.0)(better-sqlite3@12.2.0)(postgres@3.4.7)))(drizzle-orm@0.44.4(@cloudflare/workers-types@4.20250812.0)(@electric-sql/pglite@0.3.7)(@opentelemetry/api@1.9.0)(better-sqlite3@12.2.0)(postgres@3.4.7))(eslint@9.33.0(jiti@2.5.1))(ioredis@5.7.0)(lightningcss@1.30.1)(magicast@0.3.5)(optionator@0.9.4)(rollup@4.46.2)(terser@5.43.1)(typescript@5.9.2)(vite@7.1.2(@types/node@22.17.1)(jiti@2.5.1)(lightningcss@1.30.1)(terser@5.43.1)(yaml@2.8.1))(vue-tsc@3.0.5(typescript@5.9.2))(yaml@2.8.1) vue: 3.5.18(typescript@5.9.2) transitivePeerDependencies: - magicast @@ -10896,6 +11017,10 @@ snapshots: '@babel/parser': 7.28.0 ast-kit: 2.1.2 + async-retry@1.3.3: + dependencies: + retry: 0.13.1 + async-sema@3.1.1: {} async@3.2.6: {} @@ -10953,6 +11078,7 @@ snapshots: dependencies: bindings: 1.5.0 prebuild-install: 7.1.3 + optional: true binary-extensions@2.3.0: {} @@ -10967,6 +11093,7 @@ snapshots: buffer: 5.7.1 inherits: 2.0.4 readable-stream: 3.6.2 + optional: true blake3-wasm@2.1.5: {} @@ -11135,7 +11262,8 @@ snapshots: dependencies: readdirp: 4.1.2 - chownr@1.1.4: {} + chownr@1.1.4: + optional: true chownr@3.0.0: {} @@ -11450,10 +11578,12 @@ snapshots: decompress-response@6.0.0: dependencies: mimic-response: 3.1.0 + optional: true deep-eql@5.0.2: {} - deep-extend@0.6.0: {} + deep-extend@0.6.0: + optional: true deep-is@0.1.4: {} @@ -12039,7 +12169,8 @@ snapshots: exit-hook@2.2.1: {} - expand-template@2.0.3: {} + expand-template@2.0.3: + optional: true expect-type@1.2.2: {} @@ -12203,7 +12334,8 @@ snapshots: fresh@2.0.0: {} - fs-constants@1.0.0: {} + fs-constants@1.0.0: + optional: true fsevents@2.3.3: optional: true @@ -12282,7 +12414,8 @@ snapshots: dependencies: git-up: 8.1.1 - github-from-package@0.0.0: {} + github-from-package@0.0.0: + optional: true github-slugger@2.0.0: {} @@ -12577,7 +12710,8 @@ snapshots: inherits@2.0.4: {} - ini@1.3.8: {} + ini@1.3.8: + optional: true ini@4.1.1: {} @@ -12597,7 +12731,7 @@ snapshots: ip-address@10.0.1: {} - ipx@2.1.1(@netlify/blobs@9.1.2)(aws4fetch@1.0.20)(db0@0.3.2(@electric-sql/pglite@0.3.7)(better-sqlite3@12.2.0)(drizzle-orm@0.44.4(@cloudflare/workers-types@4.20250812.0)(@electric-sql/pglite@0.3.7)(@opentelemetry/api@1.9.0)(better-sqlite3@12.2.0)(postgres@3.4.7)))(ioredis@5.7.0): + ipx@2.1.1(@netlify/blobs@9.1.2)(@vercel/blob@1.1.1)(aws4fetch@1.0.20)(db0@0.3.2(@electric-sql/pglite@0.3.7)(better-sqlite3@12.2.0)(drizzle-orm@0.44.4(@cloudflare/workers-types@4.20250812.0)(@electric-sql/pglite@0.3.7)(@opentelemetry/api@1.9.0)(better-sqlite3@12.2.0)(postgres@3.4.7)))(ioredis@5.7.0): dependencies: '@fastify/accept-negotiator': 1.1.0 citty: 0.1.6 @@ -12613,7 +12747,7 @@ snapshots: sharp: 0.32.6 svgo: 3.3.2 ufo: 1.6.1 - unstorage: 1.16.1(@netlify/blobs@9.1.2)(aws4fetch@1.0.20)(db0@0.3.2(@electric-sql/pglite@0.3.7)(better-sqlite3@12.2.0)(drizzle-orm@0.44.4(@cloudflare/workers-types@4.20250812.0)(@electric-sql/pglite@0.3.7)(@opentelemetry/api@1.9.0)(better-sqlite3@12.2.0)(postgres@3.4.7)))(ioredis@5.7.0) + unstorage: 1.16.1(@netlify/blobs@9.1.2)(@vercel/blob@1.1.1)(aws4fetch@1.0.20)(db0@0.3.2(@electric-sql/pglite@0.3.7)(better-sqlite3@12.2.0)(drizzle-orm@0.44.4(@cloudflare/workers-types@4.20250812.0)(@electric-sql/pglite@0.3.7)(@opentelemetry/api@1.9.0)(better-sqlite3@12.2.0)(postgres@3.4.7)))(ioredis@5.7.0) xss: 1.0.15 transitivePeerDependencies: - '@azure/app-configuration' @@ -12656,6 +12790,8 @@ snapshots: dependencies: binary-extensions: 2.3.0 + is-buffer@2.0.5: {} + is-builtin-module@3.2.1: dependencies: builtin-modules: 3.3.0 @@ -12695,6 +12831,8 @@ snapshots: is-module@1.0.0: {} + is-node-process@1.2.0: {} + is-number@7.0.0: {} is-path-inside@4.0.0: {} @@ -12931,6 +13069,12 @@ snapshots: pkg-types: 2.2.0 quansync: 0.2.10 + local-pkg@1.1.2: + dependencies: + mlly: 1.7.4 + pkg-types: 2.3.0 + quansync: 0.2.11 + locate-path@6.0.0: dependencies: p-locate: 5.0.0 @@ -13347,7 +13491,8 @@ snapshots: mimic-fn@4.0.0: {} - mimic-response@3.1.0: {} + mimic-response@3.1.0: + optional: true min-indent@1.0.1: {} @@ -13397,7 +13542,8 @@ snapshots: mitt@3.0.1: {} - mkdirp-classic@0.5.3: {} + mkdirp-classic@0.5.3: + optional: true mkdirp@3.0.1: {} @@ -13468,7 +13614,8 @@ snapshots: nanotar@0.2.0: {} - napi-build-utils@2.0.0: {} + napi-build-utils@2.0.0: + optional: true napi-postinstall@0.3.3: {} @@ -13491,7 +13638,7 @@ snapshots: mlly: 1.7.4 pkg-types: 2.2.0 - nitropack@2.12.4(@electric-sql/pglite@0.3.7)(@netlify/blobs@9.1.2)(aws4fetch@1.0.20)(better-sqlite3@12.2.0)(drizzle-orm@0.44.4(@cloudflare/workers-types@4.20250812.0)(@electric-sql/pglite@0.3.7)(@opentelemetry/api@1.9.0)(better-sqlite3@12.2.0)(postgres@3.4.7)): + nitropack@2.12.4(@electric-sql/pglite@0.3.7)(@netlify/blobs@9.1.2)(@vercel/blob@1.1.1)(aws4fetch@1.0.20)(better-sqlite3@12.2.0)(drizzle-orm@0.44.4(@cloudflare/workers-types@4.20250812.0)(@electric-sql/pglite@0.3.7)(@opentelemetry/api@1.9.0)(better-sqlite3@12.2.0)(postgres@3.4.7)): dependencies: '@cloudflare/kv-asset-handler': 0.4.0 '@netlify/functions': 3.1.10(rollup@4.46.2) @@ -13559,7 +13706,7 @@ snapshots: unenv: 2.0.0-rc.19 unimport: 5.2.0 unplugin-utils: 0.2.5 - unstorage: 1.16.1(@netlify/blobs@9.1.2)(aws4fetch@1.0.20)(db0@0.3.2(@electric-sql/pglite@0.3.7)(better-sqlite3@12.2.0)(drizzle-orm@0.44.4(@cloudflare/workers-types@4.20250812.0)(@electric-sql/pglite@0.3.7)(@opentelemetry/api@1.9.0)(better-sqlite3@12.2.0)(postgres@3.4.7)))(ioredis@5.7.0) + unstorage: 1.16.1(@netlify/blobs@9.1.2)(@vercel/blob@1.1.1)(aws4fetch@1.0.20)(db0@0.3.2(@electric-sql/pglite@0.3.7)(better-sqlite3@12.2.0)(drizzle-orm@0.44.4(@cloudflare/workers-types@4.20250812.0)(@electric-sql/pglite@0.3.7)(@opentelemetry/api@1.9.0)(better-sqlite3@12.2.0)(postgres@3.4.7)))(ioredis@5.7.0) untyped: 2.0.0 unwasm: 0.3.9 youch: 4.1.0-beta.8 @@ -13594,6 +13741,7 @@ snapshots: node-abi@3.75.0: dependencies: semver: 7.7.2 + optional: true node-addon-api@6.1.0: optional: true @@ -13709,7 +13857,7 @@ snapshots: transitivePeerDependencies: - magicast - nuxt-og-image@5.1.9(@unhead/vue@2.0.14(vue@3.5.18(typescript@5.9.2)))(magicast@0.3.5)(unstorage@1.16.1(@netlify/blobs@9.1.2)(aws4fetch@1.0.20)(db0@0.3.2(@electric-sql/pglite@0.3.7)(better-sqlite3@12.2.0)(drizzle-orm@0.44.4(@cloudflare/workers-types@4.20250812.0)(@electric-sql/pglite@0.3.7)(@opentelemetry/api@1.9.0)(better-sqlite3@12.2.0)(postgres@3.4.7)))(ioredis@5.7.0))(vite@7.1.2(@types/node@22.17.1)(jiti@2.5.1)(lightningcss@1.30.1)(terser@5.43.1)(yaml@2.8.1))(vue@3.5.18(typescript@5.9.2)): + nuxt-og-image@5.1.9(@unhead/vue@2.0.14(vue@3.5.18(typescript@5.9.2)))(magicast@0.3.5)(unstorage@1.16.1(@netlify/blobs@9.1.2)(@vercel/blob@1.1.1)(aws4fetch@1.0.20)(db0@0.3.2(@electric-sql/pglite@0.3.7)(better-sqlite3@12.2.0)(drizzle-orm@0.44.4(@cloudflare/workers-types@4.20250812.0)(@electric-sql/pglite@0.3.7)(@opentelemetry/api@1.9.0)(better-sqlite3@12.2.0)(postgres@3.4.7)))(ioredis@5.7.0))(vite@7.1.2(@types/node@22.17.1)(jiti@2.5.1)(lightningcss@1.30.1)(terser@5.43.1)(yaml@2.8.1))(vue@3.5.18(typescript@5.9.2)): dependencies: '@nuxt/devtools-kit': 2.6.2(magicast@0.3.5)(vite@7.1.2(@types/node@22.17.1)(jiti@2.5.1)(lightningcss@1.30.1)(terser@5.43.1)(yaml@2.8.1)) '@nuxt/kit': 3.18.1(magicast@0.3.5) @@ -13740,7 +13888,7 @@ snapshots: strip-literal: 3.0.0 ufo: 1.6.1 unplugin: 2.3.5 - unstorage: 1.16.1(@netlify/blobs@9.1.2)(aws4fetch@1.0.20)(db0@0.3.2(@electric-sql/pglite@0.3.7)(better-sqlite3@12.2.0)(drizzle-orm@0.44.4(@cloudflare/workers-types@4.20250812.0)(@electric-sql/pglite@0.3.7)(@opentelemetry/api@1.9.0)(better-sqlite3@12.2.0)(postgres@3.4.7)))(ioredis@5.7.0) + unstorage: 1.16.1(@netlify/blobs@9.1.2)(@vercel/blob@1.1.1)(aws4fetch@1.0.20)(db0@0.3.2(@electric-sql/pglite@0.3.7)(better-sqlite3@12.2.0)(drizzle-orm@0.44.4(@cloudflare/workers-types@4.20250812.0)(@electric-sql/pglite@0.3.7)(@opentelemetry/api@1.9.0)(better-sqlite3@12.2.0)(postgres@3.4.7)))(ioredis@5.7.0) unwasm: 0.3.9 yoga-wasm-web: 0.3.3 transitivePeerDependencies: @@ -13773,7 +13921,7 @@ snapshots: - magicast - vue - nuxt@3.18.1(@electric-sql/pglite@0.3.7)(@netlify/blobs@9.1.2)(@parcel/watcher@2.5.1)(@types/node@22.17.1)(@vue/compiler-sfc@3.5.18)(aws4fetch@1.0.20)(better-sqlite3@12.2.0)(db0@0.3.2(@electric-sql/pglite@0.3.7)(better-sqlite3@12.2.0)(drizzle-orm@0.44.4(@cloudflare/workers-types@4.20250812.0)(@electric-sql/pglite@0.3.7)(@opentelemetry/api@1.9.0)(better-sqlite3@12.2.0)(postgres@3.4.7)))(drizzle-orm@0.44.4(@cloudflare/workers-types@4.20250812.0)(@electric-sql/pglite@0.3.7)(@opentelemetry/api@1.9.0)(better-sqlite3@12.2.0)(postgres@3.4.7))(eslint@9.33.0(jiti@2.5.1))(ioredis@5.7.0)(lightningcss@1.30.1)(magicast@0.3.5)(optionator@0.9.4)(rollup@4.46.2)(terser@5.43.1)(typescript@5.9.2)(vite@7.1.2(@types/node@22.17.1)(jiti@2.5.1)(lightningcss@1.30.1)(terser@5.43.1)(yaml@2.8.1))(vue-tsc@3.0.5(typescript@5.9.2))(yaml@2.8.1): + nuxt@3.18.1(@electric-sql/pglite@0.3.7)(@netlify/blobs@9.1.2)(@parcel/watcher@2.5.1)(@types/node@22.17.1)(@vercel/blob@1.1.1)(@vue/compiler-sfc@3.5.18)(aws4fetch@1.0.20)(better-sqlite3@12.2.0)(db0@0.3.2(@electric-sql/pglite@0.3.7)(better-sqlite3@12.2.0)(drizzle-orm@0.44.4(@cloudflare/workers-types@4.20250812.0)(@electric-sql/pglite@0.3.7)(@opentelemetry/api@1.9.0)(better-sqlite3@12.2.0)(postgres@3.4.7)))(drizzle-orm@0.44.4(@cloudflare/workers-types@4.20250812.0)(@electric-sql/pglite@0.3.7)(@opentelemetry/api@1.9.0)(better-sqlite3@12.2.0)(postgres@3.4.7))(eslint@9.33.0(jiti@2.5.1))(ioredis@5.7.0)(lightningcss@1.30.1)(magicast@0.3.5)(optionator@0.9.4)(rollup@4.46.2)(terser@5.43.1)(typescript@5.9.2)(vite@7.1.2(@types/node@22.17.1)(jiti@2.5.1)(lightningcss@1.30.1)(terser@5.43.1)(yaml@2.8.1))(vue-tsc@3.0.5(typescript@5.9.2))(yaml@2.8.1): dependencies: '@nuxt/cli': 3.28.0(magicast@0.3.5) '@nuxt/devalue': 2.0.2 @@ -13808,7 +13956,7 @@ snapshots: mlly: 1.7.4 mocked-exports: 0.1.1 nanotar: 0.2.0 - nitropack: 2.12.4(@electric-sql/pglite@0.3.7)(@netlify/blobs@9.1.2)(aws4fetch@1.0.20)(better-sqlite3@12.2.0)(drizzle-orm@0.44.4(@cloudflare/workers-types@4.20250812.0)(@electric-sql/pglite@0.3.7)(@opentelemetry/api@1.9.0)(better-sqlite3@12.2.0)(postgres@3.4.7)) + nitropack: 2.12.4(@electric-sql/pglite@0.3.7)(@netlify/blobs@9.1.2)(@vercel/blob@1.1.1)(aws4fetch@1.0.20)(better-sqlite3@12.2.0)(drizzle-orm@0.44.4(@cloudflare/workers-types@4.20250812.0)(@electric-sql/pglite@0.3.7)(@opentelemetry/api@1.9.0)(better-sqlite3@12.2.0)(postgres@3.4.7)) nypm: 0.6.1 ofetch: 1.4.1 ohash: 2.0.11 @@ -13833,7 +13981,7 @@ snapshots: unimport: 5.2.0 unplugin: 2.3.5 unplugin-vue-router: 0.15.0(@vue/compiler-sfc@3.5.18)(typescript@5.9.2)(vue-router@4.5.1(vue@3.5.18(typescript@5.9.2)))(vue@3.5.18(typescript@5.9.2)) - unstorage: 1.16.1(@netlify/blobs@9.1.2)(aws4fetch@1.0.20)(db0@0.3.2(@electric-sql/pglite@0.3.7)(better-sqlite3@12.2.0)(drizzle-orm@0.44.4(@cloudflare/workers-types@4.20250812.0)(@electric-sql/pglite@0.3.7)(@opentelemetry/api@1.9.0)(better-sqlite3@12.2.0)(postgres@3.4.7)))(ioredis@5.7.0) + unstorage: 1.16.1(@netlify/blobs@9.1.2)(@vercel/blob@1.1.1)(aws4fetch@1.0.20)(db0@0.3.2(@electric-sql/pglite@0.3.7)(better-sqlite3@12.2.0)(drizzle-orm@0.44.4(@cloudflare/workers-types@4.20250812.0)(@electric-sql/pglite@0.3.7)(@opentelemetry/api@1.9.0)(better-sqlite3@12.2.0)(postgres@3.4.7)))(ioredis@5.7.0) untyped: 2.0.0 vue: 3.5.18(typescript@5.9.2) vue-bundle-renderer: 2.1.2 @@ -13896,7 +14044,7 @@ snapshots: - xml2js - yaml - nuxt@4.0.3(@electric-sql/pglite@0.3.7)(@netlify/blobs@9.1.2)(@parcel/watcher@2.5.1)(@types/node@22.17.1)(@vue/compiler-sfc@3.5.18)(aws4fetch@1.0.20)(better-sqlite3@12.2.0)(db0@0.3.2(@electric-sql/pglite@0.3.7)(better-sqlite3@12.2.0)(drizzle-orm@0.44.4(@cloudflare/workers-types@4.20250812.0)(@electric-sql/pglite@0.3.7)(@opentelemetry/api@1.9.0)(better-sqlite3@12.2.0)(postgres@3.4.7)))(drizzle-orm@0.44.4(@cloudflare/workers-types@4.20250812.0)(@electric-sql/pglite@0.3.7)(@opentelemetry/api@1.9.0)(better-sqlite3@12.2.0)(postgres@3.4.7))(eslint@9.33.0(jiti@2.5.1))(ioredis@5.7.0)(lightningcss@1.30.1)(magicast@0.3.5)(optionator@0.9.4)(rollup@4.46.2)(terser@5.43.1)(typescript@5.9.2)(vite@7.1.2(@types/node@22.17.1)(jiti@2.5.1)(lightningcss@1.30.1)(terser@5.43.1)(yaml@2.8.1))(vue-tsc@3.0.5(typescript@5.9.2))(yaml@2.8.1): + nuxt@4.0.3(@electric-sql/pglite@0.3.7)(@netlify/blobs@9.1.2)(@parcel/watcher@2.5.1)(@types/node@22.17.1)(@vercel/blob@1.1.1)(@vue/compiler-sfc@3.5.18)(aws4fetch@1.0.20)(better-sqlite3@12.2.0)(db0@0.3.2(@electric-sql/pglite@0.3.7)(better-sqlite3@12.2.0)(drizzle-orm@0.44.4(@cloudflare/workers-types@4.20250812.0)(@electric-sql/pglite@0.3.7)(@opentelemetry/api@1.9.0)(better-sqlite3@12.2.0)(postgres@3.4.7)))(drizzle-orm@0.44.4(@cloudflare/workers-types@4.20250812.0)(@electric-sql/pglite@0.3.7)(@opentelemetry/api@1.9.0)(better-sqlite3@12.2.0)(postgres@3.4.7))(eslint@9.33.0(jiti@2.5.1))(ioredis@5.7.0)(lightningcss@1.30.1)(magicast@0.3.5)(optionator@0.9.4)(rollup@4.46.2)(terser@5.43.1)(typescript@5.9.2)(vite@7.1.2(@types/node@22.17.1)(jiti@2.5.1)(lightningcss@1.30.1)(terser@5.43.1)(yaml@2.8.1))(vue-tsc@3.0.5(typescript@5.9.2))(yaml@2.8.1): dependencies: '@nuxt/cli': 3.28.0(magicast@0.3.5) '@nuxt/devalue': 2.0.2 @@ -13931,7 +14079,7 @@ snapshots: mlly: 1.7.4 mocked-exports: 0.1.1 nanotar: 0.2.0 - nitropack: 2.12.4(@electric-sql/pglite@0.3.7)(@netlify/blobs@9.1.2)(aws4fetch@1.0.20)(better-sqlite3@12.2.0)(drizzle-orm@0.44.4(@cloudflare/workers-types@4.20250812.0)(@electric-sql/pglite@0.3.7)(@opentelemetry/api@1.9.0)(better-sqlite3@12.2.0)(postgres@3.4.7)) + nitropack: 2.12.4(@electric-sql/pglite@0.3.7)(@netlify/blobs@9.1.2)(@vercel/blob@1.1.1)(aws4fetch@1.0.20)(better-sqlite3@12.2.0)(drizzle-orm@0.44.4(@cloudflare/workers-types@4.20250812.0)(@electric-sql/pglite@0.3.7)(@opentelemetry/api@1.9.0)(better-sqlite3@12.2.0)(postgres@3.4.7)) nypm: 0.6.1 ofetch: 1.4.1 ohash: 2.0.11 @@ -13956,7 +14104,7 @@ snapshots: unimport: 5.2.0 unplugin: 2.3.5 unplugin-vue-router: 0.15.0(@vue/compiler-sfc@3.5.18)(typescript@5.9.2)(vue-router@4.5.1(vue@3.5.18(typescript@5.9.2)))(vue@3.5.18(typescript@5.9.2)) - unstorage: 1.16.1(@netlify/blobs@9.1.2)(aws4fetch@1.0.20)(db0@0.3.2(@electric-sql/pglite@0.3.7)(better-sqlite3@12.2.0)(drizzle-orm@0.44.4(@cloudflare/workers-types@4.20250812.0)(@electric-sql/pglite@0.3.7)(@opentelemetry/api@1.9.0)(better-sqlite3@12.2.0)(postgres@3.4.7)))(ioredis@5.7.0) + unstorage: 1.16.1(@netlify/blobs@9.1.2)(@vercel/blob@1.1.1)(aws4fetch@1.0.20)(db0@0.3.2(@electric-sql/pglite@0.3.7)(better-sqlite3@12.2.0)(drizzle-orm@0.44.4(@cloudflare/workers-types@4.20250812.0)(@electric-sql/pglite@0.3.7)(@opentelemetry/api@1.9.0)(better-sqlite3@12.2.0)(postgres@3.4.7)))(ioredis@5.7.0) untyped: 2.0.0 vue: 3.5.18(typescript@5.9.2) vue-bundle-renderer: 2.1.2 @@ -14316,6 +14464,12 @@ snapshots: exsolve: 1.0.7 pathe: 2.0.3 + pkg-types@2.3.0: + dependencies: + confbox: 0.2.2 + exsolve: 1.0.7 + pathe: 2.0.3 + playwright-core@1.54.2: {} pluralize@8.0.0: {} @@ -14515,6 +14669,7 @@ snapshots: simple-get: 4.0.1 tar-fs: 2.1.3 tunnel-agent: 0.6.0 + optional: true precinct@12.2.0: dependencies: @@ -14622,6 +14777,8 @@ snapshots: quansync@0.2.10: {} + quansync@0.2.11: {} + queue-microtask@1.2.3: {} quote-unquote@1.0.0: {} @@ -14645,6 +14802,7 @@ snapshots: ini: 1.3.8 minimist: 1.2.8 strip-json-comments: 2.0.1 + optional: true react@19.1.1: {} @@ -14882,6 +15040,8 @@ snapshots: restructure@3.0.2: {} + retry@0.13.1: {} + reusify@1.1.0: {} rfdc@1.4.1: {} @@ -15103,13 +15263,15 @@ snapshots: signal-exit@4.1.0: {} - simple-concat@1.0.1: {} + simple-concat@1.0.1: + optional: true simple-get@4.0.1: dependencies: decompress-response: 6.0.0 once: 1.4.0 simple-concat: 1.0.1 + optional: true simple-git@3.28.0: dependencies: @@ -15279,7 +15441,8 @@ snapshots: dependencies: min-indent: 1.0.1 - strip-json-comments@2.0.1: {} + strip-json-comments@2.0.1: + optional: true strip-json-comments@3.1.1: {} @@ -15357,6 +15520,7 @@ snapshots: mkdirp-classic: 0.5.3 pump: 3.0.3 tar-stream: 2.2.0 + optional: true tar-fs@3.1.0: dependencies: @@ -15375,6 +15539,7 @@ snapshots: fs-constants: 1.0.0 inherits: 2.0.4 readable-stream: 3.6.2 + optional: true tar-stream@3.1.7: dependencies: @@ -15472,6 +15637,7 @@ snapshots: tunnel-agent@0.6.0: dependencies: safe-buffer: 5.2.1 + optional: true type-check@0.4.0: dependencies: @@ -15539,6 +15705,10 @@ snapshots: undici-types@6.21.0: {} + undici@5.29.0: + dependencies: + '@fastify/busboy': 2.1.1 + undici@7.13.0: {} unenv@2.0.0-rc.19: @@ -15748,7 +15918,7 @@ snapshots: '@unrs/resolver-binding-win32-ia32-msvc': 1.11.1 '@unrs/resolver-binding-win32-x64-msvc': 1.11.1 - unstorage@1.16.1(@netlify/blobs@9.1.2)(aws4fetch@1.0.20)(db0@0.3.2(@electric-sql/pglite@0.3.7)(better-sqlite3@12.2.0)(drizzle-orm@0.44.4(@cloudflare/workers-types@4.20250812.0)(@electric-sql/pglite@0.3.7)(@opentelemetry/api@1.9.0)(better-sqlite3@12.2.0)(postgres@3.4.7)))(ioredis@5.7.0): + unstorage@1.16.1(@netlify/blobs@9.1.2)(@vercel/blob@1.1.1)(aws4fetch@1.0.20)(db0@0.3.2(@electric-sql/pglite@0.3.7)(better-sqlite3@12.2.0)(drizzle-orm@0.44.4(@cloudflare/workers-types@4.20250812.0)(@electric-sql/pglite@0.3.7)(@opentelemetry/api@1.9.0)(better-sqlite3@12.2.0)(postgres@3.4.7)))(ioredis@5.7.0): dependencies: anymatch: 3.1.3 chokidar: 4.0.3 @@ -15760,6 +15930,7 @@ snapshots: ufo: 1.6.1 optionalDependencies: '@netlify/blobs': 9.1.2 + '@vercel/blob': 1.1.1 aws4fetch: 1.0.20 db0: 0.3.2(@electric-sql/pglite@0.3.7)(better-sqlite3@12.2.0)(drizzle-orm@0.44.4(@cloudflare/workers-types@4.20250812.0)(@electric-sql/pglite@0.3.7)(@opentelemetry/api@1.9.0)(better-sqlite3@12.2.0)(postgres@3.4.7)) ioredis: 5.7.0 diff --git a/src/runtime/blob/app/composables/useMultipartUpload.ts b/src/runtime/blob/app/composables/useMultipartUpload.ts index aa6d4789..e8062ee3 100644 --- a/src/runtime/blob/app/composables/useMultipartUpload.ts +++ b/src/runtime/blob/app/composables/useMultipartUpload.ts @@ -19,7 +19,7 @@ export function useMultipartUpload( fetchOptions, prefix } = defu(options, { - partSize: 10 * 1024 * 1024, // 10MB + partSize: 4 * 1024 * 1024, // 10MB concurrent: 1, // no concurrent upload by default maxRetry: 3 }) diff --git a/src/runtime/blob/server/helpers/blob-cloudflare.ts b/src/runtime/blob/server/helpers/blob-cloudflare.ts new file mode 100644 index 00000000..f393f40e --- /dev/null +++ b/src/runtime/blob/server/helpers/blob-cloudflare.ts @@ -0,0 +1,59 @@ +import mime from 'mime' +import type { R2Bucket, R2Object } from '@cloudflare/workers-types/experimental' +import type { BlobMultipartOptions, BlobMultipartUpload, BlobObject, BlobUploadedPart } from '@nuxthub/core' + +export async function createMultipartUpload(bucket: R2Bucket, pathname: string, options?: BlobMultipartOptions): Promise { + const mpu = await bucket.createMultipartUpload(pathname!, { + httpMetadata: { + contentType: options?.contentType || 'application/octet-stream' + }, + customMetadata: options?.customMetadata || {} + }) + return { + pathname, + uploadId: mpu.uploadId, + uploadPart(partNumber, value) { + return mpu.uploadPart(partNumber, value as any) as Promise + }, + abort: async () => { + await mpu.abort() + }, + complete: async (uploadedParts) => { + const r2Object = await mpu.complete(uploadedParts) + return mapR2ObjectToBlob(r2Object) + } + } +} +export async function resumeMultipartUpload(bucket: R2Bucket, pathname: string, uploadId: string): Promise { + const mpu = bucket.resumeMultipartUpload(pathname!, uploadId) + return { + pathname, + uploadId: mpu.uploadId, + uploadPart(partNumber, value) { + return mpu.uploadPart(partNumber, value as any) as Promise + }, + abort: async () => { + await mpu.abort() + }, + complete: async (uploadedParts) => { + const r2Object = await mpu.complete(uploadedParts) + return mapR2ObjectToBlob(r2Object) + } + } +} + +function getContentType(pathOrExtension?: string) { + return (pathOrExtension && mime.getType(pathOrExtension)) || 'application/octet-stream' +} + +function mapR2ObjectToBlob(object: R2Object): BlobObject { + return { + pathname: object.key, + contentType: object.httpMetadata?.contentType || getContentType(object.key), + size: object.size, + httpEtag: object.httpEtag, + uploadedAt: object.uploaded, + httpMetadata: object.httpMetadata || {}, + customMetadata: object.customMetadata || {} + } +} diff --git a/src/runtime/blob/server/helpers/blob-fs.ts b/src/runtime/blob/server/helpers/blob-fs.ts new file mode 100644 index 00000000..e2ce9df2 --- /dev/null +++ b/src/runtime/blob/server/helpers/blob-fs.ts @@ -0,0 +1,131 @@ +import type { BlobMultipartUpload, BlobObject, BlobUploadedPart } from '~/src/types/blob' +import { randomUUID } from 'uncrypto' +import { join } from 'pathe' +import fs from 'node:fs' +import fsp from 'node:fs/promises' +import type { Driver } from 'unstorage' + +export async function createMultipartUpload(driver: Driver, pathname: string, _metadata?: Record) { + const uploadId = randomUUID() + return createOrResumeMultipartUpload(driver, pathname, uploadId, _metadata) +} + +export async function resumeMultipartUpload(driver: Driver, pathname: string, uploadId: string) { + return createOrResumeMultipartUpload(driver, pathname, uploadId) +} + +async function createOrResumeMultipartUpload(driver: Driver, pathname: string, uploadId: string, _metadata?: Record): Promise { + if (!_metadata) { + _metadata = (driver.getMeta ? await driver.getMeta!(pathname, {}) : await driver.getItem(pathname + '$', {})) as Record + + if (!_metadata) { + throw new Error('Metadata not found') + } + } + + const currentMetadata = { + pathname, + uploadId, + ..._metadata, + contentType: _metadata?.contentType as string || 'application/octet-stream', + customMetadata: _metadata?.customMetadata as Record || {}, + parts: new Map(_metadata?.parts as Array || []) + } + + return { + pathname, + uploadId, + async uploadPart(partNumber: number, value): Promise { + // Convert value to Uint8Array for binary storage - preserve binary data integrity + let processedBody: string | ReadableStream | ArrayBuffer | ArrayBufferView = value as any + if (value instanceof Blob) { + const arrayBuffer = await value.arrayBuffer() + processedBody = new Uint8Array(arrayBuffer) + } + + await driver.setItemRaw!(partKey(uploadId, partNumber), processedBody, {}) + + const uploadedPart: BlobUploadedPart = { + partNumber, + etag: `"${randomUUID()}"` + } + + // Update metadata + currentMetadata.parts.set(partNumber, uploadedPart) + await driver.setItem!(pathname + '$', JSON.stringify({ + ...currentMetadata, + parts: Array.from(currentMetadata.parts.entries()) + }), {}) + + return uploadedPart + }, + async complete(uploadedParts: BlobUploadedPart[]): Promise { + // Validate parts + if (uploadedParts.length === 0) { + throw new Error('No parts provided for completion') + } + + // Sort parts by part number + uploadedParts.sort((a, b) => a.partNumber - b.partNumber) + + // Check for missing parts + const expectedPartNumbers = Array.from({ length: uploadedParts.length }, (_, i) => i + 1) + const actualPartNumbers = uploadedParts.map(p => p.partNumber) + + for (const expectedPart of expectedPartNumbers) { + if (!actualPartNumbers.includes(expectedPart)) { + throw new Error(`Missing part ${expectedPart}`) + } + } + + await completeUpload(driver, pathname, uploadId, uploadedParts) + await cleanUploadTmpFiles(driver, uploadId, pathname) + + return { + pathname: currentMetadata.pathname, + contentType: currentMetadata.contentType, + size: 0, // TODO: get size from uploaded parts + httpEtag: `"${randomUUID()}"`, + uploadedAt: new Date(), + httpMetadata: { + contentType: currentMetadata.contentType + }, + customMetadata: currentMetadata.customMetadata || {} + } + }, + async abort(): Promise { + await cleanUploadTmpFiles(driver, uploadId, pathname) + } + } +} + +function partKey(uploadId: string, partNumber: number) { + return `${uploadId}/${partNumber.toString().padStart(10, '0')}$` +} + +async function cleanUploadTmpFiles(driver: Driver, uploadId: string, pathname: string) { + const root = driver.options.base + const fullPath = join(root, uploadId) + await fsp.rm(fullPath, { recursive: true, force: true }) + await driver.removeItem!(pathname + '$', {}) +} + +async function completeUpload(driver: Driver, pathname: string, uploadId: string, uploadedParts: BlobUploadedPart[]) { + const root = driver.options.base + const fullPath = join(root, pathname) + const orderedUploadedParts = uploadedParts.sort((a, b) => a.partNumber - b.partNumber) + + const writeStream = fs.createWriteStream(fullPath) + for (const file of orderedUploadedParts) { + await new Promise((resolve, reject) => { + const readStream = fs.createReadStream(join(root, partKey(uploadId, file.partNumber))) + readStream.pipe(writeStream, { end: false }) + readStream.on('error', reject) + readStream.on('end', resolve as () => void) + }) + } + + writeStream.end() + writeStream.on('finish', () => { + }) +} diff --git a/src/runtime/blob/server/helpers/blob-vercel.ts b/src/runtime/blob/server/helpers/blob-vercel.ts new file mode 100644 index 00000000..12ac21fe --- /dev/null +++ b/src/runtime/blob/server/helpers/blob-vercel.ts @@ -0,0 +1,78 @@ +import type { PutBlobResult } from '@vercel/blob' +import { createMultipartUpload as vercelCreateMultipartUpload, uploadPart as vercelUploadPart, completeMultipartUpload as vercelCompleteMultipartUpload } from '@vercel/blob' +import type { BlobMultipartOptions, BlobMultipartUpload, BlobObject } from '@nuxthub/core' + +export async function createMultipartUpload(token: string, pathname: string, options?: BlobMultipartOptions): Promise { + const { key, uploadId } = await vercelCreateMultipartUpload(pathname, { + access: 'public', + token, + contentType: options?.contentType || 'application/octet-stream' + }) + return { + pathname, + uploadId: uploadId, + uploadPart(partNumber, value) { + return vercelUploadPart(pathname, value as any, { + access: 'public', + token, + contentType: options?.contentType || 'application/octet-stream', + uploadId, + key, + partNumber + }) + }, + abort: async () => { + // await mpu.abort() + }, + complete: async (uploadedParts) => { + const r2Object = await vercelCompleteMultipartUpload(pathname, uploadedParts, { + access: 'public', + token, + contentType: options?.contentType || 'application/octet-stream', + uploadId, + key + }) + return mapR2ObjectToBlob(r2Object) + } + } +} +export async function resumeMultipartUpload(token: string, pathname: string, uploadId: string): Promise { + return { + pathname, + uploadId: uploadId, + uploadPart(partNumber, value) { + return vercelUploadPart(pathname, value as any, { + access: 'public', + token, + uploadId, + partNumber, + key: pathname + }) + }, + abort: async () => { + // await mpu.abort() + }, + complete: async (uploadedParts) => { + const putBlobResult = await vercelCompleteMultipartUpload(pathname, uploadedParts, { + access: 'public', + token, + uploadId, + key: pathname + }) + return mapR2ObjectToBlob(putBlobResult) + } + } +} + +function mapR2ObjectToBlob(object: PutBlobResult): BlobObject { + return { + pathname: object.pathname, + url: object.url, + contentType: object.contentType, + size: 0, // TODO: get size + httpEtag: '', // TODO: get etag + uploadedAt: new Date(), + httpMetadata: {}, + customMetadata: {} + } +} diff --git a/src/runtime/blob/server/helpers/multipart-storage.ts b/src/runtime/blob/server/helpers/multipart-storage.ts new file mode 100644 index 00000000..b2f3bb26 --- /dev/null +++ b/src/runtime/blob/server/helpers/multipart-storage.ts @@ -0,0 +1,64 @@ +import type { Driver, Storage } from 'unstorage' +import type { BlobMultipartOptions } from '@nuxthub/core' + +export function multiPartBlobStorage(storage: Storage, mountPoint: string) { + const driver = getMultiPartDriver(storage.getMount(mountPoint).driver) + return { + ...storage, + createMultipartUpload: async (pathname: string, options?: BlobMultipartOptions) => { + return driver.createMultipartUpload(pathname, options) + }, + resumeMultipartUpload: async (pathname: string, uploadId: string) => { + return driver.resumeMultipartUpload(pathname, uploadId) + } + } +} + +function getMultiPartDriver(driver: Driver) { + if (driver.name === 'cloudflare-r2-binding') { + return { + createMultipartUpload: async (pathname: string, options?: BlobMultipartOptions) => { + return await import('./blob-cloudflare').then(({ createMultipartUpload }) => { + return createMultipartUpload(driver.getInstance!(), pathname, options) + }) + }, + resumeMultipartUpload: async (pathname: string, uploadId: string) => { + return await import('./blob-cloudflare').then(({ resumeMultipartUpload }) => { + return resumeMultipartUpload(driver.getInstance!(), pathname, uploadId) + }) + } + } + } + if (driver.name === 'vercel-blob') { + const token = driver.options?.token || process.env['BLOB_READ_WRITE_TOKEN'] + return { + createMultipartUpload: async (pathname: string, options?: BlobMultipartOptions) => { + return await import('./blob-vercel').then(({ createMultipartUpload }) => { + return createMultipartUpload(token, pathname, options) + }) + }, + resumeMultipartUpload: async (pathname: string, uploadId: string) => { + return await import('./blob-vercel').then(({ resumeMultipartUpload }) => { + return resumeMultipartUpload(token, pathname, uploadId) + }) + } + } + } + + if (driver.name === 'fs') { + return { + createMultipartUpload: async (pathname: string, options?: BlobMultipartOptions) => { + return await import('./blob-fs').then(({ createMultipartUpload }) => { + return createMultipartUpload(driver, pathname, options) + }) + }, + resumeMultipartUpload: async (pathname: string, uploadId: string) => { + return await import('./blob-fs').then(({ resumeMultipartUpload }) => { + return resumeMultipartUpload(driver, pathname, uploadId) + }) + } + } + } + + throw new Error(`Unsupported driver: ${driver.name}`) +} diff --git a/src/runtime/blob/server/utils/blob.ts b/src/runtime/blob/server/utils/blob.ts index 9644621d..6b470f24 100644 --- a/src/runtime/blob/server/utils/blob.ts +++ b/src/runtime/blob/server/utils/blob.ts @@ -1,15 +1,17 @@ import mime from 'mime' -// import { z } from 'zod' -import type { H3Event } from 'h3' // getHeader, getRequestWebStream -import { setHeader, createError, readFormData, assertMethod } from 'h3' // getValidatedQuery, getValidatedRouterParams, readValidatedBody, sendNoContent, +import { z } from 'zod' +import type { H3Event } from 'h3' +import { setHeader, createError, readFormData, assertMethod, getValidatedQuery, getValidatedRouterParams, readValidatedBody, sendNoContent, getHeader, getRequestWebStream } from 'h3' import { defu } from 'defu' import { randomUUID } from 'uncrypto' import { parse } from 'pathe' import { joinURL } from 'ufo' -// import { streamToArrayBuffer } from '../../../utils/stream' +import { streamToArrayBuffer } from '../../../utils/stream' import { requireNuxtHubFeature } from '../../../utils/features' -import type { BlobType, FileSizeUnit, BlobListResult, BlobUploadOptions, BlobPutOptions, BlobEnsureOptions, BlobObject, BlobListOptions } from '@nuxthub/core' // BlobMultipartUpload, HandleMPUResponse, BlobMultipartOptions, +import type { BlobType, FileSizeUnit, BlobListResult, BlobUploadOptions, BlobPutOptions, BlobEnsureOptions, BlobObject, BlobListOptions, BlobMultipartUpload, BlobMultipartOptions, HandleMPUResponse } from '@nuxthub/core' // BlobMultipartUpload, HandleMPUResponse, BlobMultipartOptions, import { useStorage } from '#imports' +import type { Storage } from 'unstorage' +import { multiPartBlobStorage } from '../helpers/multipart-storage' interface HubBlob { /** @@ -92,20 +94,20 @@ interface HubBlob { * * @see https://hub.nuxt.com/docs/features/blob#createmultipartupload */ - // createMultipartUpload(pathname: string, options?: BlobMultipartOptions): Promise + createMultipartUpload(pathname: string, options?: BlobMultipartOptions): Promise /** * Get the specified multipart upload. * * @see https://hub.nuxt.com/docs/features/blob#resumemultipartupload */ - // resumeMultipartUpload(pathname: string, uploadId: string): BlobMultipartUpload + resumeMultipartUpload(pathname: string, uploadId: string): Promise /** * Handle the multipart upload request. * Make sure your route includes `[action]` and `[...pathname]` params. * * @see https://hub.nuxt.com/docs/features/blob#handlemultipartupload */ - // handleMultipartUpload(event: H3Event, options?: BlobMultipartOptions): Promise + handleMultipartUpload(event: H3Event, options?: BlobMultipartOptions): Promise /** * Handle a file upload. * @@ -133,6 +135,7 @@ export function hubBlob(): HubBlob { const storage = useStorage('blob') const blob = { + storage, async list(options?: BlobListOptions) { const resolvedOptions = defu(options, { limit: 1000, @@ -319,35 +322,31 @@ export function hubBlob(): HubBlob { await storage.removeItem(decodeURIComponent(pathnames)) } }, - // async createMultipartUpload(pathname: string, options: BlobMultipartOptions = {}): Promise { - // pathname = decodeURIComponent(pathname) - // const { contentType: optionsContentType, contentLength, addRandomSuffix, prefix, customMetadata } = options - // const contentType = optionsContentType || getContentType(pathname) - - // const { dir, ext, name: filename } = parse(pathname) - // if (addRandomSuffix) { - // pathname = joinURL(dir === '.' ? '' : dir, `${filename}-${randomUUID().split('-')[0]}${ext}`) - // } else { - // pathname = joinURL(dir === '.' ? '' : dir, `${filename}${ext}`) - // } - // if (prefix) { - // pathname = joinURL(prefix, pathname).replace(/\/+/g, '/').replace(/^\/+/, '') - // } - - // const httpMetadata: Record = { contentType } - // if (contentLength) { - // httpMetadata.contentLength = contentLength - // } - - // const mpu = await bucket.createMultipartUpload(pathname, { httpMetadata, customMetadata }) - - // return mapR2MpuToBlobMpu(mpu) - // }, - // resumeMultipartUpload(pathname: string, uploadId: string) { - // const mpu = bucket.resumeMultipartUpload(decodeURIComponent(pathname), uploadId) - - // return mapR2MpuToBlobMpu(mpu) - // }, + async createMultipartUpload(pathname: string, options: BlobMultipartOptions = {}): Promise { + pathname = decodeURIComponent(pathname) + const { contentType: optionsContentType, contentLength, addRandomSuffix, prefix, customMetadata } = options + const contentType = optionsContentType || getContentType(pathname) + + const { dir, ext, name: filename } = parse(pathname) + if (addRandomSuffix) { + pathname = joinURL(dir === '.' ? '' : dir, `${filename}-${randomUUID().split('-')[0]}${ext}`) + } else { + pathname = joinURL(dir === '.' ? '' : dir, `${filename}${ext}`) + } + if (prefix) { + pathname = joinURL(prefix, pathname).replace(/\/+/g, '/').replace(/^\/+/, '') + } + + const httpMetadata: Record = { contentType } + if (contentLength) { + httpMetadata.contentLength = contentLength + } + + return await multiPartBlobStorage(useStorage('blob'), 'blob').createMultipartUpload(pathname, { httpMetadata, customMetadata }) + }, + async resumeMultipartUpload(pathname: string, uploadId: string) { + return await multiPartBlobStorage(useStorage('blob'), 'blob').resumeMultipartUpload(decodeURIComponent(pathname), uploadId) + }, async handleUpload(event: H3Event, options: BlobUploadOptions = {}) { assertMethod(event, ['POST', 'PUT', 'PATCH']) @@ -388,155 +387,151 @@ export function hubBlob(): HubBlob { } return { ...blob, - delete: blob.del - // handleMultipartUpload: createMultipartUploadHandler(blob) + delete: blob.del, + handleMultipartUpload: createMultipartUploadHandler(useStorage('blob'), 'blob') } } -// function createMultipartUploadHandler( -// hub: Pick -// ): HubBlob['handleMultipartUpload'] { -// const { createMultipartUpload, resumeMultipartUpload } = hub - -// const createHandler = async (event: H3Event, options?: BlobMultipartOptions) => { -// const { pathname } = await getValidatedRouterParams(event, z.object({ -// pathname: z.string().min(1) -// }).parse) - -// options ||= {} -// if (getHeader(event, 'x-nuxthub-file-content-type')) { -// options.contentType ||= getHeader(event, 'x-nuxthub-file-content-type') -// } - -// try { -// const object = await createMultipartUpload(pathname, options) -// return { -// uploadId: object.uploadId, -// pathname: object.pathname -// } -// } catch (e: any) { -// throw createError({ -// statusCode: 400, -// message: e.message -// }) -// } -// } - -// const uploadHandler = async (event: H3Event) => { -// const { pathname } = await getValidatedRouterParams(event, z.object({ -// pathname: z.string().min(1) -// }).parse) - -// const { uploadId, partNumber } = await getValidatedQuery(event, z.object({ -// uploadId: z.string(), -// partNumber: z.coerce.number() -// }).parse) - -// const contentLength = Number(getHeader(event, 'content-length') || '0') - -// const stream = getRequestWebStream(event)! -// const body = await streamToArrayBuffer(stream, contentLength) - -// const mpu = resumeMultipartUpload(pathname, uploadId) - -// try { -// return await mpu.uploadPart(partNumber, body) -// } catch (e: any) { -// throw createError({ status: 400, message: e.message }) -// } -// } - -// const completeHandler = async (event: H3Event) => { -// const { pathname } = await getValidatedRouterParams(event, z.object({ -// pathname: z.string().min(1) -// }).parse) - -// const { uploadId } = await getValidatedQuery(event, z.object({ -// uploadId: z.string().min(1) -// }).parse) - -// const { parts } = await readValidatedBody(event, z.object({ -// parts: z.array(z.object({ -// partNumber: z.number(), -// etag: z.string() -// })) -// }).parse) - -// const mpu = resumeMultipartUpload(pathname, uploadId) -// try { -// const object = await mpu.complete(parts) -// return object -// } catch (e: any) { -// throw createError({ status: 400, message: e.message }) -// } -// } - -// const abortHandler = async (event: H3Event) => { -// const { pathname } = await getValidatedRouterParams(event, z.object({ -// pathname: z.string().min(1) -// }).parse) - -// const { uploadId } = await getValidatedQuery(event, z.object({ -// uploadId: z.string().min(1) -// }).parse) - -// const mpu = resumeMultipartUpload(pathname, uploadId) - -// try { -// await mpu.abort() -// } catch (e: any) { -// throw createError({ status: 400, message: e.message }) -// } -// } - -// const handler = async (event: H3Event, options?: BlobMultipartOptions) => { -// const method = event.method -// const { action } = await getValidatedRouterParams(event, z.object({ -// action: z.enum(['create', 'upload', 'complete', 'abort']) -// }).parse) - -// if (action === 'create' && method === 'POST') { -// return { -// action, -// data: await createHandler(event, options) -// } -// } - -// if (action === 'upload' && method === 'PUT') { -// return { -// action, -// data: await uploadHandler(event) -// } -// } - -// if (action === 'complete' && method === 'POST') { -// return { -// action, -// data: await completeHandler(event) -// } -// } - -// if (action === 'abort' && method === 'DELETE') { -// return { -// action, -// data: await abortHandler(event) -// } -// } - -// throw createError({ status: 405 }) -// } - -// return async (event: H3Event, options?: BlobMultipartOptions) => { -// const result = await handler(event, options) - -// if (result.data) { -// event.respondWith(Response.json(result.data)) -// } else { -// sendNoContent(event) -// } -// return result -// } -// } +function createMultipartUploadHandler(storage: Storage, mountPoint: string): HubBlob['handleMultipartUpload'] { + const createHandler = async (event: H3Event, options?: BlobMultipartOptions) => { + const { pathname } = await getValidatedRouterParams(event, z.object({ + pathname: z.string().min(1) + }).parse) + + options ||= {} + if (getHeader(event, 'x-nuxthub-file-content-type')) { + options.contentType ||= getHeader(event, 'x-nuxthub-file-content-type') + } + + try { + const object = await multiPartBlobStorage(storage, mountPoint).createMultipartUpload(pathname, options) + return { + uploadId: object.uploadId, + pathname: object.pathname + } + } catch (e: any) { + throw createError({ + statusCode: 400, + message: e.message + }) + } + } + + const uploadHandler = async (event: H3Event) => { + const { pathname } = await getValidatedRouterParams(event, z.object({ + pathname: z.string().min(1) + }).parse) + + const { uploadId, partNumber } = await getValidatedQuery(event, z.object({ + uploadId: z.string(), + partNumber: z.coerce.number() + }).parse) + + const contentLength = Number(getHeader(event, 'content-length') || '0') + + const stream = getRequestWebStream(event)! + const body = await streamToArrayBuffer(stream, contentLength) + + const mpu = await multiPartBlobStorage(storage, mountPoint).resumeMultipartUpload(pathname, uploadId) + + try { + return await mpu.uploadPart(partNumber, body) + } catch (e: any) { + throw createError({ status: 400, message: e.message }) + } + } + + const completeHandler = async (event: H3Event) => { + const { pathname } = await getValidatedRouterParams(event, z.object({ + pathname: z.string().min(1) + }).parse) + + const { uploadId } = await getValidatedQuery(event, z.object({ + uploadId: z.string().min(1) + }).parse) + + const { parts } = await readValidatedBody(event, z.object({ + parts: z.array(z.object({ + partNumber: z.number(), + etag: z.string() + })) + }).parse) + + const mpu = await multiPartBlobStorage(storage, mountPoint).resumeMultipartUpload(pathname, uploadId) + try { + const object = await mpu.complete(parts) + return object + } catch (e: any) { + throw createError({ status: 400, message: e.message }) + } + } + + const abortHandler = async (event: H3Event) => { + const { pathname } = await getValidatedRouterParams(event, z.object({ + pathname: z.string().min(1) + }).parse) + + const { uploadId } = await getValidatedQuery(event, z.object({ + uploadId: z.string().min(1) + }).parse) + + const mpu = await multiPartBlobStorage(storage, mountPoint).resumeMultipartUpload(pathname, uploadId) + + try { + await mpu.abort() + } catch (e: any) { + throw createError({ status: 400, message: e.message }) + } + } + + const handler = async (event: H3Event, options?: BlobMultipartOptions) => { + const method = event.method + const { action } = await getValidatedRouterParams(event, z.object({ + action: z.enum(['create', 'upload', 'complete', 'abort']) + }).parse) + + if (action === 'create' && method === 'POST') { + return { + action, + data: await createHandler(event, options) + } + } + + if (action === 'upload' && method === 'PUT') { + return { + action, + data: await uploadHandler(event) + } + } + + if (action === 'complete' && method === 'POST') { + return { + action, + data: await completeHandler(event) + } + } + + if (action === 'abort' && method === 'DELETE') { + return { + action, + data: await abortHandler(event) + } + } + + throw createError({ status: 405 }) + } + + return async (event: H3Event, options?: BlobMultipartOptions) => { + const result = await handler(event, options) + + if (result.data) { + event.respondWith(Response.json(result.data)) + } else { + sendNoContent(event) + } + return result + } +} function getContentType(pathOrExtension?: string) { return (pathOrExtension && mime.getType(pathOrExtension)) || 'application/octet-stream' From 23b28b16920e9d678f92b731c3ba540e746a3cb9 Mon Sep 17 00:00:00 2001 From: Rihan Date: Wed, 3 Sep 2025 16:12:25 +0100 Subject: [PATCH 02/15] chore: update comment --- src/runtime/blob/app/composables/useMultipartUpload.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/runtime/blob/app/composables/useMultipartUpload.ts b/src/runtime/blob/app/composables/useMultipartUpload.ts index e8062ee3..04176fd1 100644 --- a/src/runtime/blob/app/composables/useMultipartUpload.ts +++ b/src/runtime/blob/app/composables/useMultipartUpload.ts @@ -19,7 +19,7 @@ export function useMultipartUpload( fetchOptions, prefix } = defu(options, { - partSize: 4 * 1024 * 1024, // 10MB + partSize: 4 * 1024 * 1024, // 4MB concurrent: 1, // no concurrent upload by default maxRetry: 3 }) From 4ae13943bbfb51220ee88a717e8258cfa2cf7ef5 Mon Sep 17 00:00:00 2001 From: Farnabaz Date: Thu, 4 Sep 2025 15:59:17 +0200 Subject: [PATCH 03/15] feat: vercel blob multipart --- src/features.ts | 4 ++++ .../app/composables/useMultipartUpload.ts | 18 +++++++++++++++-- .../blob/server/helpers/blob-vercel.ts | 20 ++++++++++++++++++- src/runtime/blob/server/utils/blob.ts | 6 +++++- 4 files changed, 44 insertions(+), 4 deletions(-) diff --git a/src/features.ts b/src/features.ts index cff688b9..78455bfe 100644 --- a/src/features.ts +++ b/src/features.ts @@ -75,6 +75,10 @@ export function setupBlob(nuxt: Nuxt, hub: HubConfig) { // Add Composables addImportsDir(resolve('./runtime/blob/app/composables')) + + if (nuxt.options.nitro.storage?.blob?.driver === 'vercel-blob') { + nuxt.options.runtimeConfig.public.hub.blobProvider = 'vercel-blob' + } } export async function setupCache(nuxt: Nuxt, hub: HubConfig) { diff --git a/src/runtime/blob/app/composables/useMultipartUpload.ts b/src/runtime/blob/app/composables/useMultipartUpload.ts index 04176fd1..9dc0ed0c 100644 --- a/src/runtime/blob/app/composables/useMultipartUpload.ts +++ b/src/runtime/blob/app/composables/useMultipartUpload.ts @@ -4,7 +4,7 @@ import { joinURL } from 'ufo' import { readonly, ref, type Ref } from 'vue' import type { SerializeObject } from 'nitropack' import type { BlobUploadedPart, BlobObject } from '@nuxthub/core' - +import { useRuntimeConfig } from '#imports' /** * Create a multipart uploader. */ @@ -19,7 +19,7 @@ export function useMultipartUpload( fetchOptions, prefix } = defu(options, { - partSize: 4 * 1024 * 1024, // 4MB + partSize: 10 * 1024 * 1024, // 10MB concurrent: 1, // no concurrent upload by default maxRetry: 3 }) @@ -128,6 +128,20 @@ export function useMultipartUpload( } const start = async () => { + const hub = useRuntimeConfig().public.hub + if (hub.blobProvider === 'vercel-blob') { + return import('@vercel/blob/client').then(({ upload: vercelUpload }) => { + return vercelUpload(file.name, file, { + access: 'public', + multipart: true, + handleUploadUrl: joinURL(baseURL, 'multipart', file.name || ''), + onUploadProgress: (uploadProgress) => { + progress.value = uploadProgress.percentage + } + }) + }) + } + try { await Promise.all(Array.from({ length: concurrent }).map(() => { const partNumber = queue.shift() diff --git a/src/runtime/blob/server/helpers/blob-vercel.ts b/src/runtime/blob/server/helpers/blob-vercel.ts index 12ac21fe..b2b06213 100644 --- a/src/runtime/blob/server/helpers/blob-vercel.ts +++ b/src/runtime/blob/server/helpers/blob-vercel.ts @@ -1,6 +1,8 @@ +import { readBody, type H3Event } from 'h3' +import { handleUpload, type HandleUploadBody } from '@vercel/blob/client' import type { PutBlobResult } from '@vercel/blob' import { createMultipartUpload as vercelCreateMultipartUpload, uploadPart as vercelUploadPart, completeMultipartUpload as vercelCompleteMultipartUpload } from '@vercel/blob' -import type { BlobMultipartOptions, BlobMultipartUpload, BlobObject } from '@nuxthub/core' +import type { BlobMultipartOptions, BlobMultipartUpload, BlobObject, HandleMPUResponse } from '@nuxthub/core' export async function createMultipartUpload(token: string, pathname: string, options?: BlobMultipartOptions): Promise { const { key, uploadId } = await vercelCreateMultipartUpload(pathname, { @@ -64,6 +66,22 @@ export async function resumeMultipartUpload(token: string, pathname: string, upl } } +export const multipartUploadHandler = async (event: H3Event, options?: BlobMultipartOptions): Promise => { + const body = await readBody(event) + + const json = await handleUpload({ + body, + request: event.node.req, + onBeforeGenerateToken: async (pathname, clientPayload) => { + const result = options?.onBeforeGenerateToken ? options?.onBeforeGenerateToken?.(pathname, clientPayload) : {} + return { ...options, ...result } + }, + onUploadCompleted: options?.onUploadCompleted || undefined + }) + + return json +} + function mapR2ObjectToBlob(object: PutBlobResult): BlobObject { return { pathname: object.pathname, diff --git a/src/runtime/blob/server/utils/blob.ts b/src/runtime/blob/server/utils/blob.ts index 6b470f24..fb65aa9c 100644 --- a/src/runtime/blob/server/utils/blob.ts +++ b/src/runtime/blob/server/utils/blob.ts @@ -9,7 +9,7 @@ import { joinURL } from 'ufo' import { streamToArrayBuffer } from '../../../utils/stream' import { requireNuxtHubFeature } from '../../../utils/features' import type { BlobType, FileSizeUnit, BlobListResult, BlobUploadOptions, BlobPutOptions, BlobEnsureOptions, BlobObject, BlobListOptions, BlobMultipartUpload, BlobMultipartOptions, HandleMPUResponse } from '@nuxthub/core' // BlobMultipartUpload, HandleMPUResponse, BlobMultipartOptions, -import { useStorage } from '#imports' +import { useStorage, useRuntimeConfig } from '#imports' import type { Storage } from 'unstorage' import { multiPartBlobStorage } from '../helpers/multipart-storage' @@ -522,6 +522,10 @@ function createMultipartUploadHandler(storage: Storage, mountPoint: string): Hub } return async (event: H3Event, options?: BlobMultipartOptions) => { + if (useRuntimeConfig().public.hub.blobProvider === 'vercel-blob') { + return import('../helpers/blob-vercel').then(({ multipartUploadHandler }) => multipartUploadHandler(event, options)) + } + const result = await handler(event, options) if (result.data) { From b6ca7d4108c53e0d1d059ec108c31d2365c536d3 Mon Sep 17 00:00:00 2001 From: Farnabaz Date: Thu, 4 Sep 2025 16:25:39 +0200 Subject: [PATCH 04/15] fix: blob driver --- .../app/composables/useMultipartUpload.ts | 2 +- .../{multipart-storage.ts => blob-storage.ts} | 18 +- .../blob/server/helpers/blob-vercel.ts | 1 - .../blob/server/helpers/multipart-handler.ts | 157 +++++++++++++++++ src/runtime/blob/server/utils/blob.ts | 165 +----------------- 5 files changed, 180 insertions(+), 163 deletions(-) rename src/runtime/blob/server/helpers/{multipart-storage.ts => blob-storage.ts} (75%) create mode 100644 src/runtime/blob/server/helpers/multipart-handler.ts diff --git a/src/runtime/blob/app/composables/useMultipartUpload.ts b/src/runtime/blob/app/composables/useMultipartUpload.ts index 9dc0ed0c..69403938 100644 --- a/src/runtime/blob/app/composables/useMultipartUpload.ts +++ b/src/runtime/blob/app/composables/useMultipartUpload.ts @@ -132,7 +132,7 @@ export function useMultipartUpload( if (hub.blobProvider === 'vercel-blob') { return import('@vercel/blob/client').then(({ upload: vercelUpload }) => { return vercelUpload(file.name, file, { - access: 'public', + access: 'public', multipart: true, handleUploadUrl: joinURL(baseURL, 'multipart', file.name || ''), onUploadProgress: (uploadProgress) => { diff --git a/src/runtime/blob/server/helpers/multipart-storage.ts b/src/runtime/blob/server/helpers/blob-storage.ts similarity index 75% rename from src/runtime/blob/server/helpers/multipart-storage.ts rename to src/runtime/blob/server/helpers/blob-storage.ts index b2f3bb26..7d38b5b7 100644 --- a/src/runtime/blob/server/helpers/multipart-storage.ts +++ b/src/runtime/blob/server/helpers/blob-storage.ts @@ -1,15 +1,23 @@ import type { Driver, Storage } from 'unstorage' -import type { BlobMultipartOptions } from '@nuxthub/core' +import type { BlobMultipartOptions, BlobMultipartUpload } from '@nuxthub/core' -export function multiPartBlobStorage(storage: Storage, mountPoint: string) { - const driver = getMultiPartDriver(storage.getMount(mountPoint).driver) +export interface BlobStorage extends Storage { + driverName: string + createMultipartUpload: (pathname: string, options?: BlobMultipartOptions) => Promise + resumeMultipartUpload: (pathname: string, uploadId: string) => Promise +} + +export function blobStorage(storage: Storage, mountPoint: string): BlobStorage { + const driver = storage.getMount(mountPoint).driver + const multiPartDriver = getMultiPartDriver(driver) return { + driverName: driver.name!, ...storage, createMultipartUpload: async (pathname: string, options?: BlobMultipartOptions) => { - return driver.createMultipartUpload(pathname, options) + return multiPartDriver.createMultipartUpload(pathname, options) }, resumeMultipartUpload: async (pathname: string, uploadId: string) => { - return driver.resumeMultipartUpload(pathname, uploadId) + return multiPartDriver.resumeMultipartUpload(pathname, uploadId) } } } diff --git a/src/runtime/blob/server/helpers/blob-vercel.ts b/src/runtime/blob/server/helpers/blob-vercel.ts index b2b06213..290c544b 100644 --- a/src/runtime/blob/server/helpers/blob-vercel.ts +++ b/src/runtime/blob/server/helpers/blob-vercel.ts @@ -85,7 +85,6 @@ export const multipartUploadHandler = async (event: H3Event, options?: BlobMulti function mapR2ObjectToBlob(object: PutBlobResult): BlobObject { return { pathname: object.pathname, - url: object.url, contentType: object.contentType, size: 0, // TODO: get size httpEtag: '', // TODO: get etag diff --git a/src/runtime/blob/server/helpers/multipart-handler.ts b/src/runtime/blob/server/helpers/multipart-handler.ts new file mode 100644 index 00000000..51a14472 --- /dev/null +++ b/src/runtime/blob/server/helpers/multipart-handler.ts @@ -0,0 +1,157 @@ +import z from 'zod' +import type { H3Event } from 'h3' +import type { BlobMultipartOptions } from '@nuxthub/core' +import { type BlobStorage } from './blob-storage' +import { createError, getValidatedQuery, readValidatedBody, getRequestWebStream, sendNoContent, getHeader, getValidatedRouterParams } from 'h3' +import { streamToArrayBuffer } from '../../../utils/stream' + +export function createMultipartUploadHandler(storage: BlobStorage) { + if (storage.driverName === 'vercel-blob') { + return async (event: H3Event, options?: BlobMultipartOptions) => + import('../helpers/blob-vercel') + .then(({ multipartUploadHandler }) => multipartUploadHandler(event, options)) + } + + return createMultipartUploadHandlerWithBlobStorage(storage) +} + +function createMultipartUploadHandlerWithBlobStorage(blobStorage: BlobStorage) { + const createHandler = async (event: H3Event, options?: BlobMultipartOptions) => { + const { pathname } = await getValidatedRouterParams(event, z.object({ + pathname: z.string().min(1) + }).parse) + + options ||= {} + if (getHeader(event, 'x-nuxthub-file-content-type')) { + options.contentType ||= getHeader(event, 'x-nuxthub-file-content-type') + } + + try { + const object = await blobStorage.createMultipartUpload(pathname, options) + return { + uploadId: object.uploadId, + pathname: object.pathname + } + } catch (e: any) { + throw createError({ + statusCode: 400, + message: e.message + }) + } + } + + const uploadHandler = async (event: H3Event) => { + const { pathname } = await getValidatedRouterParams(event, z.object({ + pathname: z.string().min(1) + }).parse) + + const { uploadId, partNumber } = await getValidatedQuery(event, z.object({ + uploadId: z.string(), + partNumber: z.coerce.number() + }).parse) + + const contentLength = Number(getHeader(event, 'content-length') || '0') + + const stream = getRequestWebStream(event)! + const body = await streamToArrayBuffer(stream, contentLength) + + const mpu = await blobStorage.resumeMultipartUpload(pathname, uploadId) + + try { + return await mpu.uploadPart(partNumber, body) + } catch (e: any) { + throw createError({ status: 400, message: e.message }) + } + } + + const completeHandler = async (event: H3Event) => { + const { pathname } = await getValidatedRouterParams(event, z.object({ + pathname: z.string().min(1) + }).parse) + + const { uploadId } = await getValidatedQuery(event, z.object({ + uploadId: z.string().min(1) + }).parse) + + const { parts } = await readValidatedBody(event, z.object({ + parts: z.array(z.object({ + partNumber: z.number(), + etag: z.string() + })) + }).parse) + + const mpu = await blobStorage.resumeMultipartUpload(pathname, uploadId) + try { + const object = await mpu.complete(parts) + return object + } catch (e: any) { + throw createError({ status: 400, message: e.message }) + } + } + + const abortHandler = async (event: H3Event) => { + const { pathname } = await getValidatedRouterParams(event, z.object({ + pathname: z.string().min(1) + }).parse) + + const { uploadId } = await getValidatedQuery(event, z.object({ + uploadId: z.string().min(1) + }).parse) + + const mpu = await blobStorage.resumeMultipartUpload(pathname, uploadId) + + try { + await mpu.abort() + } catch (e: any) { + throw createError({ status: 400, message: e.message }) + } + } + + const handler = async (event: H3Event, options?: BlobMultipartOptions) => { + const method = event.method + const { action } = await getValidatedRouterParams(event, z.object({ + action: z.enum(['create', 'upload', 'complete', 'abort']) + }).parse) + + if (action === 'create' && method === 'POST') { + return { + action, + data: await createHandler(event, options) + } + } + + if (action === 'upload' && method === 'PUT') { + return { + action, + data: await uploadHandler(event) + } + } + + if (action === 'complete' && method === 'POST') { + return { + action, + data: await completeHandler(event) + } + } + + if (action === 'abort' && method === 'DELETE') { + return { + action, + data: await abortHandler(event) + } + } + + throw createError({ status: 405 }) + } + + return async (event: H3Event, options?: BlobMultipartOptions) => { + const result = await handler(event, options) + + if (result.data) { + event.respondWith(Response.json(result.data)) + } else { + sendNoContent(event) + } + return result + } +} diff --git a/src/runtime/blob/server/utils/blob.ts b/src/runtime/blob/server/utils/blob.ts index fb65aa9c..6319f64d 100644 --- a/src/runtime/blob/server/utils/blob.ts +++ b/src/runtime/blob/server/utils/blob.ts @@ -1,17 +1,15 @@ import mime from 'mime' -import { z } from 'zod' import type { H3Event } from 'h3' -import { setHeader, createError, readFormData, assertMethod, getValidatedQuery, getValidatedRouterParams, readValidatedBody, sendNoContent, getHeader, getRequestWebStream } from 'h3' +import { setHeader, createError, readFormData, assertMethod } from 'h3' import { defu } from 'defu' import { randomUUID } from 'uncrypto' import { parse } from 'pathe' import { joinURL } from 'ufo' -import { streamToArrayBuffer } from '../../../utils/stream' import { requireNuxtHubFeature } from '../../../utils/features' -import type { BlobType, FileSizeUnit, BlobListResult, BlobUploadOptions, BlobPutOptions, BlobEnsureOptions, BlobObject, BlobListOptions, BlobMultipartUpload, BlobMultipartOptions, HandleMPUResponse } from '@nuxthub/core' // BlobMultipartUpload, HandleMPUResponse, BlobMultipartOptions, -import { useStorage, useRuntimeConfig } from '#imports' -import type { Storage } from 'unstorage' -import { multiPartBlobStorage } from '../helpers/multipart-storage' +import type { BlobType, FileSizeUnit, BlobListResult, BlobUploadOptions, BlobPutOptions, BlobEnsureOptions, BlobObject, BlobListOptions, BlobMultipartUpload, BlobMultipartOptions, HandleMPUResponse } from '@nuxthub/core' +import { useStorage } from '#imports' +import { blobStorage } from '../helpers/blob-storage' +import { createMultipartUploadHandler } from '../helpers/multipart-handler' interface HubBlob { /** @@ -132,7 +130,7 @@ interface HubBlob { export function hubBlob(): HubBlob { requireNuxtHubFeature('blob') - const storage = useStorage('blob') + const storage = blobStorage(useStorage('blob'), 'blob') const blob = { storage, @@ -342,10 +340,10 @@ export function hubBlob(): HubBlob { httpMetadata.contentLength = contentLength } - return await multiPartBlobStorage(useStorage('blob'), 'blob').createMultipartUpload(pathname, { httpMetadata, customMetadata }) + return await storage.createMultipartUpload(pathname, { httpMetadata, customMetadata }) }, async resumeMultipartUpload(pathname: string, uploadId: string) { - return await multiPartBlobStorage(useStorage('blob'), 'blob').resumeMultipartUpload(decodeURIComponent(pathname), uploadId) + return await storage.resumeMultipartUpload(decodeURIComponent(pathname), uploadId) }, async handleUpload(event: H3Event, options: BlobUploadOptions = {}) { assertMethod(event, ['POST', 'PUT', 'PATCH']) @@ -388,152 +386,7 @@ export function hubBlob(): HubBlob { return { ...blob, delete: blob.del, - handleMultipartUpload: createMultipartUploadHandler(useStorage('blob'), 'blob') - } -} - -function createMultipartUploadHandler(storage: Storage, mountPoint: string): HubBlob['handleMultipartUpload'] { - const createHandler = async (event: H3Event, options?: BlobMultipartOptions) => { - const { pathname } = await getValidatedRouterParams(event, z.object({ - pathname: z.string().min(1) - }).parse) - - options ||= {} - if (getHeader(event, 'x-nuxthub-file-content-type')) { - options.contentType ||= getHeader(event, 'x-nuxthub-file-content-type') - } - - try { - const object = await multiPartBlobStorage(storage, mountPoint).createMultipartUpload(pathname, options) - return { - uploadId: object.uploadId, - pathname: object.pathname - } - } catch (e: any) { - throw createError({ - statusCode: 400, - message: e.message - }) - } - } - - const uploadHandler = async (event: H3Event) => { - const { pathname } = await getValidatedRouterParams(event, z.object({ - pathname: z.string().min(1) - }).parse) - - const { uploadId, partNumber } = await getValidatedQuery(event, z.object({ - uploadId: z.string(), - partNumber: z.coerce.number() - }).parse) - - const contentLength = Number(getHeader(event, 'content-length') || '0') - - const stream = getRequestWebStream(event)! - const body = await streamToArrayBuffer(stream, contentLength) - - const mpu = await multiPartBlobStorage(storage, mountPoint).resumeMultipartUpload(pathname, uploadId) - - try { - return await mpu.uploadPart(partNumber, body) - } catch (e: any) { - throw createError({ status: 400, message: e.message }) - } - } - - const completeHandler = async (event: H3Event) => { - const { pathname } = await getValidatedRouterParams(event, z.object({ - pathname: z.string().min(1) - }).parse) - - const { uploadId } = await getValidatedQuery(event, z.object({ - uploadId: z.string().min(1) - }).parse) - - const { parts } = await readValidatedBody(event, z.object({ - parts: z.array(z.object({ - partNumber: z.number(), - etag: z.string() - })) - }).parse) - - const mpu = await multiPartBlobStorage(storage, mountPoint).resumeMultipartUpload(pathname, uploadId) - try { - const object = await mpu.complete(parts) - return object - } catch (e: any) { - throw createError({ status: 400, message: e.message }) - } - } - - const abortHandler = async (event: H3Event) => { - const { pathname } = await getValidatedRouterParams(event, z.object({ - pathname: z.string().min(1) - }).parse) - - const { uploadId } = await getValidatedQuery(event, z.object({ - uploadId: z.string().min(1) - }).parse) - - const mpu = await multiPartBlobStorage(storage, mountPoint).resumeMultipartUpload(pathname, uploadId) - - try { - await mpu.abort() - } catch (e: any) { - throw createError({ status: 400, message: e.message }) - } - } - - const handler = async (event: H3Event, options?: BlobMultipartOptions) => { - const method = event.method - const { action } = await getValidatedRouterParams(event, z.object({ - action: z.enum(['create', 'upload', 'complete', 'abort']) - }).parse) - - if (action === 'create' && method === 'POST') { - return { - action, - data: await createHandler(event, options) - } - } - - if (action === 'upload' && method === 'PUT') { - return { - action, - data: await uploadHandler(event) - } - } - - if (action === 'complete' && method === 'POST') { - return { - action, - data: await completeHandler(event) - } - } - - if (action === 'abort' && method === 'DELETE') { - return { - action, - data: await abortHandler(event) - } - } - - throw createError({ status: 405 }) - } - - return async (event: H3Event, options?: BlobMultipartOptions) => { - if (useRuntimeConfig().public.hub.blobProvider === 'vercel-blob') { - return import('../helpers/blob-vercel').then(({ multipartUploadHandler }) => multipartUploadHandler(event, options)) - } - - const result = await handler(event, options) - - if (result.data) { - event.respondWith(Response.json(result.data)) - } else { - sendNoContent(event) - } - return result + handleMultipartUpload: createMultipartUploadHandler(storage) } } From 6a1b0a6bb55ad26003052c937baee8dcd2b100ea Mon Sep 17 00:00:00 2001 From: Farnabaz Date: Thu, 4 Sep 2025 16:58:45 +0200 Subject: [PATCH 05/15] up --- src/runtime/blob/app/composables/useMultipartUpload.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/runtime/blob/app/composables/useMultipartUpload.ts b/src/runtime/blob/app/composables/useMultipartUpload.ts index 69403938..8d13c3f7 100644 --- a/src/runtime/blob/app/composables/useMultipartUpload.ts +++ b/src/runtime/blob/app/composables/useMultipartUpload.ts @@ -130,8 +130,8 @@ export function useMultipartUpload( const start = async () => { const hub = useRuntimeConfig().public.hub if (hub.blobProvider === 'vercel-blob') { - return import('@vercel/blob/client').then(({ upload: vercelUpload }) => { - return vercelUpload(file.name, file, { + return import('@vercel/blob/client').then(({ upload }) => { + return upload(file.name, file, { access: 'public', multipart: true, handleUploadUrl: joinURL(baseURL, 'multipart', file.name || ''), From 1133565f49c5a48744a24a4ee79ce0ed0af049b3 Mon Sep 17 00:00:00 2001 From: "autofix-ci[bot]" <114827586+autofix-ci[bot]@users.noreply.github.com> Date: Mon, 8 Sep 2025 08:30:00 +0000 Subject: [PATCH 06/15] [autofix.ci] apply automated fixes --- src/runtime/blob/server/helpers/multipart-handler.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/runtime/blob/server/helpers/multipart-handler.ts b/src/runtime/blob/server/helpers/multipart-handler.ts index 51a14472..613b7332 100644 --- a/src/runtime/blob/server/helpers/multipart-handler.ts +++ b/src/runtime/blob/server/helpers/multipart-handler.ts @@ -1,7 +1,7 @@ import z from 'zod' import type { H3Event } from 'h3' import type { BlobMultipartOptions } from '@nuxthub/core' -import { type BlobStorage } from './blob-storage' +import type { BlobStorage } from './blob-storage' import { createError, getValidatedQuery, readValidatedBody, getRequestWebStream, sendNoContent, getHeader, getValidatedRouterParams } from 'h3' import { streamToArrayBuffer } from '../../../utils/stream' From e3fc5feb60a1cd6ed7160af9b3a232a33b10ab65 Mon Sep 17 00:00:00 2001 From: Farnabaz Date: Tue, 9 Sep 2025 12:21:03 +0200 Subject: [PATCH 07/15] feat: support multi-part upload with `s3` driver --- src/runtime/blob/server/helpers/blob-s3.ts | 144 ++++++++++++++++++ .../blob/server/helpers/blob-storage.ts | 17 +++ .../blob/server/helpers/blob-vercel.ts | 9 +- src/runtime/blob/server/utils/blob.ts | 2 +- src/types/blob.ts | 4 + 5 files changed, 171 insertions(+), 5 deletions(-) create mode 100644 src/runtime/blob/server/helpers/blob-s3.ts diff --git a/src/runtime/blob/server/helpers/blob-s3.ts b/src/runtime/blob/server/helpers/blob-s3.ts new file mode 100644 index 00000000..22500a64 --- /dev/null +++ b/src/runtime/blob/server/helpers/blob-s3.ts @@ -0,0 +1,144 @@ +import { AwsClient } from 'aws4fetch' +import type { BlobMultipartUpload, BlobMultipartOptions } from '@nuxthub/core' +import type { S3DriverOptions } from 'unstorage/drivers/s3' + +export async function createMultipartUpload(pathname: string, driverOptions: S3DriverOptions, options?: BlobMultipartOptions): Promise { + const aws = new AwsClient({ + accessKeyId: driverOptions.accessKeyId!, + secretAccessKey: driverOptions.secretAccessKey!, + region: driverOptions.region!, + service: 's3' + }) + + const res = await aws.fetch(`${baseUrl(driverOptions, pathname)}?uploads`, { method: 'POST', headers: buildCreateHeaders(options) }).catch((e) => { + console.log('error', e) + throw e + }) + if (!res.ok) { + console.log('error', res) + throw new Error(`Initiate failed: ${res.status} ${res.statusText}`) + } + const xmlText = await res.text() + const uploadId = xmlTagContent(xmlText, 'UploadId')! + + return resumeMultipartUpload(pathname, uploadId, driverOptions, options) +} + +export async function resumeMultipartUpload(pathname: string, uploadId: string, driverOptions: S3DriverOptions, options?: BlobMultipartOptions): Promise { + const aws = new AwsClient({ + accessKeyId: driverOptions.accessKeyId!, + secretAccessKey: driverOptions.secretAccessKey!, + region: driverOptions.region! + }) + const objectUrl = baseUrl(driverOptions, pathname) + + return { + pathname, + uploadId, + + async uploadPart(partNumber, value) { + if (!Number.isInteger(partNumber) || partNumber < 1) { + throw new Error('partNumber must be a positive integer starting at 1') + } + + const res = await aws.fetch(`${objectUrl}?partNumber=${partNumber}&uploadId=${encodeURIComponent(uploadId)}`, { + method: 'PUT', + body: value as unknown as BodyInit + }) + if (!res.ok) { + const text = await res.text() + throw new Error(`UploadPart ${partNumber} failed: ${res.status} ${res.statusText} ${text}`) + } + + const ETag = res.headers.get('ETag') + if (!ETag) { + throw new Error('Missing ETag on UploadPart response; check bucket CORS ExposeHeader.') + } + return { partNumber, etag: ETag } + }, + + async abort() { + const res = await aws.fetch(`${objectUrl}?uploadId=${encodeURIComponent(uploadId)}`, { method: 'DELETE' }) + if (!res.ok) { + const text = await res.text() + throw new Error(`Abort failed: ${res.status} ${res.statusText} ${text}`) + } + }, + + async complete(uploadedParts) { + if (!Array.isArray(uploadedParts) || uploadedParts.length === 0) { + throw new Error('uploadedParts must be a non-empty array') + } + + const parts = [...uploadedParts].sort((a, b) => a.partNumber - b.partNumber) + + const res = await aws.fetch(`${objectUrl}?uploadId=${encodeURIComponent(uploadId)}`, { + method: 'POST', + headers: { 'Content-Type': 'application/xml' }, + body: xmlTag( + 'CompleteMultipartUpload', + parts.map( + p => xmlTag( + 'Part', + xmlTag('PartNumber', p.partNumber) + xmlTag('ETag', p.etag) + ) + ).join('') + ) + }) + + if (!res.ok) { + const text = await res.text() + throw new Error(`Complete failed: ${res.status} ${res.statusText} ${text}`) + } + + const xmlText = await res.text() + console.log('xmlText', xmlText) + return { + pathname, + contentType: 'application/octet-stream', + url: xmlTagContent(xmlText, 'Location'), + size: 0, + httpEtag: xmlTagContent(xmlText, 'ETag'), + uploadedAt: new Date(), + httpMetadata: {}, + customMetadata: {} + } + } + } +} + +const baseUrl = (driverOptions: S3DriverOptions, key?: string) => { + const host = driverOptions.endpoint + ? driverOptions.endpoint + : ( + driverOptions.region + ? `https://${driverOptions.bucket}.s3.${driverOptions.region}.amazonaws.com` + : `https://${driverOptions.bucket}.s3.amazonaws.com` + ) + return key ? `${host}/${encodeURI(driverOptions.bucket)}/${encodeURI(key)}` : host +} + +const buildCreateHeaders = (opts?: BlobMultipartOptions) => { + const headers: Record = {} + if (opts?.contentType) headers['Content-Type'] = opts.contentType + if (opts?.serverSideEncryption) + headers['x-amz-server-side-encryption'] = opts.serverSideEncryption + if (opts?.sseKmsKeyId) + headers['x-amz-server-side-encryption-aws-kms-key-id'] = opts.sseKmsKeyId + if (opts?.metadata) { + for (const [k, v] of Object.entries(opts.metadata)) { + headers[`x-amz-meta-${k.toLowerCase()}`] = String(v) + } + } + + return headers +} + +const xmlTagContent = (xml: string, tag: string) => { + const m = xml.match(new RegExp(`<${tag}>([\\s\\S]*?)`)) + if (!m) throw new Error(`Missing <${tag}> in XML.`) + return decodeURIComponent(m[1]!) +} +const xmlTag = (tag: string, content: string | number) => { + return `<${tag}>${content}` +} diff --git a/src/runtime/blob/server/helpers/blob-storage.ts b/src/runtime/blob/server/helpers/blob-storage.ts index 7d38b5b7..17e792f1 100644 --- a/src/runtime/blob/server/helpers/blob-storage.ts +++ b/src/runtime/blob/server/helpers/blob-storage.ts @@ -1,5 +1,6 @@ import type { Driver, Storage } from 'unstorage' import type { BlobMultipartOptions, BlobMultipartUpload } from '@nuxthub/core' +import type { S3DriverOptions } from 'unstorage/drivers/s3' export interface BlobStorage extends Storage { driverName: string @@ -23,6 +24,22 @@ export function blobStorage(storage: Storage, mountPoint: string): BlobStorage { } function getMultiPartDriver(driver: Driver) { + if (driver.name === 's3') { + const driverOptions = driver.options as S3DriverOptions + return { + createMultipartUpload: async (pathname: string, options?: BlobMultipartOptions) => { + return await import('./blob-s3').then(({ createMultipartUpload }) => { + return createMultipartUpload(pathname, driverOptions, options) + }) + }, + resumeMultipartUpload: async (pathname: string, uploadId: string) => { + return await import('./blob-s3').then(({ resumeMultipartUpload }) => { + return resumeMultipartUpload(pathname, uploadId, driverOptions) + }) + } + } + } + if (driver.name === 'cloudflare-r2-binding') { return { createMultipartUpload: async (pathname: string, options?: BlobMultipartOptions) => { diff --git a/src/runtime/blob/server/helpers/blob-vercel.ts b/src/runtime/blob/server/helpers/blob-vercel.ts index 290c544b..be17fb08 100644 --- a/src/runtime/blob/server/helpers/blob-vercel.ts +++ b/src/runtime/blob/server/helpers/blob-vercel.ts @@ -34,7 +34,7 @@ export async function createMultipartUpload(token: string, pathname: string, opt uploadId, key }) - return mapR2ObjectToBlob(r2Object) + return mapPutBlobToBlob(r2Object) } } } @@ -61,7 +61,7 @@ export async function resumeMultipartUpload(token: string, pathname: string, upl uploadId, key: pathname }) - return mapR2ObjectToBlob(putBlobResult) + return mapPutBlobToBlob(putBlobResult) } } } @@ -79,13 +79,14 @@ export const multipartUploadHandler = async (event: H3Event, options?: BlobMulti onUploadCompleted: options?.onUploadCompleted || undefined }) - return json + return json as unknown as HandleMPUResponse } -function mapR2ObjectToBlob(object: PutBlobResult): BlobObject { +function mapPutBlobToBlob(object: PutBlobResult): BlobObject { return { pathname: object.pathname, contentType: object.contentType, + url: object.url, size: 0, // TODO: get size httpEtag: '', // TODO: get etag uploadedAt: new Date(), diff --git a/src/runtime/blob/server/utils/blob.ts b/src/runtime/blob/server/utils/blob.ts index b9e85381..3c3232d3 100644 --- a/src/runtime/blob/server/utils/blob.ts +++ b/src/runtime/blob/server/utils/blob.ts @@ -259,7 +259,7 @@ export function hubBlob(): HubBlob { } // Convert File or Blob to TypedArray for storage - let processedBody: string | ReadableStream | ArrayBuffer | ArrayBufferView = body + let processedBody = body as string | ReadableStream | ArrayBuffer | ArrayBufferView if (body instanceof Blob) { const arrayBuffer = await body.arrayBuffer() processedBody = new Uint8Array(arrayBuffer) diff --git a/src/types/blob.ts b/src/types/blob.ts index 04a1800f..1be666ab 100644 --- a/src/types/blob.ts +++ b/src/types/blob.ts @@ -37,6 +37,10 @@ export interface BlobObject { * The custom metadata of the blob. */ customMetadata: Record + /** + * The URL of the blob. + */ + url?: string } export interface BlobUploadedPart { From 10876c70774bd8cdf676b4637d26129897cf92e7 Mon Sep 17 00:00:00 2001 From: Farnabaz Date: Tue, 9 Sep 2025 12:46:23 +0200 Subject: [PATCH 08/15] lint: fix --- src/runtime/blob/server/helpers/blob-s3.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/runtime/blob/server/helpers/blob-s3.ts b/src/runtime/blob/server/helpers/blob-s3.ts index 22500a64..47a4705f 100644 --- a/src/runtime/blob/server/helpers/blob-s3.ts +++ b/src/runtime/blob/server/helpers/blob-s3.ts @@ -21,10 +21,10 @@ export async function createMultipartUpload(pathname: string, driverOptions: S3D const xmlText = await res.text() const uploadId = xmlTagContent(xmlText, 'UploadId')! - return resumeMultipartUpload(pathname, uploadId, driverOptions, options) + return resumeMultipartUpload(pathname, uploadId, driverOptions) } -export async function resumeMultipartUpload(pathname: string, uploadId: string, driverOptions: S3DriverOptions, options?: BlobMultipartOptions): Promise { +export async function resumeMultipartUpload(pathname: string, uploadId: string, driverOptions: S3DriverOptions): Promise { const aws = new AwsClient({ accessKeyId: driverOptions.accessKeyId!, secretAccessKey: driverOptions.secretAccessKey!, From ece73139fee347ba1b7b0f21e0ae0d57b6156559 Mon Sep 17 00:00:00 2001 From: Farnabaz Date: Tue, 9 Sep 2025 15:21:46 +0200 Subject: [PATCH 09/15] update docs --- docs/content/docs/2.features/blob.md | 12 ++++++++++-- playground/app/pages/blob.vue | 10 +++++----- 2 files changed, 15 insertions(+), 7 deletions(-) diff --git a/docs/content/docs/2.features/blob.md b/docs/content/docs/2.features/blob.md index 6ac1cb42..b15f7012 100644 --- a/docs/content/docs/2.features/blob.md +++ b/docs/content/docs/2.features/blob.md @@ -390,7 +390,7 @@ Returns a [`BlobObject`](#blobobject) or an array of [`BlobObject`](#blobobject) Throws an error if `file` doesn't meet the requirements. - +:: ## `ensureBlob()` @@ -697,6 +700,10 @@ const data = await completed Application composable that creates a multipart upload helper. +::important +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. +:: + ```ts [utils/multipart-upload.ts] export const mpu = useMultipartUpload('/api/files/multipart') ``` @@ -752,6 +759,7 @@ interface BlobObject { uploadedAt: Date httpMetadata: Record customMetadata: Record + url: string | undefined } ``` diff --git a/playground/app/pages/blob.vue b/playground/app/pages/blob.vue index 672e651b..e4e5df9a 100644 --- a/playground/app/pages/blob.vue +++ b/playground/app/pages/blob.vue @@ -40,7 +40,7 @@ async function loadMore() { async function addFile() { if (!newFilesValue.value.length) { - toast.add({ title: 'Missing files.', color: 'red' }) + toast.add({ title: 'Missing files.', color: 'error' }) return } loading.value = true @@ -54,7 +54,7 @@ async function addFile() { newFilesValue.value = [] } catch (err: any) { const title = err.data?.data?.issues?.map((issue: any) => issue.message).join('\n') || err.message - toast.add({ title, color: 'red' }) + toast.add({ title, color: 'error' }) } loading.value = false } @@ -114,7 +114,7 @@ async function uploadFiles(files: File[]) { } else { toast.add({ title: `Failed to upload ${file.name}.`, - color: 'red' + color: 'error' }) } } @@ -144,7 +144,7 @@ async function deleteFile(pathname: string) { toast.add({ title: `File "${pathname}" deleted.` }) } catch (err: any) { const title = err.data?.data?.issues?.map((issue: any) => issue.message).join('\n') || err.message - toast.add({ title, color: 'red' }) + toast.add({ title, color: 'error' }) } } @@ -159,7 +159,7 @@ async function deleteFile(pathname: string) { disabled class="flex-1" autocomplete="off" - :ui="{ wrapper: 'flex-1' }" + :ui="{ root: 'flex-1' }" /> Date: Mon, 15 Sep 2025 10:32:43 +0200 Subject: [PATCH 10/15] docs: add providers section --- docs/content/docs/2.features/blob.md | 116 +++++++++++++++++++++++++++ 1 file changed, 116 insertions(+) diff --git a/docs/content/docs/2.features/blob.md b/docs/content/docs/2.features/blob.md index b15f7012..29e19e4e 100644 --- a/docs/content/docs/2.features/blob.md +++ b/docs/content/docs/2.features/blob.md @@ -746,6 +746,122 @@ const { completed, progress, abort } = mpu(file) const data = await completed ``` +## Storage Providers + +NuxtHub supports multiple storage providers for blob storage. In development mode, NuxtHub automatically configures the filesystem (`fs`) driver for local development. + +### Filesystem (fs) + +The filesystem driver stores blobs locally on your development machine. + +```ts [nuxt.config.ts] +export default defineNuxtConfig({ + nitro: { + storage: { + BLOB: { + driver: 'fs', + base: './.data/blob' + } + } + } +}) +``` + +### Vercel Blob + +For production deployments on Vercel, use the Vercel Blob driver. + +```ts [nuxt.config.ts] +export default defineNuxtConfig({ + nitro: { + storage: { + BLOB: { + driver: 'vercel-blob', + access: 'public' + } + } + } +}) +``` + +### Cloudflare R2 + +For Cloudflare deployments, you can use Cloudflare R2 with either bindings (recommended) or the S3-compatible driver. + +#### Using R2 Bindings (Recommended) + +When deploying to Cloudflare Workers, use R2 bindings for optimal performance and integration. + +```ts [nuxt.config.ts] +export default defineNuxtConfig({ + nitro: { + storage: { + BLOB: { + driver: 'cloudflare-r2', + binding: 'BLOB' + } + } + } +}) +``` + +Make sure to configure the R2 binding in your `wrangler.toml`: + +```toml [wrangler.toml] +[[r2_buckets]] +binding = "BLOB" +bucket_name = "my-bucket" +``` + +#### Using S3-Compatible Driver + +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 Blob. + +```ts [nuxt.config.ts] +export default defineNuxtConfig({ + nitro: { + storage: { + BLOB: { + driver: 's3', + accessKeyId: process.env.CLOUDFLARE_R2_ACCESS_KEY_ID, + secretAccessKey: process.env.CLOUDFLARE_R2_SECRET_ACCESS_KEY, + region: 'auto', + endpoint: `https://${process.env.CLOUDFLARE_ACCOUNT_ID}.r2.cloudflarestorage.com`, + bucket: process.env.CLOUDFLARE_R2_BUCKET_NAME + } + } + } +}) +``` + +### Amazon S3 + +For AWS S3 storage, use the S3 driver. + +```ts [nuxt.config.ts] +export default defineNuxtConfig({ + nitro: { + storage: { + BLOB: { + driver: 's3', + accessKeyId: process.env.AWS_ACCESS_KEY_ID, + secretAccessKey: process.env.AWS_SECRET_ACCESS_KEY, + region: process.env.AWS_REGION, + bucket: process.env.AWS_S3_BUCKET + } + } + } +}) +``` + +::callout{to="https://unstorage.unjs.io/drivers"} +For additional storage providers and configuration options, see the unstorage documentation. +:: + +::note +Other unstorage drivers do not support multi-part upload. If you want to upload big files, consider using one of the above providers. +:: + ## Types ### `BlobObject` From 780b5829c7c0bed7cee860cf136998501cf76619 Mon Sep 17 00:00:00 2001 From: Farnabaz Date: Mon, 15 Sep 2025 12:21:00 +0200 Subject: [PATCH 11/15] update types and tests --- src/runtime/blob/server/helpers/blob-fs.ts | 1 - src/runtime/blob/server/helpers/blob-vercel.ts | 3 +-- src/types/blob.ts | 4 ++-- test/blob.test.ts | 13 ++++++++++++- 4 files changed, 15 insertions(+), 6 deletions(-) diff --git a/src/runtime/blob/server/helpers/blob-fs.ts b/src/runtime/blob/server/helpers/blob-fs.ts index e2ce9df2..6c5e4dc4 100644 --- a/src/runtime/blob/server/helpers/blob-fs.ts +++ b/src/runtime/blob/server/helpers/blob-fs.ts @@ -84,7 +84,6 @@ async function createOrResumeMultipartUpload(driver: Driver, pathname: string, u return { pathname: currentMetadata.pathname, contentType: currentMetadata.contentType, - size: 0, // TODO: get size from uploaded parts httpEtag: `"${randomUUID()}"`, uploadedAt: new Date(), httpMetadata: { diff --git a/src/runtime/blob/server/helpers/blob-vercel.ts b/src/runtime/blob/server/helpers/blob-vercel.ts index be17fb08..e6c2297e 100644 --- a/src/runtime/blob/server/helpers/blob-vercel.ts +++ b/src/runtime/blob/server/helpers/blob-vercel.ts @@ -87,8 +87,7 @@ function mapPutBlobToBlob(object: PutBlobResult): BlobObject { pathname: object.pathname, contentType: object.contentType, url: object.url, - size: 0, // TODO: get size - httpEtag: '', // TODO: get etag + httpEtag: undefined, uploadedAt: new Date(), httpMetadata: {}, customMetadata: {} diff --git a/src/types/blob.ts b/src/types/blob.ts index 1246acff..e863b48a 100644 --- a/src/types/blob.ts +++ b/src/types/blob.ts @@ -20,11 +20,11 @@ export interface BlobObject { /** * The size of the blob in bytes. */ - size: number + size?: number /** * The blob's etag, in quotes so as to be returned as a header. */ - httpEtag: string + httpEtag: string | undefined /** * The date the blob was uploaded at. */ diff --git a/test/blob.test.ts b/test/blob.test.ts index ea793b25..c7528b84 100644 --- a/test/blob.test.ts +++ b/test/blob.test.ts @@ -1,6 +1,6 @@ import { fileURLToPath } from 'node:url' import fs from 'node:fs/promises' -import { describe, it, expect } from 'vitest' +import { describe, it, expect, vi } from 'vitest' import { setup, $fetch, url } from '@nuxt/test-utils' import type { BlobListResult } from '../src/types/blob' import { useUpload } from '../src/runtime/blob/app/composables/useUpload' @@ -19,6 +19,17 @@ const images = [ } ] +// create mock for import { useRuntimeConfig } from '#imports' +vi.mock('#imports', () => ({ + useRuntimeConfig: () => ({ + public: { + hub: { + blobProvider: 'fs' + } + } + }) +})) + describe('Blob', async () => { await cleanUp() From e98c072e49468c1cb96b569ce57714bcbc0831e6 Mon Sep 17 00:00:00 2001 From: Farnabaz Date: Mon, 15 Sep 2025 15:58:34 +0200 Subject: [PATCH 12/15] fix: use `better-sqlite3` connector --- playground/server/api/cached.ts | 15 +++++++++++++++ src/utils/database.ts | 2 +- 2 files changed, 16 insertions(+), 1 deletion(-) create mode 100644 playground/server/api/cached.ts diff --git a/playground/server/api/cached.ts b/playground/server/api/cached.ts new file mode 100644 index 00000000..2a8de8be --- /dev/null +++ b/playground/server/api/cached.ts @@ -0,0 +1,15 @@ +const test = defineCachedFunction((_event) => { + return 'test' +}, { + getKey: () => 'test' +}) + +export default cachedEventHandler(async (event) => { + return { + now: Date.now(), + test: test(event) + } +}, { + maxAge: 10, + swr: true +}) \ No newline at end of file diff --git a/src/utils/database.ts b/src/utils/database.ts index 8f08fba9..8ea72ec9 100644 --- a/src/utils/database.ts +++ b/src/utils/database.ts @@ -18,7 +18,7 @@ export async function getDb0Connector(nitro: Nitro) { throw new Error('No database connector configured in nitro.options.database.db') } - const connector = dbConfig.connector + const connector = dbConfig.connector === 'sqlite' ? 'better-sqlite3' : dbConfig.connector const connectorPath = `db0/connectors/${connector}` try { From 3019a15f08846b4d8c96ade8639c8caca12a681d Mon Sep 17 00:00:00 2001 From: "autofix-ci[bot]" <114827586+autofix-ci[bot]@users.noreply.github.com> Date: Mon, 15 Sep 2025 13:59:44 +0000 Subject: [PATCH 13/15] [autofix.ci] apply automated fixes --- playground/server/api/cached.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/playground/server/api/cached.ts b/playground/server/api/cached.ts index 2a8de8be..b62fd1a0 100644 --- a/playground/server/api/cached.ts +++ b/playground/server/api/cached.ts @@ -12,4 +12,4 @@ export default cachedEventHandler(async (event) => { }, { maxAge: 10, swr: true -}) \ No newline at end of file +}) From a5b3c2771eae40b91ae1592a8658ba8e751447ca Mon Sep 17 00:00:00 2001 From: Farnabaz Date: Wed, 17 Sep 2025 10:44:45 +0200 Subject: [PATCH 14/15] apply reviews --- docs/content/docs/2.features/blob.md | 10 +++--- package.json | 1 - pnpm-lock.yaml | 3 -- src/runtime/blob/server/helpers/blob-fs.ts | 35 +++++++++++-------- src/runtime/blob/server/helpers/blob-s3.ts | 4 --- .../blob/server/helpers/blob-vercel.ts | 4 +-- src/types/blob.ts | 5 ++- 7 files changed, 32 insertions(+), 30 deletions(-) diff --git a/docs/content/docs/2.features/blob.md b/docs/content/docs/2.features/blob.md index 29e19e4e..50aeed20 100644 --- a/docs/content/docs/2.features/blob.md +++ b/docs/content/docs/2.features/blob.md @@ -437,10 +437,10 @@ See [`useMultipartUpload()`](#usemultipartupload) on usage details. ### `createMultipartUpload()` ::note -We suggest to use [`handleMultipartUpload()`](#handlemultipartupload) method to handle the multipart upload request. +We suggest using [`handleMultipartUpload()`](#handlemultipartupload) method to handle the multipart upload request. :br :br -If you like to handle multi-part 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). +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). :: Start a new multipart upload. @@ -487,7 +487,7 @@ Returns a `BlobMultipartUpload` ### `resumeMultipartUpload()` ::note -We suggest to use [`handleMultipartUpload()`](#handlemultipartupload) method to handle the multipart upload request. +We suggest using [`handleMultipartUpload()`](#handlemultipartupload) method to handle the multipart upload request. :: Continue processing of unfinished multipart upload. @@ -815,7 +815,7 @@ bucket_name = "my-bucket" #### Using S3-Compatible Driver -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 Blob. +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. ```ts [nuxt.config.ts] export default defineNuxtConfig({ @@ -859,7 +859,7 @@ For additional storage providers and configuration options, see the unstorage do :: ::note -Other unstorage drivers do not support multi-part upload. If you want to upload big files, consider using one of the above providers. +Other unstorage drivers do not support multipart upload. If you want to upload large files, consider using one of the above providers. :: ## Types diff --git a/package.json b/package.json index b8e6d5f2..4c7edd29 100644 --- a/package.json +++ b/package.json @@ -55,7 +55,6 @@ "@nuxt/devtools-kit": "^2.6.3", "@uploadthing/mime-types": "^0.3.6", "@vercel/blob": "^1.1.1", - "confbox": "^0.2.2", "db0": "^0.3.2", "defu": "^6.1.4", "destr": "^2.0.5", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 03d1444d..514f0489 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -20,9 +20,6 @@ importers: '@vercel/blob': specifier: ^1.1.1 version: 1.1.1 - confbox: - specifier: ^0.2.2 - version: 0.2.2 db0: specifier: ^0.3.2 version: 0.3.2(better-sqlite3@12.2.0)(drizzle-orm@0.44.5(@cloudflare/workers-types@4.20250913.0)(@opentelemetry/api@1.9.0)(better-sqlite3@12.2.0)) diff --git a/src/runtime/blob/server/helpers/blob-fs.ts b/src/runtime/blob/server/helpers/blob-fs.ts index 6c5e4dc4..7c1c7e73 100644 --- a/src/runtime/blob/server/helpers/blob-fs.ts +++ b/src/runtime/blob/server/helpers/blob-fs.ts @@ -1,7 +1,7 @@ import type { BlobMultipartUpload, BlobObject, BlobUploadedPart } from '~/src/types/blob' import { randomUUID } from 'uncrypto' import { join } from 'pathe' -import fs from 'node:fs' +import fs, { read } from 'node:fs' import fsp from 'node:fs/promises' import type { Driver } from 'unstorage' @@ -78,12 +78,13 @@ async function createOrResumeMultipartUpload(driver: Driver, pathname: string, u } } - await completeUpload(driver, pathname, uploadId, uploadedParts) + const size = await completeUpload(driver, pathname, uploadId, uploadedParts) await cleanUploadTmpFiles(driver, uploadId, pathname) return { pathname: currentMetadata.pathname, contentType: currentMetadata.contentType, + size, httpEtag: `"${randomUUID()}"`, uploadedAt: new Date(), httpMetadata: { @@ -109,22 +110,28 @@ async function cleanUploadTmpFiles(driver: Driver, uploadId: string, pathname: s await driver.removeItem!(pathname + '$', {}) } -async function completeUpload(driver: Driver, pathname: string, uploadId: string, uploadedParts: BlobUploadedPart[]) { +async function completeUpload(driver: Driver, pathname: string, uploadId: string, uploadedParts: BlobUploadedPart[]): Promise { const root = driver.options.base const fullPath = join(root, pathname) const orderedUploadedParts = uploadedParts.sort((a, b) => a.partNumber - b.partNumber) - const writeStream = fs.createWriteStream(fullPath) - for (const file of orderedUploadedParts) { - await new Promise((resolve, reject) => { - const readStream = fs.createReadStream(join(root, partKey(uploadId, file.partNumber))) - readStream.pipe(writeStream, { end: false }) - readStream.on('error', reject) - readStream.on('end', resolve as () => void) - }) - } + return new Promise(async (resolve, reject) => { + const writeStream = fs.createWriteStream(fullPath) + for (const file of orderedUploadedParts) { + await new Promise((resolve, reject) => { + const readStream = fs.createReadStream(join(root, partKey(uploadId, file.partNumber))) + readStream.pipe(writeStream, { end: false }) + readStream.on('error', reject) + readStream.on('end', resolve as () => void) + }) + } - writeStream.end() - writeStream.on('finish', () => { + writeStream.end() + writeStream.on('finish', () => { + resolve(fs.statSync(fullPath).size) + }) + writeStream.on('error', (error) => { + reject(error) + }) }) } diff --git a/src/runtime/blob/server/helpers/blob-s3.ts b/src/runtime/blob/server/helpers/blob-s3.ts index 47a4705f..0998ce8f 100644 --- a/src/runtime/blob/server/helpers/blob-s3.ts +++ b/src/runtime/blob/server/helpers/blob-s3.ts @@ -11,11 +11,9 @@ export async function createMultipartUpload(pathname: string, driverOptions: S3D }) const res = await aws.fetch(`${baseUrl(driverOptions, pathname)}?uploads`, { method: 'POST', headers: buildCreateHeaders(options) }).catch((e) => { - console.log('error', e) throw e }) if (!res.ok) { - console.log('error', res) throw new Error(`Initiate failed: ${res.status} ${res.statusText}`) } const xmlText = await res.text() @@ -92,12 +90,10 @@ export async function resumeMultipartUpload(pathname: string, uploadId: string, } const xmlText = await res.text() - console.log('xmlText', xmlText) return { pathname, contentType: 'application/octet-stream', url: xmlTagContent(xmlText, 'Location'), - size: 0, httpEtag: xmlTagContent(xmlText, 'ETag'), uploadedAt: new Date(), httpMetadata: {}, diff --git a/src/runtime/blob/server/helpers/blob-vercel.ts b/src/runtime/blob/server/helpers/blob-vercel.ts index e6c2297e..c292be81 100644 --- a/src/runtime/blob/server/helpers/blob-vercel.ts +++ b/src/runtime/blob/server/helpers/blob-vercel.ts @@ -24,7 +24,7 @@ export async function createMultipartUpload(token: string, pathname: string, opt }) }, abort: async () => { - // await mpu.abort() + // Vercel Blob does not support aborting a multipart upload }, complete: async (uploadedParts) => { const r2Object = await vercelCompleteMultipartUpload(pathname, uploadedParts, { @@ -52,7 +52,7 @@ export async function resumeMultipartUpload(token: string, pathname: string, upl }) }, abort: async () => { - // await mpu.abort() + // Vercel Blob does not support aborting a multipart upload }, complete: async (uploadedParts) => { const putBlobResult = await vercelCompleteMultipartUpload(pathname, uploadedParts, { diff --git a/src/types/blob.ts b/src/types/blob.ts index e863b48a..a8387589 100644 --- a/src/types/blob.ts +++ b/src/types/blob.ts @@ -18,7 +18,10 @@ export interface BlobObject { */ contentType: string | undefined /** - * The size of the blob in bytes. + * The size of the blob in bytes + * + * Some drivers do not return the size of the blob at the time of upload. This is supported in the `fs` and `cloudflare-r2` drivers. + * In the Vercel Blob and S3 drivers, the size is missing. */ size?: number /** From 9159edfd8800fa1ce7a55e952cde8298135d495b Mon Sep 17 00:00:00 2001 From: Farnabaz Date: Wed, 17 Sep 2025 10:50:45 +0200 Subject: [PATCH 15/15] fix build --- package.json | 1 + pnpm-lock.yaml | 71 ++++++++++++---------- src/runtime/blob/server/helpers/blob-fs.ts | 22 +++---- src/types/blob.ts | 2 +- 4 files changed, 53 insertions(+), 43 deletions(-) diff --git a/package.json b/package.json index 4c7edd29..7dfbe855 100644 --- a/package.json +++ b/package.json @@ -55,6 +55,7 @@ "@nuxt/devtools-kit": "^2.6.3", "@uploadthing/mime-types": "^0.3.6", "@vercel/blob": "^1.1.1", + "aws4fetch": "^1.0.20", "db0": "^0.3.2", "defu": "^6.1.4", "destr": "^2.0.5", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 514f0489..cb12698e 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -20,6 +20,9 @@ importers: '@vercel/blob': specifier: ^1.1.1 version: 1.1.1 + aws4fetch: + specifier: ^1.0.20 + version: 1.0.20 db0: specifier: ^0.3.2 version: 0.3.2(better-sqlite3@12.2.0)(drizzle-orm@0.44.5(@cloudflare/workers-types@4.20250913.0)(@opentelemetry/api@1.9.0)(better-sqlite3@12.2.0)) @@ -67,7 +70,7 @@ importers: version: 0.1.3 unstorage: specifier: ^1.17.1 - version: 1.17.1(@vercel/blob@1.1.1)(db0@0.3.2(better-sqlite3@12.2.0)(drizzle-orm@0.44.5(@cloudflare/workers-types@4.20250913.0)(@opentelemetry/api@1.9.0)(better-sqlite3@12.2.0)))(ioredis@5.7.0) + version: 1.17.1(@vercel/blob@1.1.1)(aws4fetch@1.0.20)(db0@0.3.2(better-sqlite3@12.2.0)(drizzle-orm@0.44.5(@cloudflare/workers-types@4.20250913.0)(@opentelemetry/api@1.9.0)(better-sqlite3@12.2.0)))(ioredis@5.7.0) zod: specifier: ^3.25.34 version: 3.25.76 @@ -104,7 +107,7 @@ importers: version: 9.35.0(jiti@2.5.1) nuxt: specifier: ^4.1.2 - version: 4.1.2(@parcel/watcher@2.5.1)(@types/node@24.5.0)(@vercel/blob@1.1.1)(@vue/compiler-sfc@3.5.21)(better-sqlite3@12.2.0)(db0@0.3.2(better-sqlite3@12.2.0)(drizzle-orm@0.44.5(@cloudflare/workers-types@4.20250913.0)(@opentelemetry/api@1.9.0)(better-sqlite3@12.2.0)))(drizzle-orm@0.44.5(@cloudflare/workers-types@4.20250913.0)(@opentelemetry/api@1.9.0)(better-sqlite3@12.2.0))(eslint@9.35.0(jiti@2.5.1))(ioredis@5.7.0)(lightningcss@1.30.1)(magicast@0.3.5)(optionator@0.9.4)(rollup@4.50.0)(terser@5.44.0)(typescript@5.9.2)(vite@7.1.5(@types/node@24.5.0)(jiti@2.5.1)(lightningcss@1.30.1)(terser@5.44.0)(yaml@2.8.1))(vue-tsc@3.0.7(typescript@5.9.2))(yaml@2.8.1) + version: 4.1.2(@parcel/watcher@2.5.1)(@types/node@24.5.0)(@vercel/blob@1.1.1)(@vue/compiler-sfc@3.5.21)(aws4fetch@1.0.20)(better-sqlite3@12.2.0)(db0@0.3.2(better-sqlite3@12.2.0)(drizzle-orm@0.44.5(@cloudflare/workers-types@4.20250913.0)(@opentelemetry/api@1.9.0)(better-sqlite3@12.2.0)))(drizzle-orm@0.44.5(@cloudflare/workers-types@4.20250913.0)(@opentelemetry/api@1.9.0)(better-sqlite3@12.2.0))(eslint@9.35.0(jiti@2.5.1))(ioredis@5.7.0)(lightningcss@1.30.1)(magicast@0.3.5)(optionator@0.9.4)(rollup@4.50.0)(terser@5.44.0)(typescript@5.9.2)(vite@7.1.5(@types/node@24.5.0)(jiti@2.5.1)(lightningcss@1.30.1)(terser@5.44.0)(yaml@2.8.1))(vue-tsc@3.0.7(typescript@5.9.2))(yaml@2.8.1) typescript: specifier: ~5.9.2 version: 5.9.2 @@ -140,13 +143,13 @@ importers: version: 3.7.0(better-sqlite3@12.2.0)(drizzle-orm@0.44.5(@cloudflare/workers-types@4.20250913.0)(@opentelemetry/api@1.9.0)(better-sqlite3@12.2.0))(magicast@0.3.5)(valibot@1.1.0(typescript@5.9.2))(zod-to-json-schema@3.24.6(zod@3.25.76))(zod@3.25.76) '@nuxt/image': specifier: ^1.10.0 - version: 1.11.0(@vercel/blob@1.1.1)(db0@0.3.2(better-sqlite3@12.2.0)(drizzle-orm@0.44.5(@cloudflare/workers-types@4.20250913.0)(@opentelemetry/api@1.9.0)(better-sqlite3@12.2.0)))(ioredis@5.7.0)(magicast@0.3.5) + version: 1.11.0(@vercel/blob@1.1.1)(aws4fetch@1.0.20)(db0@0.3.2(better-sqlite3@12.2.0)(drizzle-orm@0.44.5(@cloudflare/workers-types@4.20250913.0)(@opentelemetry/api@1.9.0)(better-sqlite3@12.2.0)))(ioredis@5.7.0)(magicast@0.3.5) '@nuxt/scripts': specifier: ^0.11.6 - version: 0.11.13(@unhead/vue@2.0.14(vue@3.5.21(typescript@5.9.2)))(@vercel/blob@1.1.1)(db0@0.3.2(better-sqlite3@12.2.0)(drizzle-orm@0.44.5(@cloudflare/workers-types@4.20250913.0)(@opentelemetry/api@1.9.0)(better-sqlite3@12.2.0)))(ioredis@5.7.0)(magicast@0.3.5)(typescript@5.9.2)(vue@3.5.21(typescript@5.9.2)) + version: 0.11.13(@unhead/vue@2.0.14(vue@3.5.21(typescript@5.9.2)))(@vercel/blob@1.1.1)(aws4fetch@1.0.20)(db0@0.3.2(better-sqlite3@12.2.0)(drizzle-orm@0.44.5(@cloudflare/workers-types@4.20250913.0)(@opentelemetry/api@1.9.0)(better-sqlite3@12.2.0)))(ioredis@5.7.0)(magicast@0.3.5)(typescript@5.9.2)(vue@3.5.21(typescript@5.9.2)) '@nuxt/ui': specifier: 4.0.0-alpha.1 - version: 4.0.0-alpha.1(@babel/parser@7.28.3)(@vercel/blob@1.1.1)(change-case@5.4.4)(db0@0.3.2(better-sqlite3@12.2.0)(drizzle-orm@0.44.5(@cloudflare/workers-types@4.20250913.0)(@opentelemetry/api@1.9.0)(better-sqlite3@12.2.0)))(embla-carousel@8.6.0)(ioredis@5.7.0)(magicast@0.3.5)(react@19.1.1)(typescript@5.9.2)(valibot@1.1.0(typescript@5.9.2))(vite@7.1.5(@types/node@24.5.0)(jiti@2.5.1)(lightningcss@1.30.1)(terser@5.44.0)(yaml@2.8.1))(vue-router@4.5.1(vue@3.5.21(typescript@5.9.2)))(vue@3.5.21(typescript@5.9.2))(zod@3.25.76) + version: 4.0.0-alpha.1(@babel/parser@7.28.3)(@vercel/blob@1.1.1)(aws4fetch@1.0.20)(change-case@5.4.4)(db0@0.3.2(better-sqlite3@12.2.0)(drizzle-orm@0.44.5(@cloudflare/workers-types@4.20250913.0)(@opentelemetry/api@1.9.0)(better-sqlite3@12.2.0)))(embla-carousel@8.6.0)(ioredis@5.7.0)(magicast@0.3.5)(react@19.1.1)(typescript@5.9.2)(valibot@1.1.0(typescript@5.9.2))(vite@7.1.5(@types/node@24.5.0)(jiti@2.5.1)(lightningcss@1.30.1)(terser@5.44.0)(yaml@2.8.1))(vue-router@4.5.1(vue@3.5.21(typescript@5.9.2)))(vue@3.5.21(typescript@5.9.2))(zod@3.25.76) '@rollup/plugin-yaml': specifier: ^4.1.2 version: 4.1.2(rollup@4.50.0) @@ -161,7 +164,7 @@ importers: version: 13.9.0(vue@3.5.21(typescript@5.9.2)) '@vueuse/nuxt': specifier: ^13.1.0 - version: 13.9.0(magicast@0.3.5)(nuxt@4.1.2(@parcel/watcher@2.5.1)(@types/node@24.5.0)(@vercel/blob@1.1.1)(@vue/compiler-sfc@3.5.21)(better-sqlite3@12.2.0)(db0@0.3.2(better-sqlite3@12.2.0)(drizzle-orm@0.44.5(@cloudflare/workers-types@4.20250913.0)(@opentelemetry/api@1.9.0)(better-sqlite3@12.2.0)))(drizzle-orm@0.44.5(@cloudflare/workers-types@4.20250913.0)(@opentelemetry/api@1.9.0)(better-sqlite3@12.2.0))(eslint@9.35.0(jiti@2.5.1))(ioredis@5.7.0)(lightningcss@1.30.1)(magicast@0.3.5)(optionator@0.9.4)(rollup@4.50.0)(terser@5.44.0)(typescript@5.9.2)(vite@7.1.5(@types/node@24.5.0)(jiti@2.5.1)(lightningcss@1.30.1)(terser@5.44.0)(yaml@2.8.1))(vue-tsc@3.0.7(typescript@5.9.2))(yaml@2.8.1))(vue@3.5.21(typescript@5.9.2)) + version: 13.9.0(magicast@0.3.5)(nuxt@4.1.2(@parcel/watcher@2.5.1)(@types/node@24.5.0)(@vercel/blob@1.1.1)(@vue/compiler-sfc@3.5.21)(aws4fetch@1.0.20)(better-sqlite3@12.2.0)(db0@0.3.2(better-sqlite3@12.2.0)(drizzle-orm@0.44.5(@cloudflare/workers-types@4.20250913.0)(@opentelemetry/api@1.9.0)(better-sqlite3@12.2.0)))(drizzle-orm@0.44.5(@cloudflare/workers-types@4.20250913.0)(@opentelemetry/api@1.9.0)(better-sqlite3@12.2.0))(eslint@9.35.0(jiti@2.5.1))(ioredis@5.7.0)(lightningcss@1.30.1)(magicast@0.3.5)(optionator@0.9.4)(rollup@4.50.0)(terser@5.44.0)(typescript@5.9.2)(vite@7.1.5(@types/node@24.5.0)(jiti@2.5.1)(lightningcss@1.30.1)(terser@5.44.0)(yaml@2.8.1))(vue-tsc@3.0.7(typescript@5.9.2))(yaml@2.8.1))(vue@3.5.21(typescript@5.9.2)) better-sqlite3: specifier: ^12.2.0 version: 12.2.0 @@ -170,13 +173,13 @@ importers: version: 5.1.0 nuxt: specifier: ^4.1.2 - version: 4.1.2(@parcel/watcher@2.5.1)(@types/node@24.5.0)(@vercel/blob@1.1.1)(@vue/compiler-sfc@3.5.21)(better-sqlite3@12.2.0)(db0@0.3.2(better-sqlite3@12.2.0)(drizzle-orm@0.44.5(@cloudflare/workers-types@4.20250913.0)(@opentelemetry/api@1.9.0)(better-sqlite3@12.2.0)))(drizzle-orm@0.44.5(@cloudflare/workers-types@4.20250913.0)(@opentelemetry/api@1.9.0)(better-sqlite3@12.2.0))(eslint@9.35.0(jiti@2.5.1))(ioredis@5.7.0)(lightningcss@1.30.1)(magicast@0.3.5)(optionator@0.9.4)(rollup@4.50.0)(terser@5.44.0)(typescript@5.9.2)(vite@7.1.5(@types/node@24.5.0)(jiti@2.5.1)(lightningcss@1.30.1)(terser@5.44.0)(yaml@2.8.1))(vue-tsc@3.0.7(typescript@5.9.2))(yaml@2.8.1) + version: 4.1.2(@parcel/watcher@2.5.1)(@types/node@24.5.0)(@vercel/blob@1.1.1)(@vue/compiler-sfc@3.5.21)(aws4fetch@1.0.20)(better-sqlite3@12.2.0)(db0@0.3.2(better-sqlite3@12.2.0)(drizzle-orm@0.44.5(@cloudflare/workers-types@4.20250913.0)(@opentelemetry/api@1.9.0)(better-sqlite3@12.2.0)))(drizzle-orm@0.44.5(@cloudflare/workers-types@4.20250913.0)(@opentelemetry/api@1.9.0)(better-sqlite3@12.2.0))(eslint@9.35.0(jiti@2.5.1))(ioredis@5.7.0)(lightningcss@1.30.1)(magicast@0.3.5)(optionator@0.9.4)(rollup@4.50.0)(terser@5.44.0)(typescript@5.9.2)(vite@7.1.5(@types/node@24.5.0)(jiti@2.5.1)(lightningcss@1.30.1)(terser@5.44.0)(yaml@2.8.1))(vue-tsc@3.0.7(typescript@5.9.2))(yaml@2.8.1) nuxt-llms: specifier: ^0.1.2 version: 0.1.3(magicast@0.3.5) nuxt-og-image: specifier: ^5.1.2 - version: 5.1.9(@unhead/vue@2.0.14(vue@3.5.21(typescript@5.9.2)))(magicast@0.3.5)(unstorage@1.17.1(@vercel/blob@1.1.1)(db0@0.3.2(better-sqlite3@12.2.0)(drizzle-orm@0.44.5(@cloudflare/workers-types@4.20250913.0)(@opentelemetry/api@1.9.0)(better-sqlite3@12.2.0)))(ioredis@5.7.0))(vite@7.1.5(@types/node@24.5.0)(jiti@2.5.1)(lightningcss@1.30.1)(terser@5.44.0)(yaml@2.8.1))(vue@3.5.21(typescript@5.9.2)) + version: 5.1.9(@unhead/vue@2.0.14(vue@3.5.21(typescript@5.9.2)))(magicast@0.3.5)(unstorage@1.17.1(@vercel/blob@1.1.1)(aws4fetch@1.0.20)(db0@0.3.2(better-sqlite3@12.2.0)(drizzle-orm@0.44.5(@cloudflare/workers-types@4.20250913.0)(@opentelemetry/api@1.9.0)(better-sqlite3@12.2.0)))(ioredis@5.7.0))(vite@7.1.5(@types/node@24.5.0)(jiti@2.5.1)(lightningcss@1.30.1)(terser@5.44.0)(yaml@2.8.1))(vue@3.5.21(typescript@5.9.2)) playground: dependencies: @@ -191,7 +194,7 @@ importers: version: 1.7.0(magicast@0.3.5) '@nuxt/ui': specifier: 4.0.0-alpha.1 - version: 4.0.0-alpha.1(@babel/parser@7.28.3)(@vercel/blob@1.1.1)(change-case@5.4.4)(db0@0.3.2(better-sqlite3@12.2.0)(drizzle-orm@0.44.5(@cloudflare/workers-types@4.20250913.0)(@opentelemetry/api@1.9.0)(better-sqlite3@12.2.0)))(embla-carousel@8.6.0)(ioredis@5.7.0)(magicast@0.3.5)(react@19.1.1)(typescript@5.9.2)(valibot@1.1.0(typescript@5.9.2))(vite@7.1.5(@types/node@24.5.0)(jiti@2.5.1)(lightningcss@1.30.1)(terser@5.44.0)(yaml@2.8.1))(vue-router@4.5.1(vue@3.5.21(typescript@5.9.2)))(vue@3.5.21(typescript@5.9.2))(zod@3.25.76) + version: 4.0.0-alpha.1(@babel/parser@7.28.3)(@vercel/blob@1.1.1)(aws4fetch@1.0.20)(change-case@5.4.4)(db0@0.3.2(better-sqlite3@12.2.0)(drizzle-orm@0.44.5(@cloudflare/workers-types@4.20250913.0)(@opentelemetry/api@1.9.0)(better-sqlite3@12.2.0)))(embla-carousel@8.6.0)(ioredis@5.7.0)(magicast@0.3.5)(react@19.1.1)(typescript@5.9.2)(valibot@1.1.0(typescript@5.9.2))(vite@7.1.5(@types/node@24.5.0)(jiti@2.5.1)(lightningcss@1.30.1)(terser@5.44.0)(yaml@2.8.1))(vue-router@4.5.1(vue@3.5.21(typescript@5.9.2)))(vue@3.5.21(typescript@5.9.2))(zod@3.25.76) '@nuxthub/core': specifier: workspace:* version: link:.. @@ -203,7 +206,7 @@ importers: version: 13.9.0(vue@3.5.21(typescript@5.9.2)) '@vueuse/nuxt': specifier: ^13.9.0 - version: 13.9.0(magicast@0.3.5)(nuxt@4.1.2(@parcel/watcher@2.5.1)(@types/node@24.5.0)(@vercel/blob@1.1.1)(@vue/compiler-sfc@3.5.21)(better-sqlite3@12.2.0)(db0@0.3.2(better-sqlite3@12.2.0)(drizzle-orm@0.44.5(@cloudflare/workers-types@4.20250913.0)(@opentelemetry/api@1.9.0)(better-sqlite3@12.2.0)))(drizzle-orm@0.44.5(@cloudflare/workers-types@4.20250913.0)(@opentelemetry/api@1.9.0)(better-sqlite3@12.2.0))(eslint@9.35.0(jiti@2.5.1))(ioredis@5.7.0)(lightningcss@1.30.1)(magicast@0.3.5)(optionator@0.9.4)(rollup@4.50.0)(terser@5.44.0)(typescript@5.9.2)(vite@7.1.5(@types/node@24.5.0)(jiti@2.5.1)(lightningcss@1.30.1)(terser@5.44.0)(yaml@2.8.1))(vue-tsc@3.0.7(typescript@5.9.2))(yaml@2.8.1))(vue@3.5.21(typescript@5.9.2)) + version: 13.9.0(magicast@0.3.5)(nuxt@4.1.2(@parcel/watcher@2.5.1)(@types/node@24.5.0)(@vercel/blob@1.1.1)(@vue/compiler-sfc@3.5.21)(aws4fetch@1.0.20)(better-sqlite3@12.2.0)(db0@0.3.2(better-sqlite3@12.2.0)(drizzle-orm@0.44.5(@cloudflare/workers-types@4.20250913.0)(@opentelemetry/api@1.9.0)(better-sqlite3@12.2.0)))(drizzle-orm@0.44.5(@cloudflare/workers-types@4.20250913.0)(@opentelemetry/api@1.9.0)(better-sqlite3@12.2.0))(eslint@9.35.0(jiti@2.5.1))(ioredis@5.7.0)(lightningcss@1.30.1)(magicast@0.3.5)(optionator@0.9.4)(rollup@4.50.0)(terser@5.44.0)(typescript@5.9.2)(vite@7.1.5(@types/node@24.5.0)(jiti@2.5.1)(lightningcss@1.30.1)(terser@5.44.0)(yaml@2.8.1))(vue-tsc@3.0.7(typescript@5.9.2))(yaml@2.8.1))(vue@3.5.21(typescript@5.9.2)) ai: specifier: ^4.3.15 version: 4.3.19(react@19.1.1)(zod@3.25.76) @@ -212,7 +215,7 @@ importers: version: 0.44.5(@cloudflare/workers-types@4.20250913.0)(@opentelemetry/api@1.9.0)(better-sqlite3@12.2.0) nuxt: specifier: ^4.1.2 - version: 4.1.2(@parcel/watcher@2.5.1)(@types/node@24.5.0)(@vercel/blob@1.1.1)(@vue/compiler-sfc@3.5.21)(better-sqlite3@12.2.0)(db0@0.3.2(better-sqlite3@12.2.0)(drizzle-orm@0.44.5(@cloudflare/workers-types@4.20250913.0)(@opentelemetry/api@1.9.0)(better-sqlite3@12.2.0)))(drizzle-orm@0.44.5(@cloudflare/workers-types@4.20250913.0)(@opentelemetry/api@1.9.0)(better-sqlite3@12.2.0))(eslint@9.35.0(jiti@2.5.1))(ioredis@5.7.0)(lightningcss@1.30.1)(magicast@0.3.5)(optionator@0.9.4)(rollup@4.50.0)(terser@5.44.0)(typescript@5.9.2)(vite@7.1.5(@types/node@24.5.0)(jiti@2.5.1)(lightningcss@1.30.1)(terser@5.44.0)(yaml@2.8.1))(vue-tsc@3.0.7(typescript@5.9.2))(yaml@2.8.1) + version: 4.1.2(@parcel/watcher@2.5.1)(@types/node@24.5.0)(@vercel/blob@1.1.1)(@vue/compiler-sfc@3.5.21)(aws4fetch@1.0.20)(better-sqlite3@12.2.0)(db0@0.3.2(better-sqlite3@12.2.0)(drizzle-orm@0.44.5(@cloudflare/workers-types@4.20250913.0)(@opentelemetry/api@1.9.0)(better-sqlite3@12.2.0)))(drizzle-orm@0.44.5(@cloudflare/workers-types@4.20250913.0)(@opentelemetry/api@1.9.0)(better-sqlite3@12.2.0))(eslint@9.35.0(jiti@2.5.1))(ioredis@5.7.0)(lightningcss@1.30.1)(magicast@0.3.5)(optionator@0.9.4)(rollup@4.50.0)(terser@5.44.0)(typescript@5.9.2)(vite@7.1.5(@types/node@24.5.0)(jiti@2.5.1)(lightningcss@1.30.1)(terser@5.44.0)(yaml@2.8.1))(vue-tsc@3.0.7(typescript@5.9.2))(yaml@2.8.1) nuxt-auth-utils: specifier: ^0.5.24 version: 0.5.24(magicast@0.3.5) @@ -2582,6 +2585,9 @@ packages: peerDependencies: postcss: ^8.1.0 + aws4fetch@1.0.20: + resolution: {integrity: sha512-/djoAN709iY65ETD6LKCtyyEI04XIBP5xVvfmNxsEP0uJB5tyaGBztSryRr4HqMStr9R06PisQE7m9zDTXKu6g==} + b4a@1.6.7: resolution: {integrity: sha512-OnAYlL5b7LEkALw87fUVafQw5rVR9RjwGd4KUwNQ6DrrNmaVaUCgLipfVlzrPQ4tWOR9P0IXGNOx50jYCCdSJg==} @@ -7220,7 +7226,7 @@ snapshots: - supports-color - typescript - '@nuxt/fonts@0.11.4(@vercel/blob@1.1.1)(db0@0.3.2(better-sqlite3@12.2.0)(drizzle-orm@0.44.5(@cloudflare/workers-types@4.20250913.0)(@opentelemetry/api@1.9.0)(better-sqlite3@12.2.0)))(ioredis@5.7.0)(magicast@0.3.5)(vite@7.1.5(@types/node@24.5.0)(jiti@2.5.1)(lightningcss@1.30.1)(terser@5.44.0)(yaml@2.8.1))': + '@nuxt/fonts@0.11.4(@vercel/blob@1.1.1)(aws4fetch@1.0.20)(db0@0.3.2(better-sqlite3@12.2.0)(drizzle-orm@0.44.5(@cloudflare/workers-types@4.20250913.0)(@opentelemetry/api@1.9.0)(better-sqlite3@12.2.0)))(ioredis@5.7.0)(magicast@0.3.5)(vite@7.1.5(@types/node@24.5.0)(jiti@2.5.1)(lightningcss@1.30.1)(terser@5.44.0)(yaml@2.8.1))': dependencies: '@nuxt/devtools-kit': 2.6.3(magicast@0.3.5)(vite@7.1.5(@types/node@24.5.0)(jiti@2.5.1)(lightningcss@1.30.1)(terser@5.44.0)(yaml@2.8.1)) '@nuxt/kit': 3.19.0(magicast@0.3.5) @@ -7241,7 +7247,7 @@ snapshots: ufo: 1.6.1 unifont: 0.4.1 unplugin: 2.3.10 - unstorage: 1.17.1(@vercel/blob@1.1.1)(db0@0.3.2(better-sqlite3@12.2.0)(drizzle-orm@0.44.5(@cloudflare/workers-types@4.20250913.0)(@opentelemetry/api@1.9.0)(better-sqlite3@12.2.0)))(ioredis@5.7.0) + unstorage: 1.17.1(@vercel/blob@1.1.1)(aws4fetch@1.0.20)(db0@0.3.2(better-sqlite3@12.2.0)(drizzle-orm@0.44.5(@cloudflare/workers-types@4.20250913.0)(@opentelemetry/api@1.9.0)(better-sqlite3@12.2.0)))(ioredis@5.7.0) transitivePeerDependencies: - '@azure/app-configuration' - '@azure/cosmos' @@ -7288,7 +7294,7 @@ snapshots: - vite - vue - '@nuxt/image@1.11.0(@vercel/blob@1.1.1)(db0@0.3.2(better-sqlite3@12.2.0)(drizzle-orm@0.44.5(@cloudflare/workers-types@4.20250913.0)(@opentelemetry/api@1.9.0)(better-sqlite3@12.2.0)))(ioredis@5.7.0)(magicast@0.3.5)': + '@nuxt/image@1.11.0(@vercel/blob@1.1.1)(aws4fetch@1.0.20)(db0@0.3.2(better-sqlite3@12.2.0)(drizzle-orm@0.44.5(@cloudflare/workers-types@4.20250913.0)(@opentelemetry/api@1.9.0)(better-sqlite3@12.2.0)))(ioredis@5.7.0)(magicast@0.3.5)': dependencies: '@nuxt/kit': 3.19.0(magicast@0.3.5) consola: 3.4.2 @@ -7301,7 +7307,7 @@ snapshots: std-env: 3.9.0 ufo: 1.6.1 optionalDependencies: - ipx: 2.1.1(@vercel/blob@1.1.1)(db0@0.3.2(better-sqlite3@12.2.0)(drizzle-orm@0.44.5(@cloudflare/workers-types@4.20250913.0)(@opentelemetry/api@1.9.0)(better-sqlite3@12.2.0)))(ioredis@5.7.0) + ipx: 2.1.1(@vercel/blob@1.1.1)(aws4fetch@1.0.20)(db0@0.3.2(better-sqlite3@12.2.0)(drizzle-orm@0.44.5(@cloudflare/workers-types@4.20250913.0)(@opentelemetry/api@1.9.0)(better-sqlite3@12.2.0)))(ioredis@5.7.0) transitivePeerDependencies: - '@azure/app-configuration' - '@azure/cosmos' @@ -7449,7 +7455,7 @@ snapshots: std-env: 3.9.0 ufo: 1.6.1 - '@nuxt/scripts@0.11.13(@unhead/vue@2.0.14(vue@3.5.21(typescript@5.9.2)))(@vercel/blob@1.1.1)(db0@0.3.2(better-sqlite3@12.2.0)(drizzle-orm@0.44.5(@cloudflare/workers-types@4.20250913.0)(@opentelemetry/api@1.9.0)(better-sqlite3@12.2.0)))(ioredis@5.7.0)(magicast@0.3.5)(typescript@5.9.2)(vue@3.5.21(typescript@5.9.2))': + '@nuxt/scripts@0.11.13(@unhead/vue@2.0.14(vue@3.5.21(typescript@5.9.2)))(@vercel/blob@1.1.1)(aws4fetch@1.0.20)(db0@0.3.2(better-sqlite3@12.2.0)(drizzle-orm@0.44.5(@cloudflare/workers-types@4.20250913.0)(@opentelemetry/api@1.9.0)(better-sqlite3@12.2.0)))(ioredis@5.7.0)(magicast@0.3.5)(typescript@5.9.2)(vue@3.5.21(typescript@5.9.2))': dependencies: '@nuxt/kit': 4.1.2(magicast@0.3.5) '@unhead/vue': 2.0.14(vue@3.5.21(typescript@5.9.2)) @@ -7466,7 +7472,7 @@ snapshots: std-env: 3.9.0 ufo: 1.6.1 unplugin: 2.3.10 - unstorage: 1.17.1(@vercel/blob@1.1.1)(db0@0.3.2(better-sqlite3@12.2.0)(drizzle-orm@0.44.5(@cloudflare/workers-types@4.20250913.0)(@opentelemetry/api@1.9.0)(better-sqlite3@12.2.0)))(ioredis@5.7.0) + unstorage: 1.17.1(@vercel/blob@1.1.1)(aws4fetch@1.0.20)(db0@0.3.2(better-sqlite3@12.2.0)(drizzle-orm@0.44.5(@cloudflare/workers-types@4.20250913.0)(@opentelemetry/api@1.9.0)(better-sqlite3@12.2.0)))(ioredis@5.7.0) valibot: 1.1.0(typescript@5.9.2) transitivePeerDependencies: - '@azure/app-configuration' @@ -7542,13 +7548,13 @@ snapshots: - magicast - typescript - '@nuxt/ui@4.0.0-alpha.1(@babel/parser@7.28.3)(@vercel/blob@1.1.1)(change-case@5.4.4)(db0@0.3.2(better-sqlite3@12.2.0)(drizzle-orm@0.44.5(@cloudflare/workers-types@4.20250913.0)(@opentelemetry/api@1.9.0)(better-sqlite3@12.2.0)))(embla-carousel@8.6.0)(ioredis@5.7.0)(magicast@0.3.5)(react@19.1.1)(typescript@5.9.2)(valibot@1.1.0(typescript@5.9.2))(vite@7.1.5(@types/node@24.5.0)(jiti@2.5.1)(lightningcss@1.30.1)(terser@5.44.0)(yaml@2.8.1))(vue-router@4.5.1(vue@3.5.21(typescript@5.9.2)))(vue@3.5.21(typescript@5.9.2))(zod@3.25.76)': + '@nuxt/ui@4.0.0-alpha.1(@babel/parser@7.28.3)(@vercel/blob@1.1.1)(aws4fetch@1.0.20)(change-case@5.4.4)(db0@0.3.2(better-sqlite3@12.2.0)(drizzle-orm@0.44.5(@cloudflare/workers-types@4.20250913.0)(@opentelemetry/api@1.9.0)(better-sqlite3@12.2.0)))(embla-carousel@8.6.0)(ioredis@5.7.0)(magicast@0.3.5)(react@19.1.1)(typescript@5.9.2)(valibot@1.1.0(typescript@5.9.2))(vite@7.1.5(@types/node@24.5.0)(jiti@2.5.1)(lightningcss@1.30.1)(terser@5.44.0)(yaml@2.8.1))(vue-router@4.5.1(vue@3.5.21(typescript@5.9.2)))(vue@3.5.21(typescript@5.9.2))(zod@3.25.76)': dependencies: '@ai-sdk/vue': 2.0.32(vue@3.5.21(typescript@5.9.2))(zod@3.25.76) '@iconify/vue': 5.0.0(vue@3.5.21(typescript@5.9.2)) '@internationalized/date': 3.9.0 '@internationalized/number': 3.6.5 - '@nuxt/fonts': 0.11.4(@vercel/blob@1.1.1)(db0@0.3.2(better-sqlite3@12.2.0)(drizzle-orm@0.44.5(@cloudflare/workers-types@4.20250913.0)(@opentelemetry/api@1.9.0)(better-sqlite3@12.2.0)))(ioredis@5.7.0)(magicast@0.3.5)(vite@7.1.5(@types/node@24.5.0)(jiti@2.5.1)(lightningcss@1.30.1)(terser@5.44.0)(yaml@2.8.1)) + '@nuxt/fonts': 0.11.4(@vercel/blob@1.1.1)(aws4fetch@1.0.20)(db0@0.3.2(better-sqlite3@12.2.0)(drizzle-orm@0.44.5(@cloudflare/workers-types@4.20250913.0)(@opentelemetry/api@1.9.0)(better-sqlite3@12.2.0)))(ioredis@5.7.0)(magicast@0.3.5)(vite@7.1.5(@types/node@24.5.0)(jiti@2.5.1)(lightningcss@1.30.1)(terser@5.44.0)(yaml@2.8.1)) '@nuxt/icon': 2.0.0(magicast@0.3.5)(vite@7.1.5(@types/node@24.5.0)(jiti@2.5.1)(lightningcss@1.30.1)(terser@5.44.0)(yaml@2.8.1))(vue@3.5.21(typescript@5.9.2)) '@nuxt/kit': 4.1.0(magicast@0.3.5) '@nuxt/schema': 4.1.0 @@ -9072,13 +9078,13 @@ snapshots: '@vueuse/metadata@13.9.0': {} - '@vueuse/nuxt@13.9.0(magicast@0.3.5)(nuxt@4.1.2(@parcel/watcher@2.5.1)(@types/node@24.5.0)(@vercel/blob@1.1.1)(@vue/compiler-sfc@3.5.21)(better-sqlite3@12.2.0)(db0@0.3.2(better-sqlite3@12.2.0)(drizzle-orm@0.44.5(@cloudflare/workers-types@4.20250913.0)(@opentelemetry/api@1.9.0)(better-sqlite3@12.2.0)))(drizzle-orm@0.44.5(@cloudflare/workers-types@4.20250913.0)(@opentelemetry/api@1.9.0)(better-sqlite3@12.2.0))(eslint@9.35.0(jiti@2.5.1))(ioredis@5.7.0)(lightningcss@1.30.1)(magicast@0.3.5)(optionator@0.9.4)(rollup@4.50.0)(terser@5.44.0)(typescript@5.9.2)(vite@7.1.5(@types/node@24.5.0)(jiti@2.5.1)(lightningcss@1.30.1)(terser@5.44.0)(yaml@2.8.1))(vue-tsc@3.0.7(typescript@5.9.2))(yaml@2.8.1))(vue@3.5.21(typescript@5.9.2))': + '@vueuse/nuxt@13.9.0(magicast@0.3.5)(nuxt@4.1.2(@parcel/watcher@2.5.1)(@types/node@24.5.0)(@vercel/blob@1.1.1)(@vue/compiler-sfc@3.5.21)(aws4fetch@1.0.20)(better-sqlite3@12.2.0)(db0@0.3.2(better-sqlite3@12.2.0)(drizzle-orm@0.44.5(@cloudflare/workers-types@4.20250913.0)(@opentelemetry/api@1.9.0)(better-sqlite3@12.2.0)))(drizzle-orm@0.44.5(@cloudflare/workers-types@4.20250913.0)(@opentelemetry/api@1.9.0)(better-sqlite3@12.2.0))(eslint@9.35.0(jiti@2.5.1))(ioredis@5.7.0)(lightningcss@1.30.1)(magicast@0.3.5)(optionator@0.9.4)(rollup@4.50.0)(terser@5.44.0)(typescript@5.9.2)(vite@7.1.5(@types/node@24.5.0)(jiti@2.5.1)(lightningcss@1.30.1)(terser@5.44.0)(yaml@2.8.1))(vue-tsc@3.0.7(typescript@5.9.2))(yaml@2.8.1))(vue@3.5.21(typescript@5.9.2))': dependencies: '@nuxt/kit': 3.19.0(magicast@0.3.5) '@vueuse/core': 13.9.0(vue@3.5.21(typescript@5.9.2)) '@vueuse/metadata': 13.9.0 local-pkg: 1.1.2 - nuxt: 4.1.2(@parcel/watcher@2.5.1)(@types/node@24.5.0)(@vercel/blob@1.1.1)(@vue/compiler-sfc@3.5.21)(better-sqlite3@12.2.0)(db0@0.3.2(better-sqlite3@12.2.0)(drizzle-orm@0.44.5(@cloudflare/workers-types@4.20250913.0)(@opentelemetry/api@1.9.0)(better-sqlite3@12.2.0)))(drizzle-orm@0.44.5(@cloudflare/workers-types@4.20250913.0)(@opentelemetry/api@1.9.0)(better-sqlite3@12.2.0))(eslint@9.35.0(jiti@2.5.1))(ioredis@5.7.0)(lightningcss@1.30.1)(magicast@0.3.5)(optionator@0.9.4)(rollup@4.50.0)(terser@5.44.0)(typescript@5.9.2)(vite@7.1.5(@types/node@24.5.0)(jiti@2.5.1)(lightningcss@1.30.1)(terser@5.44.0)(yaml@2.8.1))(vue-tsc@3.0.7(typescript@5.9.2))(yaml@2.8.1) + nuxt: 4.1.2(@parcel/watcher@2.5.1)(@types/node@24.5.0)(@vercel/blob@1.1.1)(@vue/compiler-sfc@3.5.21)(aws4fetch@1.0.20)(better-sqlite3@12.2.0)(db0@0.3.2(better-sqlite3@12.2.0)(drizzle-orm@0.44.5(@cloudflare/workers-types@4.20250913.0)(@opentelemetry/api@1.9.0)(better-sqlite3@12.2.0)))(drizzle-orm@0.44.5(@cloudflare/workers-types@4.20250913.0)(@opentelemetry/api@1.9.0)(better-sqlite3@12.2.0))(eslint@9.35.0(jiti@2.5.1))(ioredis@5.7.0)(lightningcss@1.30.1)(magicast@0.3.5)(optionator@0.9.4)(rollup@4.50.0)(terser@5.44.0)(typescript@5.9.2)(vite@7.1.5(@types/node@24.5.0)(jiti@2.5.1)(lightningcss@1.30.1)(terser@5.44.0)(yaml@2.8.1))(vue-tsc@3.0.7(typescript@5.9.2))(yaml@2.8.1) vue: 3.5.21(typescript@5.9.2) transitivePeerDependencies: - magicast @@ -9224,6 +9230,8 @@ snapshots: postcss: 8.5.6 postcss-value-parser: 4.2.0 + aws4fetch@1.0.20: {} + b4a@1.6.7: {} bail@2.0.2: {} @@ -10572,7 +10580,7 @@ snapshots: transitivePeerDependencies: - supports-color - ipx@2.1.1(@vercel/blob@1.1.1)(db0@0.3.2(better-sqlite3@12.2.0)(drizzle-orm@0.44.5(@cloudflare/workers-types@4.20250913.0)(@opentelemetry/api@1.9.0)(better-sqlite3@12.2.0)))(ioredis@5.7.0): + ipx@2.1.1(@vercel/blob@1.1.1)(aws4fetch@1.0.20)(db0@0.3.2(better-sqlite3@12.2.0)(drizzle-orm@0.44.5(@cloudflare/workers-types@4.20250913.0)(@opentelemetry/api@1.9.0)(better-sqlite3@12.2.0)))(ioredis@5.7.0): dependencies: '@fastify/accept-negotiator': 1.1.0 citty: 0.1.6 @@ -10588,7 +10596,7 @@ snapshots: sharp: 0.32.6 svgo: 3.3.2 ufo: 1.6.1 - unstorage: 1.17.1(@vercel/blob@1.1.1)(db0@0.3.2(better-sqlite3@12.2.0)(drizzle-orm@0.44.5(@cloudflare/workers-types@4.20250913.0)(@opentelemetry/api@1.9.0)(better-sqlite3@12.2.0)))(ioredis@5.7.0) + unstorage: 1.17.1(@vercel/blob@1.1.1)(aws4fetch@1.0.20)(db0@0.3.2(better-sqlite3@12.2.0)(drizzle-orm@0.44.5(@cloudflare/workers-types@4.20250913.0)(@opentelemetry/api@1.9.0)(better-sqlite3@12.2.0)))(ioredis@5.7.0) xss: 1.0.15 transitivePeerDependencies: - '@azure/app-configuration' @@ -11383,7 +11391,7 @@ snapshots: mlly: 1.8.0 pkg-types: 2.3.0 - nitropack@2.12.5(@vercel/blob@1.1.1)(better-sqlite3@12.2.0)(drizzle-orm@0.44.5(@cloudflare/workers-types@4.20250913.0)(@opentelemetry/api@1.9.0)(better-sqlite3@12.2.0)): + nitropack@2.12.5(@vercel/blob@1.1.1)(aws4fetch@1.0.20)(better-sqlite3@12.2.0)(drizzle-orm@0.44.5(@cloudflare/workers-types@4.20250913.0)(@opentelemetry/api@1.9.0)(better-sqlite3@12.2.0)): dependencies: '@cloudflare/kv-asset-handler': 0.4.0 '@rollup/plugin-alias': 5.1.1(rollup@4.50.0) @@ -11450,7 +11458,7 @@ snapshots: unenv: 2.0.0-rc.20 unimport: 5.2.0 unplugin-utils: 0.3.0 - unstorage: 1.17.1(@vercel/blob@1.1.1)(db0@0.3.2(better-sqlite3@12.2.0)(drizzle-orm@0.44.5(@cloudflare/workers-types@4.20250913.0)(@opentelemetry/api@1.9.0)(better-sqlite3@12.2.0)))(ioredis@5.7.0) + unstorage: 1.17.1(@vercel/blob@1.1.1)(aws4fetch@1.0.20)(db0@0.3.2(better-sqlite3@12.2.0)(drizzle-orm@0.44.5(@cloudflare/workers-types@4.20250913.0)(@opentelemetry/api@1.9.0)(better-sqlite3@12.2.0)))(ioredis@5.7.0) untyped: 2.0.0 unwasm: 0.3.11 youch: 4.1.0-beta.8 @@ -11572,7 +11580,7 @@ snapshots: transitivePeerDependencies: - magicast - nuxt-og-image@5.1.9(@unhead/vue@2.0.14(vue@3.5.21(typescript@5.9.2)))(magicast@0.3.5)(unstorage@1.17.1(@vercel/blob@1.1.1)(db0@0.3.2(better-sqlite3@12.2.0)(drizzle-orm@0.44.5(@cloudflare/workers-types@4.20250913.0)(@opentelemetry/api@1.9.0)(better-sqlite3@12.2.0)))(ioredis@5.7.0))(vite@7.1.5(@types/node@24.5.0)(jiti@2.5.1)(lightningcss@1.30.1)(terser@5.44.0)(yaml@2.8.1))(vue@3.5.21(typescript@5.9.2)): + nuxt-og-image@5.1.9(@unhead/vue@2.0.14(vue@3.5.21(typescript@5.9.2)))(magicast@0.3.5)(unstorage@1.17.1(@vercel/blob@1.1.1)(aws4fetch@1.0.20)(db0@0.3.2(better-sqlite3@12.2.0)(drizzle-orm@0.44.5(@cloudflare/workers-types@4.20250913.0)(@opentelemetry/api@1.9.0)(better-sqlite3@12.2.0)))(ioredis@5.7.0))(vite@7.1.5(@types/node@24.5.0)(jiti@2.5.1)(lightningcss@1.30.1)(terser@5.44.0)(yaml@2.8.1))(vue@3.5.21(typescript@5.9.2)): dependencies: '@nuxt/devtools-kit': 2.6.3(magicast@0.3.5)(vite@7.1.5(@types/node@24.5.0)(jiti@2.5.1)(lightningcss@1.30.1)(terser@5.44.0)(yaml@2.8.1)) '@nuxt/kit': 3.19.0(magicast@0.3.5) @@ -11603,7 +11611,7 @@ snapshots: strip-literal: 3.0.0 ufo: 1.6.1 unplugin: 2.3.10 - unstorage: 1.17.1(@vercel/blob@1.1.1)(db0@0.3.2(better-sqlite3@12.2.0)(drizzle-orm@0.44.5(@cloudflare/workers-types@4.20250913.0)(@opentelemetry/api@1.9.0)(better-sqlite3@12.2.0)))(ioredis@5.7.0) + unstorage: 1.17.1(@vercel/blob@1.1.1)(aws4fetch@1.0.20)(db0@0.3.2(better-sqlite3@12.2.0)(drizzle-orm@0.44.5(@cloudflare/workers-types@4.20250913.0)(@opentelemetry/api@1.9.0)(better-sqlite3@12.2.0)))(ioredis@5.7.0) unwasm: 0.3.11 yoga-wasm-web: 0.3.3 transitivePeerDependencies: @@ -11636,7 +11644,7 @@ snapshots: - magicast - vue - nuxt@4.1.2(@parcel/watcher@2.5.1)(@types/node@24.5.0)(@vercel/blob@1.1.1)(@vue/compiler-sfc@3.5.21)(better-sqlite3@12.2.0)(db0@0.3.2(better-sqlite3@12.2.0)(drizzle-orm@0.44.5(@cloudflare/workers-types@4.20250913.0)(@opentelemetry/api@1.9.0)(better-sqlite3@12.2.0)))(drizzle-orm@0.44.5(@cloudflare/workers-types@4.20250913.0)(@opentelemetry/api@1.9.0)(better-sqlite3@12.2.0))(eslint@9.35.0(jiti@2.5.1))(ioredis@5.7.0)(lightningcss@1.30.1)(magicast@0.3.5)(optionator@0.9.4)(rollup@4.50.0)(terser@5.44.0)(typescript@5.9.2)(vite@7.1.5(@types/node@24.5.0)(jiti@2.5.1)(lightningcss@1.30.1)(terser@5.44.0)(yaml@2.8.1))(vue-tsc@3.0.7(typescript@5.9.2))(yaml@2.8.1): + nuxt@4.1.2(@parcel/watcher@2.5.1)(@types/node@24.5.0)(@vercel/blob@1.1.1)(@vue/compiler-sfc@3.5.21)(aws4fetch@1.0.20)(better-sqlite3@12.2.0)(db0@0.3.2(better-sqlite3@12.2.0)(drizzle-orm@0.44.5(@cloudflare/workers-types@4.20250913.0)(@opentelemetry/api@1.9.0)(better-sqlite3@12.2.0)))(drizzle-orm@0.44.5(@cloudflare/workers-types@4.20250913.0)(@opentelemetry/api@1.9.0)(better-sqlite3@12.2.0))(eslint@9.35.0(jiti@2.5.1))(ioredis@5.7.0)(lightningcss@1.30.1)(magicast@0.3.5)(optionator@0.9.4)(rollup@4.50.0)(terser@5.44.0)(typescript@5.9.2)(vite@7.1.5(@types/node@24.5.0)(jiti@2.5.1)(lightningcss@1.30.1)(terser@5.44.0)(yaml@2.8.1))(vue-tsc@3.0.7(typescript@5.9.2))(yaml@2.8.1): dependencies: '@nuxt/cli': 3.28.0(magicast@0.3.5) '@nuxt/devalue': 2.0.2 @@ -11671,7 +11679,7 @@ snapshots: mlly: 1.8.0 mocked-exports: 0.1.1 nanotar: 0.2.0 - nitropack: 2.12.5(@vercel/blob@1.1.1)(better-sqlite3@12.2.0)(drizzle-orm@0.44.5(@cloudflare/workers-types@4.20250913.0)(@opentelemetry/api@1.9.0)(better-sqlite3@12.2.0)) + nitropack: 2.12.5(@vercel/blob@1.1.1)(aws4fetch@1.0.20)(better-sqlite3@12.2.0)(drizzle-orm@0.44.5(@cloudflare/workers-types@4.20250913.0)(@opentelemetry/api@1.9.0)(better-sqlite3@12.2.0)) nypm: 0.6.2 ofetch: 1.4.1 ohash: 2.0.11 @@ -11695,7 +11703,7 @@ snapshots: unimport: 5.2.0 unplugin: 2.3.10 unplugin-vue-router: 0.15.0(@vue/compiler-sfc@3.5.21)(typescript@5.9.2)(vue-router@4.5.1(vue@3.5.21(typescript@5.9.2)))(vue@3.5.21(typescript@5.9.2)) - unstorage: 1.17.1(@vercel/blob@1.1.1)(db0@0.3.2(better-sqlite3@12.2.0)(drizzle-orm@0.44.5(@cloudflare/workers-types@4.20250913.0)(@opentelemetry/api@1.9.0)(better-sqlite3@12.2.0)))(ioredis@5.7.0) + unstorage: 1.17.1(@vercel/blob@1.1.1)(aws4fetch@1.0.20)(db0@0.3.2(better-sqlite3@12.2.0)(drizzle-orm@0.44.5(@cloudflare/workers-types@4.20250913.0)(@opentelemetry/api@1.9.0)(better-sqlite3@12.2.0)))(ioredis@5.7.0) untyped: 2.0.0 vue: 3.5.21(typescript@5.9.2) vue-bundle-renderer: 2.1.2 @@ -13220,7 +13228,7 @@ snapshots: '@unrs/resolver-binding-win32-ia32-msvc': 1.11.1 '@unrs/resolver-binding-win32-x64-msvc': 1.11.1 - unstorage@1.17.1(@vercel/blob@1.1.1)(db0@0.3.2(better-sqlite3@12.2.0)(drizzle-orm@0.44.5(@cloudflare/workers-types@4.20250913.0)(@opentelemetry/api@1.9.0)(better-sqlite3@12.2.0)))(ioredis@5.7.0): + unstorage@1.17.1(@vercel/blob@1.1.1)(aws4fetch@1.0.20)(db0@0.3.2(better-sqlite3@12.2.0)(drizzle-orm@0.44.5(@cloudflare/workers-types@4.20250913.0)(@opentelemetry/api@1.9.0)(better-sqlite3@12.2.0)))(ioredis@5.7.0): dependencies: anymatch: 3.1.3 chokidar: 4.0.3 @@ -13232,6 +13240,7 @@ snapshots: ufo: 1.6.1 optionalDependencies: '@vercel/blob': 1.1.1 + aws4fetch: 1.0.20 db0: 0.3.2(better-sqlite3@12.2.0)(drizzle-orm@0.44.5(@cloudflare/workers-types@4.20250913.0)(@opentelemetry/api@1.9.0)(better-sqlite3@12.2.0)) ioredis: 5.7.0 diff --git a/src/runtime/blob/server/helpers/blob-fs.ts b/src/runtime/blob/server/helpers/blob-fs.ts index 7c1c7e73..0a041438 100644 --- a/src/runtime/blob/server/helpers/blob-fs.ts +++ b/src/runtime/blob/server/helpers/blob-fs.ts @@ -1,7 +1,7 @@ import type { BlobMultipartUpload, BlobObject, BlobUploadedPart } from '~/src/types/blob' import { randomUUID } from 'uncrypto' import { join } from 'pathe' -import fs, { read } from 'node:fs' +import fs from 'node:fs' import fsp from 'node:fs/promises' import type { Driver } from 'unstorage' @@ -115,17 +115,17 @@ async function completeUpload(driver: Driver, pathname: string, uploadId: string const fullPath = join(root, pathname) const orderedUploadedParts = uploadedParts.sort((a, b) => a.partNumber - b.partNumber) - return new Promise(async (resolve, reject) => { - const writeStream = fs.createWriteStream(fullPath) - for (const file of orderedUploadedParts) { - await new Promise((resolve, reject) => { - const readStream = fs.createReadStream(join(root, partKey(uploadId, file.partNumber))) - readStream.pipe(writeStream, { end: false }) - readStream.on('error', reject) - readStream.on('end', resolve as () => void) - }) - } + const writeStream = fs.createWriteStream(fullPath) + for (const file of orderedUploadedParts) { + await new Promise((resolve, reject) => { + const readStream = fs.createReadStream(join(root, partKey(uploadId, file.partNumber))) + readStream.pipe(writeStream, { end: false }) + readStream.on('error', reject) + readStream.on('end', resolve as () => void) + }) + } + return new Promise((resolve, reject) => { writeStream.end() writeStream.on('finish', () => { resolve(fs.statSync(fullPath).size) diff --git a/src/types/blob.ts b/src/types/blob.ts index a8387589..162f5e08 100644 --- a/src/types/blob.ts +++ b/src/types/blob.ts @@ -19,7 +19,7 @@ export interface BlobObject { contentType: string | undefined /** * The size of the blob in bytes - * + * * Some drivers do not return the size of the blob at the time of upload. This is supported in the `fs` and `cloudflare-r2` drivers. * In the Vercel Blob and S3 drivers, the size is missing. */