0.1.04 文件树与导入mmap
This commit is contained in:
@@ -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,
|
||||
|
||||
Reference in New Issue
Block a user