Spaces:
Sleeping
Sleeping
File size: 4,953 Bytes
05c5ed5 | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 | 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 },
);
}
}
|