diff --git a/storage/blob-starter/app/api/blob/route.ts b/storage/blob-starter/app/api/blob/route.ts new file mode 100644 index 0000000000..2ff6437577 --- /dev/null +++ b/storage/blob-starter/app/api/blob/route.ts @@ -0,0 +1,30 @@ +import { type NextRequest, NextResponse } from 'next/server' +import { get } from '@vercel/blob' + +// Delivery route for private blobs. +// Private blob URLs are not directly accessible in the browser. +// This route streams the blob content after authenticating the request. +// See: https://vercel.com/docs/vercel-blob/private-storage +export async function GET(request: NextRequest) { + // ⚠️ Add your own authentication here. + // For example: await authRequest(request) + + const pathname = request.nextUrl.searchParams.get('pathname') + + if (!pathname) { + return NextResponse.json({ error: 'Missing pathname' }, { status: 400 }) + } + + const result = await get(pathname, { access: 'private' }) + + if (result?.statusCode !== 200) { + return new NextResponse('Not found', { status: 404 }) + } + + return new NextResponse(result.stream, { + headers: { + 'Content-Type': result.blob.contentType, + 'X-Content-Type-Options': 'nosniff', + }, + }) +} diff --git a/storage/blob-starter/components/uploader.tsx b/storage/blob-starter/components/uploader.tsx index f79e271f03..a0dd07bd1c 100644 --- a/storage/blob-starter/components/uploader.tsx +++ b/storage/blob-starter/components/uploader.tsx @@ -28,27 +28,32 @@ export default function Uploader() { if (file) { try { const blob = await upload(file.name, file, { - access: 'public', + access: 'private', handleUploadUrl: '/api/upload', onUploadProgress: (progressEvent) => { setProgress(progressEvent.percentage) }, }) + // Private blob URLs are not directly accessible in the browser. + // Serve them through a delivery route that streams the content. + // See: https://vercel.com/docs/vercel-blob/private-storage + const deliveryUrl = `/api/blob?pathname=${encodeURIComponent(blob.pathname)}` + toast( (t: { id: string }) => (

File uploaded!

- Your file has been uploaded to{' '} + Your file has been uploaded.{' '} - {blob.url} + View file

diff --git a/storage/blob-starter/package.json b/storage/blob-starter/package.json index 3f54a9c9e8..6eaab905e9 100644 --- a/storage/blob-starter/package.json +++ b/storage/blob-starter/package.json @@ -15,7 +15,7 @@ "lint": "next lint" }, "dependencies": { - "@vercel/blob": "^1.0.0", + "@vercel/blob": "^2.3.0", "next": "^16.0.10", "react": "^19.2.1", "react-dom": "^19.2.1", diff --git a/storage/blob-sveltekit/package.json b/storage/blob-sveltekit/package.json index a28c2bc154..4c916ea968 100644 --- a/storage/blob-sveltekit/package.json +++ b/storage/blob-sveltekit/package.json @@ -26,7 +26,7 @@ "vite": "^5.4.4" }, "dependencies": { - "@vercel/blob": "^1.0.0", + "@vercel/blob": "^2.3.0", "dotenv-expand": "^10.0.0" } } diff --git a/storage/blob-sveltekit/src/routes/+page.server.ts b/storage/blob-sveltekit/src/routes/+page.server.ts index e1ceb69a3d..a40d2f4b6f 100644 --- a/storage/blob-sveltekit/src/routes/+page.server.ts +++ b/storage/blob-sveltekit/src/routes/+page.server.ts @@ -11,11 +11,13 @@ export const actions = { error(400, { message: 'No file to upload.' }) } - const { url } = await put(file.name, file, { - access: 'public', + const { pathname } = await put(file.name, file, { + access: 'private', addRandomSuffix: true, token: env.BLOB_READ_WRITE_TOKEN, }) - return { uploaded: url } + // Private blob URLs are not directly accessible in the browser. + // Return a delivery route URL that streams the content. + return { uploaded: `/api/blob?pathname=${encodeURIComponent(pathname)}` } }, } diff --git a/storage/blob-sveltekit/src/routes/api/blob/+server.ts b/storage/blob-sveltekit/src/routes/api/blob/+server.ts new file mode 100644 index 0000000000..766e2b3ff2 --- /dev/null +++ b/storage/blob-sveltekit/src/routes/api/blob/+server.ts @@ -0,0 +1,33 @@ +import { error } from '@sveltejs/kit' +import { get } from '@vercel/blob' +import { env } from '$env/dynamic/private' + +// Delivery route for private blobs. +// Private blob URLs are not directly accessible in the browser. +// This route streams the blob content after authenticating the request. +// See: https://vercel.com/docs/vercel-blob/private-storage +export async function GET({ url }) { + // ⚠️ Add your own authentication here. + + const pathname = url.searchParams.get('pathname') + + if (!pathname) { + error(400, { message: 'Missing pathname' }) + } + + const result = await get(pathname, { + access: 'private', + token: env.BLOB_READ_WRITE_TOKEN, + }) + + if (result?.statusCode !== 200) { + error(404, { message: 'Not found' }) + } + + return new Response(result.stream, { + headers: { + 'Content-Type': result.blob.contentType, + 'X-Content-Type-Options': 'nosniff', + }, + }) +}