-
Notifications
You must be signed in to change notification settings - Fork 38
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
* refactor: use `withAuthenticatedUser` helper in file upload route * feat: client side file uploads Upload files to the Vercel S3 buckets directly from the client (browser) without the server acting as an intermediary. This also solves the 4.5 MB file size limit that the server uploads have. * fix: correct HTTP status code in case of insufficient user rights
- Loading branch information
Showing
3 changed files
with
74 additions
and
65 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,57 +1,41 @@ | ||
import crypto from "crypto"; | ||
|
||
import { NextResponse } from "next/server"; | ||
|
||
import { put } from "@vercel/blob"; | ||
import { getServerSession } from "next-auth"; | ||
import { handleUpload, type HandleUploadBody } from "@vercel/blob/client"; | ||
|
||
import { authOptions, type OurUser } from "~/src/auth"; | ||
import { withAuthenticatedUser } from "~/src/auth"; | ||
import { getUserProfile } from "~/src/data/user-profile"; | ||
|
||
export async function POST(request: Request): Promise<Response> { | ||
// Only allow signed-in users to upload assets | ||
const session = await getServerSession(authOptions); | ||
if (!session?.user) { | ||
return new Response("Unauthorized", { status: 401 }); | ||
} | ||
|
||
// And only users with the `assetUpload` feature flag may upload | ||
const user = await getUserProfile((session.user as OurUser).id); | ||
if (!user?.featureFlags.includes("assetUpload")) { | ||
return new Response("Unauthorized", { status: 401 }); | ||
} | ||
|
||
if (!request.body) { | ||
return new Response("Missing payload", { status: 400 }); | ||
} | ||
|
||
// We use the original filename to get the extension | ||
const { searchParams } = new URL(request.url); | ||
const originalFilename = searchParams.get("filename"); | ||
if (!originalFilename) { | ||
return new Response("Missing filename", { status: 400 }); | ||
} | ||
|
||
// But the file name itself is an 8-character prefix of the SHA1 | ||
// of file content | ||
const data = await getArrayBufferView(request.body); | ||
const hash = shasumPrefix(new Uint8Array(data)); | ||
const target = hash + "." + getFilenameExtension(originalFilename); | ||
|
||
// Upload | ||
const blob = await put(target, data, { | ||
addRandomSuffix: false, | ||
access: "public", | ||
return withAuthenticatedUser(async ({ id }) => { | ||
// Only users with the `assetUpload` feature flag may upload | ||
const user = await getUserProfile(id); | ||
if (!user?.featureFlags.includes("assetUpload")) { | ||
return NextResponse.json( | ||
{ | ||
error: `Feature flag 'assetUpload' not set for user '${user?.name}'.`, | ||
}, | ||
{ status: 403 }, | ||
); | ||
} | ||
|
||
const body = (await request.json()) as HandleUploadBody; | ||
|
||
try { | ||
// The `handleUpload` function will generate a token for uploading the file from the client (browser). | ||
const jsonResponse = await handleUpload({ | ||
body, | ||
request, | ||
onBeforeGenerateToken: async () => ({ addRandomSuffix: false }), | ||
onUploadCompleted: async () => {}, | ||
}); | ||
return NextResponse.json(jsonResponse); | ||
} catch (error) { | ||
console.error("Error uploading blob", error); | ||
return NextResponse.json( | ||
{ error: (error as Error).message }, | ||
// The webhook will retry 5 times waiting for a 200 status code. | ||
{ status: 400 }, | ||
); | ||
} | ||
}); | ||
|
||
return NextResponse.json(blob); | ||
} | ||
|
||
const getArrayBufferView = async (data: ReadableStream<Uint8Array>) => | ||
new Response(data).arrayBuffer(); | ||
|
||
const shasumPrefix = (data: crypto.BinaryLike) => | ||
crypto.createHash("sha1").update(data).digest("hex").slice(0, 8); | ||
|
||
const getFilenameExtension = (name: string) => | ||
name.split(".").pop()?.toLowerCase(); |