feat: add server file attachment pipeline
This commit is contained in:
45
src/app/api/v1/attachments/[attachmentId]/download/route.ts
Normal file
45
src/app/api/v1/attachments/[attachmentId]/download/route.ts
Normal file
@@ -0,0 +1,45 @@
|
||||
import { createReadStream } from "node:fs";
|
||||
import { stat } from "node:fs/promises";
|
||||
import { Readable } from "node:stream";
|
||||
import { NextRequest, NextResponse } from "next/server";
|
||||
import { requireRequestSession } from "@/lib/boss-auth";
|
||||
import { getAttachmentById } from "@/lib/boss-data";
|
||||
import { buildAttachmentDownloadHeaders } from "@/lib/boss-attachments";
|
||||
import { resolveServerFileAttachmentAbsolutePath } from "@/lib/boss-storage-server-file";
|
||||
|
||||
export const runtime = "nodejs";
|
||||
|
||||
export async function GET(
|
||||
request: NextRequest,
|
||||
context: { params: Promise<{ attachmentId: string }> },
|
||||
) {
|
||||
const session = await requireRequestSession(request);
|
||||
if (!session) {
|
||||
return NextResponse.json({ ok: false, message: "UNAUTHORIZED" }, { status: 401 });
|
||||
}
|
||||
|
||||
const { attachmentId } = await context.params;
|
||||
const record = await getAttachmentById(attachmentId);
|
||||
if (!record) {
|
||||
return NextResponse.json({ ok: false, message: "ATTACHMENT_NOT_FOUND" }, { status: 404 });
|
||||
}
|
||||
|
||||
if (record.attachment.storageBackend !== "server_file") {
|
||||
return NextResponse.json(
|
||||
{ ok: false, message: "UNSUPPORTED_ATTACHMENT_STORAGE_BACKEND" },
|
||||
{ status: 501 },
|
||||
);
|
||||
}
|
||||
|
||||
const absolutePath = resolveServerFileAttachmentAbsolutePath(record.attachment.storagePath);
|
||||
try {
|
||||
await stat(absolutePath);
|
||||
} catch {
|
||||
return NextResponse.json({ ok: false, message: "ATTACHMENT_FILE_NOT_FOUND" }, { status: 404 });
|
||||
}
|
||||
|
||||
const stream = createReadStream(absolutePath);
|
||||
return new NextResponse(Readable.toWeb(stream) as BodyInit, {
|
||||
headers: buildAttachmentDownloadHeaders(record.attachment),
|
||||
});
|
||||
}
|
||||
86
src/app/api/v1/projects/[projectId]/attachments/route.ts
Normal file
86
src/app/api/v1/projects/[projectId]/attachments/route.ts
Normal file
@@ -0,0 +1,86 @@
|
||||
import { randomBytes } from "node:crypto";
|
||||
import { NextRequest, NextResponse } from "next/server";
|
||||
import { requireRequestSession } from "@/lib/boss-auth";
|
||||
import {
|
||||
appendAttachmentMessage,
|
||||
getAttachmentStorageConfig,
|
||||
readState,
|
||||
type MessageAttachment,
|
||||
} from "@/lib/boss-data";
|
||||
import { detectAttachmentKind, resolveAttachmentAnalysisState } from "@/lib/boss-attachments";
|
||||
import { getAttachmentStorageProvider } from "@/lib/boss-storage";
|
||||
|
||||
export const runtime = "nodejs";
|
||||
|
||||
function randomToken(prefix: string) {
|
||||
return `${prefix}-${randomBytes(4).toString("hex")}`;
|
||||
}
|
||||
|
||||
export async function POST(
|
||||
request: NextRequest,
|
||||
context: { params: Promise<{ projectId: string }> },
|
||||
) {
|
||||
const session = await requireRequestSession(request);
|
||||
if (!session) {
|
||||
return NextResponse.json({ ok: false, message: "UNAUTHORIZED" }, { status: 401 });
|
||||
}
|
||||
|
||||
const { projectId } = await context.params;
|
||||
const state = await readState();
|
||||
const project = state.projects.find((item) => item.id === projectId);
|
||||
if (!project) {
|
||||
return NextResponse.json({ ok: false, message: "PROJECT_NOT_FOUND" }, { status: 404 });
|
||||
}
|
||||
|
||||
const form = await request.formData();
|
||||
const file = form.get("file");
|
||||
if (!(file instanceof File)) {
|
||||
return NextResponse.json({ ok: false, message: "FILE_REQUIRED" }, { status: 400 });
|
||||
}
|
||||
|
||||
const bytes = Buffer.from(await file.arrayBuffer());
|
||||
const attachmentId = randomToken("att");
|
||||
const messageId = randomToken("msg");
|
||||
const fileName = file.name || "attachment";
|
||||
const mimeType = file.type || "application/octet-stream";
|
||||
const attachmentKind = detectAttachmentKind(fileName, mimeType);
|
||||
const analysisState = resolveAttachmentAnalysisState(attachmentKind, bytes.byteLength);
|
||||
const storageConfig = await getAttachmentStorageConfig(session.account);
|
||||
const storageProvider = getAttachmentStorageProvider(storageConfig);
|
||||
const stored = await storageProvider.storeAttachment({
|
||||
account: session.account,
|
||||
messageId,
|
||||
attachmentId,
|
||||
fileName,
|
||||
mimeType,
|
||||
buffer: bytes,
|
||||
});
|
||||
|
||||
const attachment: MessageAttachment = {
|
||||
attachmentId,
|
||||
fileName,
|
||||
mimeType,
|
||||
fileSizeBytes: bytes.byteLength,
|
||||
attachmentKind,
|
||||
storageBackend: stored.storageBackend,
|
||||
storagePath: stored.storagePath,
|
||||
previewAvailable: attachmentKind === "image" || attachmentKind === "video" || attachmentKind === "pdf",
|
||||
uploadedAt: new Date().toISOString(),
|
||||
uploadedBy: session.account,
|
||||
analysisState,
|
||||
};
|
||||
|
||||
const message = await appendAttachmentMessage({
|
||||
projectId,
|
||||
sender: "user",
|
||||
senderLabel: session.displayName || "你",
|
||||
attachment,
|
||||
});
|
||||
|
||||
return NextResponse.json({
|
||||
ok: true,
|
||||
attachment,
|
||||
message,
|
||||
downloadUrl: `/api/v1/attachments/${attachmentId}/download`,
|
||||
});
|
||||
}
|
||||
Reference in New Issue
Block a user