Spaces:
Sleeping
Sleeping
| import { handleUpload, type HandleUploadBody } from "@vercel/blob/client"; | |
| import { NextResponse } from "next/server"; | |
| import { getSession } from "auth/server"; | |
| import { serverFileStorage, storageDriver } from "lib/file-storage"; | |
| import globalLogger from "lib/logger"; | |
| import { colorize } from "consola/utils"; | |
| import { checkStorageAction } from "../actions"; | |
| const logger = globalLogger.withDefaults({ | |
| message: colorize("blackBright", `[${storageDriver} Upload URL API]`), | |
| }); | |
| // Constants | |
| const DEFAULT_UPLOAD_EXPIRES_SECONDS = 3600; // 1 hour | |
| const FALLBACK_UPLOAD_URL = "/api/storage/upload"; | |
| // Types | |
| interface GenericUploadRequest { | |
| filename?: string; | |
| contentType?: string; | |
| } | |
| interface FallbackResponse { | |
| directUploadSupported: false; | |
| fallbackUrl: string; | |
| message: string; | |
| } | |
| // Helpers | |
| function createFallbackResponse(): FallbackResponse { | |
| return { | |
| directUploadSupported: false, | |
| fallbackUrl: FALLBACK_UPLOAD_URL, | |
| message: "Use multipart/form-data upload to fallbackUrl", | |
| }; | |
| } | |
| function isVercelBlobRequest(body: unknown): body is HandleUploadBody { | |
| return ( | |
| typeof body === "object" && | |
| body !== null && | |
| (body as HandleUploadBody).type === "blob.generate-client-token" | |
| ); | |
| } | |
| /** | |
| * Handles Vercel Blob client upload flow. | |
| * Generates client token and handles upload completion webhook. | |
| */ | |
| async function handleVercelBlobUpload( | |
| body: HandleUploadBody, | |
| request: Request, | |
| userId: string, | |
| ) { | |
| const jsonResponse = await handleUpload({ | |
| body, | |
| request, | |
| onBeforeGenerateToken: async () => { | |
| return { | |
| allowedContentTypes: undefined, // Allow all file types | |
| addRandomSuffix: true, // Prevent filename collisions | |
| tokenPayload: JSON.stringify({ | |
| userId, | |
| uploadedAt: new Date().toISOString(), | |
| }), | |
| }; | |
| }, | |
| onUploadCompleted: async ({ blob, tokenPayload }) => { | |
| logger.info("Upload completed", { | |
| url: blob.url, | |
| pathname: blob.pathname, | |
| tokenPayload, | |
| }); | |
| try { | |
| // TODO: Add custom logic here (save to database, send notification, etc.) | |
| // const { userId } = JSON.parse(tokenPayload); | |
| // await db.files.create({ url: blob.url, userId }); | |
| } catch (error) { | |
| logger.error("Error in onUploadCompleted callback", error); | |
| } | |
| }, | |
| }); | |
| return NextResponse.json(jsonResponse); | |
| } | |
| /** | |
| * Handles generic upload URL request (S3, Local FS, etc.). | |
| * Returns presigned URL if supported, otherwise returns fallback response. | |
| */ | |
| async function handleGenericUpload(request: GenericUploadRequest) { | |
| // Check if storage backend supports direct upload | |
| if (typeof serverFileStorage.createUploadUrl !== "function") { | |
| logger.info("Storage doesn't support createUploadUrl, using fallback"); | |
| return NextResponse.json(createFallbackResponse()); | |
| } | |
| const uploadUrl = await serverFileStorage.createUploadUrl({ | |
| filename: request.filename || "file", | |
| contentType: request.contentType || "application/octet-stream", | |
| expiresInSeconds: DEFAULT_UPLOAD_EXPIRES_SECONDS, | |
| }); | |
| if (!uploadUrl) { | |
| logger.info("Storage returned null, using fallback"); | |
| return NextResponse.json(createFallbackResponse()); | |
| } | |
| // Provide a public source URL for clients to reference after successful PUT | |
| const sourceUrl = await serverFileStorage.getSourceUrl(uploadUrl.key); | |
| return NextResponse.json({ | |
| directUploadSupported: true, | |
| ...uploadUrl, | |
| sourceUrl, | |
| }); | |
| } | |
| /** | |
| * Upload URL endpoint. | |
| * | |
| * Provides optimal upload method based on storage backend: | |
| * - Vercel Blob: Client token for direct upload | |
| * - S3: Presigned URL (future) | |
| * - Local FS: Fallback to server upload | |
| */ | |
| export async function POST(request: Request) { | |
| // Authenticate | |
| const session = await getSession(); | |
| if (!session?.user?.id) { | |
| return NextResponse.json({ error: "Unauthorized" }, { status: 401 }); | |
| } | |
| // Check storage configuration first | |
| const storageCheck = await checkStorageAction(); | |
| if (!storageCheck.isValid) { | |
| logger.error("Storage configuration error", { | |
| error: storageCheck.error, | |
| solution: storageCheck.solution, | |
| }); | |
| return NextResponse.json( | |
| { | |
| error: storageCheck.error, | |
| solution: storageCheck.solution, | |
| storageDriver, | |
| }, | |
| { status: 500 }, | |
| ); | |
| } | |
| // Parse request body | |
| let body: unknown; | |
| try { | |
| body = await request.json(); | |
| } catch { | |
| return NextResponse.json({ error: "Invalid JSON body" }, { status: 400 }); | |
| } | |
| try { | |
| // Route to appropriate handler | |
| if (isVercelBlobRequest(body)) { | |
| return await handleVercelBlobUpload(body, request, session.user.id); | |
| } | |
| return await handleGenericUpload(body as GenericUploadRequest); | |
| } catch (error) { | |
| logger.error("Upload URL generation failed", error); | |
| return NextResponse.json( | |
| { error: "Failed to create upload URL" }, | |
| { status: 500 }, | |
| ); | |
| } | |
| } | |