Files
mnote/wolai-frontend/src/app/api/media/sign/route.ts
T
2026-01-15 20:54:21 +08:00

162 lines
5.1 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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,
},
});
}