Bot
Initial commit for HF Spaces
05c5ed5
Raw
History Blame Contribute Delete
4.95 kB
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 },
);
}
}