import { NextResponse } from "next/server"; import { createSupabaseRouteClient } from "@/lib/supabase/server"; import { rewriteToPublicOrigin } from "@/lib/url/rewritePublicOrigin"; import { getMnoteRuntimeConfig } from "@/lib/runtime-config"; import supabaseAdmin from "@/lib/supabase/admin"; export const dynamic = "force-dynamic"; const isUuid = (value: string) => /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i.test(value); const resolveAssetObjectLocation = async (params: { bucket?: string | null; storagePath?: string | null; workspaceId?: string | null; fileName?: string | null; }) => { const { bucket, storagePath, workspaceId, fileName } = params; const ws = (workspaceId ?? "").trim(); const fn = (fileName ?? "").trim(); if (!ws || !isUuid(ws) || !fn) return null; try { const storage = supabaseAdmin.schema("storage"); // 1) 若已给出 bucket/path,先校验是否存在 const b = (bucket ?? "").trim(); const p = (storagePath ?? "").trim(); if (b && p) { const { data: exact, error: exactError } = await storage .from("objects") .select("bucket_id,name") .eq("bucket_id", b) .eq("name", p) .limit(1); if (!exactError && exact && exact.length > 0) { return { bucket: b, path: p }; } } // 2) 兼容历史数据:按 workspace_id/%/file_name 查找 const pattern = `${ws}/%/${fn}`; const { data: candidates, error: candError } = await storage .from("objects") .select("bucket_id,name,created_at") .like("name", pattern) .order("created_at", { ascending: false }) .limit(20); if (candError || !candidates || candidates.length === 0) return null; const exactCandidate = candidates.find((o) => String(o?.name || "").endsWith(`/${fn}`)) ?? candidates[0]; if (!exactCandidate?.bucket_id || !exactCandidate?.name) return null; return { bucket: String(exactCandidate.bucket_id), path: String(exactCandidate.name) }; } catch { return null; } }; export async function GET(request: Request) { const supabase = await createSupabaseRouteClient(); const { data: { user }, error: authError, } = await supabase.auth.getUser(); if (authError || !user) { return NextResponse.json({ error: "未登录" }, { status: 401 }); } const { searchParams } = new URL(request.url); const assetId = searchParams.get("assetId"); if (!assetId) { return NextResponse.json({ error: "缺少 assetId" }, { status: 400 }); } // 获取资源信息 const { data: asset, error: assetError } = await supabase .from("media_assets") .select("*") .eq("id", assetId) .single(); if (assetError || !asset) { return NextResponse.json({ error: "资源不存在" }, { status: 404 }); } // 兼容:早期/异常写入的 media_assets 可能缺少 bucket/storage_path, // 导致生成的签名链接指向不存在的对象(ONLYOFFICE 会报“下载失败”)。 let bucket = (asset.bucket as string | null) ?? null; let storagePath = (asset.storage_path as string | null) ?? null; if (!bucket || !storagePath) { const resolved = await resolveAssetObjectLocation({ bucket, storagePath, workspaceId: asset.workspace_id ?? null, fileName: asset.file_name ?? null, }); if (!resolved) { return NextResponse.json({ error: "未找到存储对象,无法生成签名链接" }, { status: 404 }); } bucket = resolved.bucket; storagePath = resolved.path; // 尽量回填,避免后续重复修复 await supabase .from("media_assets") .update({ bucket, storage_path: storagePath }) .eq("id", asset.id); } // 对于图片,添加高质量参数以获得更好的显示效果 // Supabase Storage 支持通过 URL 参数控制图片质量和尺寸 const isImage = asset.mime_type?.startsWith("image/"); let signedUrl: string; if (isImage) { // 使用 createSignedUrl 并添加高质量参数 // 注意:transform 参数需要在签名时指定 const { data: signedUrlData, error: signError } = await supabase.storage .from(bucket) .createSignedUrl(storagePath, 60 * 60, { // 添加转换参数以获得高质量图片 transform: { quality: 95, // 高质量 format: "origin", // 保持原始格式 }, }); if (signError || !signedUrlData) { return NextResponse.json({ error: "生成签名 URL 失败" }, { status: 500 }); } signedUrl = signedUrlData.signedUrl; } else { // 非图片文件,直接生成签名 URL const { data: signedUrlData, error: signError } = await supabase.storage .from(bucket) .createSignedUrl(storagePath, 60 * 60); if (signError || !signedUrlData) { return NextResponse.json({ error: "生成签名 URL 失败" }, { status: 500 }); } signedUrl = signedUrlData.signedUrl; } signedUrl = rewriteToPublicOrigin(signedUrl, getMnoteRuntimeConfig().supabaseUrl); return NextResponse.json({ signedUrl, asset: { id: asset.id, file_name: asset.file_name, mime_type: asset.mime_type, file_size: asset.file_size, }, }); }