142 lines
4.3 KiB
TypeScript
142 lines
4.3 KiB
TypeScript
import JSZip from "jszip";
|
|
import { NextResponse } from "next/server";
|
|
import { auth } from "@/app/(auth)/auth";
|
|
import { getAllStorageConfigs } from "@/lib/db/queries";
|
|
import {
|
|
countOpenClawArchiveRootFolders,
|
|
sanitizeOpenClawArchiveEntryPath,
|
|
sanitizeOpenClawArchiveName,
|
|
} from "@/lib/openclaw-chat/folder-archive";
|
|
import {
|
|
OPENCLAW_CHAT_FOLDER_MAX_FILES,
|
|
} from "@/lib/openclaw-chat/media-attachments";
|
|
import { createLogger } from "@/lib/observability/logger";
|
|
import {
|
|
toDbS3UploadConfig,
|
|
uploadFileWithS3Config,
|
|
} from "@/lib/storage/service";
|
|
|
|
export const runtime = "nodejs";
|
|
export const maxDuration = 600;
|
|
|
|
const logger = createLogger("openclaw-folder-upload");
|
|
const LEGACY_FOLDER_UPLOAD_MAX_BYTES = 300 * 1024 * 1024;
|
|
|
|
function isUploadedFile(value: FormDataEntryValue): value is File {
|
|
return (
|
|
typeof value === "object" &&
|
|
value !== null &&
|
|
"arrayBuffer" in value &&
|
|
"name" in value &&
|
|
"size" in value
|
|
);
|
|
}
|
|
|
|
function toArrayBuffer(buffer: Buffer) {
|
|
return buffer.buffer.slice(
|
|
buffer.byteOffset,
|
|
buffer.byteOffset + buffer.byteLength
|
|
) as ArrayBuffer;
|
|
}
|
|
|
|
export async function POST(request: Request) {
|
|
const session = await auth();
|
|
if (!session?.user?.id) {
|
|
return NextResponse.json({ error: "请先登录" }, { status: 401 });
|
|
}
|
|
|
|
try {
|
|
const formData = await request.formData();
|
|
const files = formData
|
|
.getAll("files")
|
|
.filter((entry): entry is File => isUploadedFile(entry));
|
|
const relativePaths = formData
|
|
.getAll("relativePaths")
|
|
.map((entry) => (typeof entry === "string" ? entry : ""));
|
|
|
|
if (files.length === 0) {
|
|
return NextResponse.json(
|
|
{ error: "所选文件夹中没有可上传文件" },
|
|
{ status: 400 }
|
|
);
|
|
}
|
|
if (files.length > OPENCLAW_CHAT_FOLDER_MAX_FILES) {
|
|
return NextResponse.json(
|
|
{ error: `单次最多打包 ${OPENCLAW_CHAT_FOLDER_MAX_FILES} 个文件` },
|
|
{ status: 413 }
|
|
);
|
|
}
|
|
|
|
const originalSize = files.reduce((sum, file) => sum + file.size, 0);
|
|
if (originalSize > LEGACY_FOLDER_UPLOAD_MAX_BYTES) {
|
|
return NextResponse.json(
|
|
{ error: "请刷新页面后使用支持 2GB 和断点续传的新文件夹上传方式" },
|
|
{ status: 413 }
|
|
);
|
|
}
|
|
|
|
const archivePaths = files.map((file, index) =>
|
|
sanitizeOpenClawArchiveEntryPath(
|
|
relativePaths[index] || file.name,
|
|
file.name,
|
|
index
|
|
)
|
|
);
|
|
const zip = new JSZip();
|
|
for (const [index, file] of files.entries()) {
|
|
zip.file(archivePaths[index], await file.arrayBuffer(), {
|
|
date: new Date(file.lastModified || Date.now()),
|
|
});
|
|
}
|
|
|
|
const archiveBaseName = sanitizeOpenClawArchiveName(
|
|
String(formData.get("archiveName") || archivePaths[0]?.split("/")[0] || "")
|
|
);
|
|
const archiveName = `${archiveBaseName.replace(/\.zip$/i, "")}.zip`;
|
|
const archiveBuffer = await zip.generateAsync({
|
|
type: "nodebuffer",
|
|
compression: "DEFLATE",
|
|
compressionOptions: { level: 6 },
|
|
platform: "UNIX",
|
|
});
|
|
|
|
const storageConfigs = await getAllStorageConfigs();
|
|
const qiniuStorage = storageConfigs
|
|
.filter((config) => config.type === "qiniu-kodo" && config.isEnabled)
|
|
.sort((left, right) => Number(right.isDefault) - Number(left.isDefault))
|
|
.find((config) => toDbS3UploadConfig(config));
|
|
const qiniuConfig = toDbS3UploadConfig(qiniuStorage);
|
|
if (!qiniuConfig) {
|
|
return NextResponse.json(
|
|
{ error: "系统尚未配置可用的七牛云储存桶" },
|
|
{ status: 503 }
|
|
);
|
|
}
|
|
|
|
const uploaded = await uploadFileWithS3Config(
|
|
archiveName,
|
|
toArrayBuffer(archiveBuffer),
|
|
qiniuConfig
|
|
);
|
|
if (!/^https?:\/\//i.test(uploaded.url)) {
|
|
throw new Error("七牛云未返回可访问的公网地址");
|
|
}
|
|
|
|
return NextResponse.json({
|
|
url: uploaded.url,
|
|
name: archiveName,
|
|
contentType: "application/zip",
|
|
size: archiveBuffer.byteLength,
|
|
originalSize,
|
|
fileCount: files.length,
|
|
folderCount: countOpenClawArchiveRootFolders(archivePaths),
|
|
});
|
|
} catch (error) {
|
|
logger.error("Failed to package and upload Claw folders", error);
|
|
return NextResponse.json(
|
|
{ error: error instanceof Error ? error.message : "文件夹上传失败" },
|
|
{ status: 500 }
|
|
);
|
|
}
|
|
}
|