Files
zeekay a15864b16b ci: amd64-only build (matches hanzo runner pool topology)
hanzo-build-linux-amd64 is the only ARC runner pool installed on
hanzo-k8s today (arm64 pool is paused until DigitalOcean ships
arm64 droplets, per ~/work/hanzo/.github/RUNNERS.md). The arm64
job was waiting on spark-hanzoai-arm64 — not provisioned in this
cluster — so every build sat in queue forever and the amd64 leg
got cancelled.

Set platforms: linux/amd64 on the reusable build call to skip
arm64 entirely. Re-add arm64 if/when DO ships arm64 droplets and
the spark runner pool comes back online.
2026-06-07 11:38:49 -07:00

352 lines
9.9 KiB
TypeScript

import { NextRequest, NextResponse } from "next/server";
import { processDocument } from "@/lib/api/documents/process-document";
import { verifyDataroomSession } from "@/lib/auth/dataroom-auth";
import { DocumentData } from "@/lib/documents/create-document";
import prisma from "@/lib/prisma";
import { sendDataroomUploadNotificationTask } from "@/lib/trigger/dataroom-upload-notification";
import { sanitizePlainText } from "@/lib/utils/sanitize-html";
import { supportsAdvancedExcelMode } from "@/lib/utils/get-content-type";
import { runs } from "@trigger.dev/sdk/v3";
import { waitUntil } from "@vercel/functions";
/**
* GET /api/links/[id]/upload?dataroomId=xxx
* Returns the viewer's previously uploaded documents for this dataroom.
*/
export async function GET(
request: NextRequest,
{ params }: { params: { id: string } },
) {
try {
const linkId = params.id;
const dataroomId = request.nextUrl.searchParams.get("dataroomId");
if (!linkId || !dataroomId) {
return NextResponse.json(
{ message: "Missing required parameters" },
{ status: 400 },
);
}
// Verify the dataroom session
const dataroomSession = await verifyDataroomSession(
request,
linkId,
dataroomId,
);
if (!dataroomSession || !dataroomSession.viewerId) {
return NextResponse.json(
{ message: "Unauthorized" },
{ status: 401 },
);
}
const { viewerId } = dataroomSession;
// Fetch the viewer's uploads for this dataroom
const uploads = await prisma.documentUpload.findMany({
where: {
viewerId,
dataroomId,
linkId,
},
select: {
id: true,
documentId: true,
dataroomDocumentId: true,
originalFilename: true,
uploadedAt: true,
document: {
select: {
id: true,
name: true,
type: true,
versions: {
where: { isPrimary: true },
select: {
id: true,
hasPages: true,
},
take: 1,
},
},
},
dataroomDocument: {
select: {
folderId: true,
},
},
},
orderBy: { uploadedAt: "desc" },
});
const formattedUploads = uploads.map((upload) => {
const fileType = upload.document?.type ?? "";
const hasPages = upload.document?.versions?.[0]?.hasPages ?? false;
const needsProcessing = ["pdf", "docs", "slides"].includes(fileType);
const isComplete = !needsProcessing || hasPages;
return {
id: upload.id,
documentId: upload.documentId,
dataroomDocumentId: upload.dataroomDocumentId,
documentVersionId: upload.document?.versions?.[0]?.id ?? null,
name: upload.originalFilename ?? upload.document?.name ?? "Unknown",
fileType,
folderId: upload.dataroomDocument?.folderId ?? null,
uploadedAt: upload.uploadedAt,
status: isComplete ? "complete" : "processing",
};
});
return NextResponse.json({ uploads: formattedUploads });
} catch (error) {
console.error("Error fetching viewer uploads:", error);
return NextResponse.json(
{ message: "Error fetching uploads" },
{ status: 500 },
);
}
}
export async function POST(
request: NextRequest,
{ params }: { params: { id: string } },
) {
try {
const linkId = params.id;
const body = await request.json();
const { documentData, dataroomId, folderId } = body as {
documentData: DocumentData;
dataroomId: string;
folderId?: string;
};
if (!linkId || !documentData || !dataroomId) {
return NextResponse.json(
{ message: "Missing required parameters" },
{ status: 400 },
);
}
// 0. Verify the dataroom session
const dataroomSession = await verifyDataroomSession(
request,
linkId,
dataroomId,
);
if (!dataroomSession || !dataroomSession.viewerId) {
return NextResponse.json(
{ message: "You need to be logged in to upload a document." },
{ status: 401 },
);
}
// Check if the link exists and has visitor upload enabled
const link = await prisma.link.findUnique({
where: { id: linkId, dataroomId },
select: {
id: true,
name: true,
enableUpload: true,
enableNotification: true,
uploadFolderId: true,
dataroomId: true,
teamId: true,
team: {
select: {
plan: true,
enableExcelAdvancedMode: true,
},
},
},
});
if (
!link ||
!link.enableUpload ||
link.dataroomId !== dataroomId ||
!link.teamId
) {
return NextResponse.json(
{ message: "Uploads not allowed for this link" },
{ status: 403 },
);
}
const { viewerId, viewId } = dataroomSession;
// Check if the viewer exists
const viewer = await prisma.viewer.findUnique({
where: {
id: viewerId,
teamId: link.teamId,
views: { some: { id: viewId } },
},
select: { id: true },
});
if (!viewer) {
return NextResponse.json(
{ message: "Viewer not found" },
{ status: 404 },
);
}
if (typeof documentData.name !== "string") {
return NextResponse.json(
{ message: "Document name is required" },
{ status: 400 },
);
}
const sanitizedDocumentName = sanitizePlainText(documentData.name);
if (!sanitizedDocumentName) {
return NextResponse.json(
{ message: "Document name is required" },
{ status: 400 },
);
}
if (sanitizedDocumentName.length > 255) {
return NextResponse.json(
{ message: "Document name too long" },
{ status: 400 },
);
}
const updatedDocumentData = {
...documentData,
name: sanitizedDocumentName,
enableExcelAdvancedMode: documentData.supportedFileType === "sheet" &&
link.team?.enableExcelAdvancedMode &&
supportsAdvancedExcelMode(documentData.contentType),
};
// 1. Create the document
const document = await processDocument({
documentData: updatedDocumentData,
teamId: link.teamId,
teamPlan: link.team?.plan ?? "free",
isExternalUpload: true,
});
// 2. Create the dataroom document
// If folderId is provided and link has no uploadFolderId, use folderId as the dataroomFolderId
// Otherwise, use the link's uploadFolderId
// or null if it doesn't exist
let dataroomFolderId: string | null = folderId ?? null;
if (link.uploadFolderId) {
const dataroomFolder = await prisma.dataroomFolder.findUnique({
where: {
id: link.uploadFolderId,
dataroomId,
},
select: {
id: true,
},
});
dataroomFolderId = dataroomFolder?.id ?? null;
}
const newDataroomDocument = await prisma.dataroomDocument.create({
data: {
dataroomId: dataroomId,
documentId: document.id,
folderId: dataroomFolderId,
},
});
// 3. Create the DocumentUpload record to track the upload details
await prisma.documentUpload.create({
data: {
documentId: document.id,
viewerId: viewerId,
viewId: viewId,
linkId: linkId,
originalFilename: document.name,
fileSize: documentData.fileSize ?? 0,
numPages: document.numPages,
mimeType: document.contentType,
dataroomId: dataroomId,
dataroomDocumentId: newDataroomDocument.id,
teamId: link.teamId,
},
});
// 4. Send upload notification to team if enabled
if (link.enableNotification) {
try {
// Cancel any existing pending notification runs for this viewer+dataroom+link
// Note: runs.list tag filter uses OR logic, so we must post-filter
// to ensure we only cancel runs matching ALL three tags
const requiredTags = [
`dataroom_${dataroomId}`,
`link_${linkId}`,
`viewer_${viewerId}`,
];
const allRuns = await runs.list({
taskIdentifier: ["send-dataroom-upload-notification"],
tag: requiredTags,
status: ["DELAYED", "QUEUED"],
period: "10m",
});
const matchingRuns = allRuns.data.filter((run) =>
requiredTags.every((tag) => run.tags?.includes(tag)),
);
await Promise.all(matchingRuns.map((run) => runs.cancel(run.id)));
// Trigger a new notification with 5-minute delay to batch uploads
waitUntil(
sendDataroomUploadNotificationTask.trigger(
{
dataroomId,
linkId,
viewerId,
teamId: link.teamId,
},
{
idempotencyKey: `upload-notification-${link.teamId}-${dataroomId}-${linkId}-${viewerId}-${newDataroomDocument.id}`,
tags: [
`team_${link.teamId}`,
`dataroom_${dataroomId}`,
`link_${linkId}`,
`viewer_${viewerId}`,
],
delay: new Date(Date.now() + 5 * 60 * 1000), // 5 minute delay
},
),
);
} catch (error) {
console.error("Error triggering upload notification:", error);
}
}
// Return document data for optimistic UI rendering
return NextResponse.json({
success: true,
document: {
id: document.id,
name: document.name,
dataroomDocumentId: newDataroomDocument.id,
documentVersionId: document.versions[0]?.id,
folderId: dataroomFolderId,
fileType: document.type,
hasPages: (document.numPages ?? 0) > 0,
createdAt: document.createdAt,
},
});
} catch (error) {
console.error("Error uploading document:", error);
return NextResponse.json(
{ message: "Error uploading document" },
{ status: 500 },
);
}
}