2025-12-26 07:52:40 +08:00
|
|
|
|
import { NextResponse } from "next/server";
|
|
|
|
|
|
import supabaseAdmin from "@/lib/supabase/admin";
|
2026-01-11 12:35:53 +08:00
|
|
|
|
import { rewriteToPublicOrigin } from "@/lib/url/rewritePublicOrigin";
|
2025-12-26 07:52:40 +08:00
|
|
|
|
|
|
|
|
|
|
export const dynamic = "force-dynamic";
|
|
|
|
|
|
|
2026-01-11 12:35:53 +08:00
|
|
|
|
const applyHostOverride = (rawUrl: string, hostOverride: string) => {
|
|
|
|
|
|
try {
|
|
|
|
|
|
const u = new URL(rawUrl);
|
|
|
|
|
|
|
|
|
|
|
|
// 支持两种写法:hostname 或完整 origin(https://xxx:port)
|
|
|
|
|
|
if (/^https?:\/\//i.test(hostOverride)) {
|
|
|
|
|
|
const ov = new URL(hostOverride);
|
|
|
|
|
|
u.protocol = ov.protocol;
|
|
|
|
|
|
u.host = ov.host;
|
|
|
|
|
|
return u.toString();
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
u.hostname = hostOverride;
|
|
|
|
|
|
return u.toString();
|
|
|
|
|
|
} catch {
|
|
|
|
|
|
return rawUrl;
|
|
|
|
|
|
}
|
|
|
|
|
|
};
|
|
|
|
|
|
|
2025-12-26 07:52:40 +08:00
|
|
|
|
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;
|
|
|
|
|
|
// pattern 1: /storage/v1/object/public/<bucket>/<path...>
|
|
|
|
|
|
if (segments[objectIdx + 1] === "public") {
|
|
|
|
|
|
const bucket = segments[objectIdx + 2];
|
|
|
|
|
|
const path = segments.slice(objectIdx + 3).join("/");
|
|
|
|
|
|
return { bucket, path };
|
|
|
|
|
|
}
|
|
|
|
|
|
// pattern 2: /storage/v1/object/sign/<bucket>/<path...> (token in query)
|
|
|
|
|
|
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;
|
|
|
|
|
|
}
|
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
export async function GET(request: Request) {
|
|
|
|
|
|
const { searchParams } = new URL(request.url);
|
|
|
|
|
|
const fileUrl = searchParams.get("fileUrl");
|
|
|
|
|
|
const fileName = searchParams.get("fileName") ?? undefined;
|
|
|
|
|
|
const forOnlyOffice = searchParams.get("for") === "onlyoffice";
|
|
|
|
|
|
const hostOverride = process.env.NEXT_PUBLIC_ONLYOFFICE_STORAGE_HOST_OVERRIDE;
|
|
|
|
|
|
if (searchParams.get("debug") === "1") {
|
|
|
|
|
|
return NextResponse.json({
|
|
|
|
|
|
keyLen: (process.env.SUPABASE_SERVICE_ROLE_KEY || "").length,
|
|
|
|
|
|
url: process.env.SUPABASE_URL,
|
|
|
|
|
|
});
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
if (!fileUrl) {
|
|
|
|
|
|
return NextResponse.json({ error: "缺少 fileUrl" }, { status: 400 });
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
const parsed = parseStoragePath(fileUrl);
|
|
|
|
|
|
if (!parsed) {
|
|
|
|
|
|
return NextResponse.json({ error: "无法解析 Supabase 存储路径" }, { status: 400 });
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
const { bucket, path } = parsed;
|
|
|
|
|
|
|
2026-01-11 12:35:53 +08:00
|
|
|
|
// OnlyOffice 需要“可被文档服务器拉取”的 URL:不要强制 download(Content-Disposition: attachment)。
|
|
|
|
|
|
const { data, error } = forOnlyOffice
|
|
|
|
|
|
? await supabaseAdmin.storage.from(bucket).createSignedUrl(path, 60 * 60)
|
|
|
|
|
|
: await supabaseAdmin.storage.from(bucket).createSignedUrl(path, 60 * 60, { download: fileName });
|
2025-12-26 07:52:40 +08:00
|
|
|
|
|
|
|
|
|
|
if (error || !data?.signedUrl) {
|
|
|
|
|
|
return NextResponse.json({ error: error?.message ?? "生成签名 URL 失败" }, { status: 500 });
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
let signedUrl = data.signedUrl;
|
2026-01-11 12:35:53 +08:00
|
|
|
|
if (forOnlyOffice && hostOverride) {
|
|
|
|
|
|
// 优先按 OnlyOffice 专用回源地址改写(可设置为 http://host.docker.internal:18000 以降低延迟)
|
|
|
|
|
|
signedUrl = applyHostOverride(signedUrl, hostOverride);
|
|
|
|
|
|
} else {
|
|
|
|
|
|
// 返回给浏览器的 URL 必须是公网可达的(不能是 127.0.0.1:18000)
|
|
|
|
|
|
signedUrl = rewriteToPublicOrigin(signedUrl, process.env.NEXT_PUBLIC_SUPABASE_URL);
|
2025-12-26 07:52:40 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
return NextResponse.json({ signedUrl });
|
|
|
|
|
|
}
|