0.1.04 文件树与导入mmap

This commit is contained in:
liaibo
2026-01-07 18:38:56 +08:00
parent 103e36b1c5
commit 294d1d5fb1
28 changed files with 2703 additions and 442 deletions
+88 -21
View File
@@ -1,5 +1,7 @@
import { NextResponse } from "next/server";
import { createSupabaseRouteClient } from "@/lib/supabase/server";
import { extname } from "path";
import { makeUniqueFileName } from "@/lib/file-tree/naming";
export const dynamic = "force-dynamic";
@@ -13,6 +15,39 @@ interface BatchPayload {
}
const BUCKET = process.env.NEXT_PUBLIC_SUPABASE_MEDIA_BUCKET ?? "workspace";
const DEFAULT_DOC_BUCKET = process.env.NEXT_PUBLIC_SUPABASE_DOC_BUCKET ?? "documents";
const parseStoragePath = (fileUrl: string) => {
try {
const url = new URL(fileUrl);
const segments = url.pathname.split("/").filter(Boolean);
const objectIdx = segments.findIndex((seg) => seg === "object");
if (objectIdx === -1 || objectIdx + 2 >= segments.length) return null;
if (segments[objectIdx + 1] === "public") {
const bucket = segments[objectIdx + 2];
const path = segments.slice(objectIdx + 3).join("/");
return { bucket, path };
}
if (segments[objectIdx + 1] === "sign") {
const bucket = segments[objectIdx + 2];
const path = segments.slice(objectIdx + 3).join("/");
return { bucket, path };
}
return null;
} catch {
return null;
}
};
function resolveAssetLocation(asset: any): { bucket: string; path: string } | null {
if (asset?.storage_path) {
return { bucket: asset.bucket || BUCKET, path: asset.storage_path };
}
if (asset?.file_url) {
return parseStoragePath(asset.file_url);
}
return null;
}
export async function POST(request: Request) {
const supabase = await createSupabaseRouteClient();
@@ -47,9 +82,9 @@ export async function POST(request: Request) {
case "delete": {
await Promise.all(
assets.map(async (asset) => {
if (asset.storage_path) {
await supabase.storage.from(asset.bucket || BUCKET).remove([asset.storage_path]);
}
const location = resolveAssetLocation(asset);
if (!location) return;
await supabase.storage.from(location.bucket || BUCKET).remove([location.path]);
}),
);
const { error } = await supabase.from("media_assets").delete().in("id", payload.assetIds);
@@ -63,20 +98,36 @@ export async function POST(request: Request) {
const asset = assets[0];
const ext = asset.file_name?.includes(".") ? `.${asset.file_name.split(".").pop()}` : "";
const newFileName = payload.newName.includes(".") || !ext ? payload.newName : `${payload.newName}${ext}`;
const targetPath = `${session.user.id}/${asset.workspace_id}/${asset.document_id}/assets/${newFileName}`;
if (asset.storage_path) {
const moveResult = await supabase.storage
.from(asset.bucket || BUCKET)
.move(asset.storage_path, targetPath);
const location = resolveAssetLocation(asset);
const safeName = newFileName.replace(/[\\/]/g, "_");
if (location) {
const uniqueId =
typeof crypto.randomUUID === "function"
? crypto.randomUUID()
: `${Date.now()}_${Math.random().toString(16).slice(2)}`;
const targetPath = `${asset.workspace_id}/${asset.document_id}/${Date.now()}-${uniqueId}-${safeName}`;
const bucket = location.bucket || asset.bucket || DEFAULT_DOC_BUCKET || BUCKET;
const moveResult = await supabase.storage.from(bucket).move(location.path, targetPath);
if (moveResult.error) throw moveResult.error;
const { data: signed } = await supabase.storage.from(bucket).createSignedUrl(targetPath, 60 * 60 * 24 * 7);
const signedUrl = signed?.signedUrl ?? null;
const { error } = await supabase
.from("media_assets")
.update({
file_name: safeName,
bucket,
storage_path: targetPath,
file_url: signedUrl,
thumbnail_url: signedUrl,
})
.eq("id", asset.id);
if (error) throw error;
return NextResponse.json({ ok: true });
}
const { error } = await supabase
.from("media_assets")
.update({
file_name: newFileName,
storage_path: targetPath,
file_url: null,
thumbnail_url: null,
file_name: safeName,
})
.eq("id", asset.id);
if (error) throw error;
@@ -95,16 +146,27 @@ export async function POST(request: Request) {
if (docErr || !targetDoc) {
return NextResponse.json({ error: "目标页面不存在" }, { status: 404 });
}
const { data: existingRows } = await supabase
.from("media_assets")
.select("file_name")
.eq("document_id", payload.targetDocumentId);
const existingNames = new Set<string>((existingRows ?? []).map((r) => (r.file_name ?? "").toString()).filter(Boolean));
const results = [];
for (const asset of assets) {
const fileName = asset.file_name ?? "附件";
const targetPath = `${session.user.id}/${targetDoc.workspace_id}/${payload.targetDocumentId}/assets/${fileName}`;
const sourcePath = asset.storage_path;
const bucket = asset.bucket || BUCKET;
if (!sourcePath) continue;
const location = resolveAssetLocation(asset);
if (!location) continue;
const fileName = makeUniqueFileName(asset.file_name ?? "附件", existingNames).replace(/[\\/]/g, "_");
const uniqueId =
typeof crypto.randomUUID === "function"
? crypto.randomUUID()
: `${Date.now()}_${Math.random().toString(16).slice(2)}`;
const targetPath = `${targetDoc.workspace_id}/${payload.targetDocumentId}/${Date.now()}-${uniqueId}-${fileName}`;
const bucket = location.bucket || asset.bucket || DEFAULT_DOC_BUCKET || BUCKET;
if (payload.action === "copy") {
const copyRes = await supabase.storage.from(bucket).copy(sourcePath, targetPath);
const copyRes = await supabase.storage.from(bucket).copy(location.path, targetPath);
if (copyRes.error) throw copyRes.error;
const { data: signed } = await supabase.storage.from(bucket).createSignedUrl(targetPath, 60 * 60 * 24 * 7);
const signedUrl = signed?.signedUrl ?? null;
const { data: inserted, error } = await supabase
.from("media_assets")
.insert({
@@ -116,6 +178,8 @@ export async function POST(request: Request) {
mime_type: asset.mime_type,
bucket,
storage_path: targetPath,
file_url: signedUrl,
thumbnail_url: signedUrl,
created_by: session.user.id,
})
.select("*")
@@ -123,8 +187,10 @@ export async function POST(request: Request) {
if (error) throw error;
results.push(inserted);
} else {
const moveRes = await supabase.storage.from(bucket).move(sourcePath, targetPath);
const moveRes = await supabase.storage.from(bucket).move(location.path, targetPath);
if (moveRes.error) throw moveRes.error;
const { data: signed } = await supabase.storage.from(bucket).createSignedUrl(targetPath, 60 * 60 * 24 * 7);
const signedUrl = signed?.signedUrl ?? null;
const { data: updated, error } = await supabase
.from("media_assets")
.update({
@@ -132,8 +198,9 @@ export async function POST(request: Request) {
document_id: payload.targetDocumentId,
storage_path: targetPath,
file_name: fileName,
file_url: null,
thumbnail_url: null,
bucket,
file_url: signedUrl,
thumbnail_url: signedUrl,
})
.eq("id", asset.id)
.select("*")
@@ -61,6 +61,8 @@ export async function POST(request: Request) {
document_id: documentId,
file_url: signedUrl,
thumbnail_url: signedUrl,
bucket: DOC_BUCKET,
storage_path: path,
asset_type: assetType,
file_name: file.name,
file_size: file.size,