Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
30 changes: 30 additions & 0 deletions storage/blob-starter/app/api/blob/route.ts
Original file line number Diff line number Diff line change
@@ -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',
},
})
}
13 changes: 9 additions & 4 deletions storage/blob-starter/components/uploader.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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 }) => (
<div className="relative">
<div className="p-2">
<p className="font-semibold text-gray-900">File uploaded!</p>
<p className="mt-1 text-sm text-gray-500">
Your file has been uploaded to{' '}
Your file has been uploaded.{' '}
<a
className="font-medium text-gray-900 underline"
href={blob.url}
href={deliveryUrl}
target="_blank"
rel="noopener noreferrer"
>
{blob.url}
View file
</a>
</p>
</div>
Expand Down
2 changes: 1 addition & 1 deletion storage/blob-starter/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down
2 changes: 1 addition & 1 deletion storage/blob-sveltekit/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@
"vite": "^5.4.4"
},
"dependencies": {
"@vercel/blob": "^1.0.0",
"@vercel/blob": "^2.3.0",
"dotenv-expand": "^10.0.0"
}
}
8 changes: 5 additions & 3 deletions storage/blob-sveltekit/src/routes/+page.server.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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)}` }
},
}
33 changes: 33 additions & 0 deletions storage/blob-sveltekit/src/routes/api/blob/+server.ts
Original file line number Diff line number Diff line change
@@ -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',
},
})
}