0.1.14 上线前更改

This commit is contained in:
liaibo
2026-01-11 12:35:53 +08:00
parent be71849aa5
commit 725a60d3aa
44 changed files with 3427 additions and 310 deletions
@@ -2,6 +2,7 @@ import { NextResponse } from "next/server";
import { createSupabaseRouteClient } from "@/lib/supabase/server";
import { extname } from "path";
import { makeUniqueFileName } from "@/lib/file-tree/naming";
import { rewriteToPublicOrigin } from "@/lib/url/rewritePublicOrigin";
export const dynamic = "force-dynamic";
@@ -11,6 +12,7 @@ interface BatchPayload {
action: Action;
assetIds: string[];
targetDocumentId?: string;
targetSubPath?: string;
newName?: string;
}
@@ -41,7 +43,7 @@ const parseStoragePath = (fileUrl: string) => {
function resolveAssetLocation(asset: any): { bucket: string; path: string } | null {
if (asset?.storage_path) {
return { bucket: asset.bucket || BUCKET, path: asset.storage_path };
return { bucket: asset.bucket || BUCKET, path: asset.storage_path };
}
if (asset?.file_url) {
return parseStoragePath(asset.file_url);
@@ -49,6 +51,18 @@ function resolveAssetLocation(asset: any): { bucket: string; path: string } | nu
return null;
}
function sanitizeSubPath(input: string | undefined): string {
const raw = typeof input === "string" ? input : "";
const cleaned = raw
.replace(/\\/g, "/")
.split("/")
.map((seg) => seg.trim())
.filter((seg) => Boolean(seg) && seg !== "." && seg !== "..")
.map((seg) => seg.replace(/[\\/]/g, "_"))
.slice(0, 8);
return cleaned.join("/");
}
export async function POST(request: Request) {
const supabase = await createSupabaseRouteClient();
const {
@@ -117,12 +131,21 @@ export async function POST(request: Request) {
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 baseDir = (() => {
const current = location.path;
const idx = current.lastIndexOf("/");
if (idx > 0) return current.slice(0, idx);
return `${asset.workspace_id}/${asset.document_id}`;
})();
const targetPath = `${baseDir}/${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;
let signedUrl = signed?.signedUrl ?? null;
if (signedUrl) {
signedUrl = rewriteToPublicOrigin(signedUrl, process.env.NEXT_PUBLIC_SUPABASE_URL);
}
const { error } = await supabase
.from("media_assets")
.update({
@@ -172,13 +195,18 @@ export async function POST(request: Request) {
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 subdir = sanitizeSubPath(payload.targetSubPath);
const prefix = `${targetDoc.workspace_id}/${payload.targetDocumentId}${subdir ? `/${subdir}` : ""}`;
const targetPath = `${prefix}/${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(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;
let signedUrl = signed?.signedUrl ?? null;
if (signedUrl) {
signedUrl = rewriteToPublicOrigin(signedUrl, process.env.NEXT_PUBLIC_SUPABASE_URL);
}
const { data: inserted, error } = await supabase
.from("media_assets")
.insert({
@@ -202,7 +230,10 @@ export async function POST(request: Request) {
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;
let signedUrl = signed?.signedUrl ?? null;
if (signedUrl) {
signedUrl = rewriteToPublicOrigin(signedUrl, process.env.NEXT_PUBLIC_SUPABASE_URL);
}
const { data: updated, error } = await supabase
.from("media_assets")
.update({
@@ -1,5 +1,6 @@
import { NextResponse } from "next/server";
import { createSupabaseRouteClient } from "@/lib/supabase/server";
import { rewriteToPublicOrigin } from "@/lib/url/rewritePublicOrigin";
export const dynamic = "force-dynamic";
@@ -66,6 +67,8 @@ export async function GET(request: Request) {
signedUrl = signedUrlData.signedUrl;
}
signedUrl = rewriteToPublicOrigin(signedUrl, process.env.NEXT_PUBLIC_SUPABASE_URL);
return NextResponse.json({
signedUrl,
asset: {
@@ -1,8 +1,28 @@
import { NextResponse } from "next/server";
import supabaseAdmin from "@/lib/supabase/admin";
import { rewriteToPublicOrigin } from "@/lib/url/rewritePublicOrigin";
export const dynamic = "force-dynamic";
const applyHostOverride = (rawUrl: string, hostOverride: string) => {
try {
const u = new URL(rawUrl);
// 支持两种写法:hostname 或完整 originhttps://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;
}
};
const parseStoragePath = (fileUrl: string) => {
try {
const url = new URL(fileUrl);
@@ -51,27 +71,22 @@ export async function GET(request: Request) {
const { bucket, path } = parsed;
const { data, error } = await supabaseAdmin.storage
.from(bucket)
.createSignedUrl(path, 60 * 60, { download: fileName });
// OnlyOffice 需要“可被文档服务器拉取”的 URL:不要强制 downloadContent-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 });
if (error || !data?.signedUrl) {
return NextResponse.json({ error: error?.message ?? "生成签名 URL 失败" }, { status: 500 });
}
let signedUrl = data.signedUrl;
if (
forOnlyOffice &&
hostOverride &&
(signedUrl.includes("127.0.0.1") || signedUrl.includes("localhost"))
) {
try {
const url = new URL(signedUrl);
url.hostname = hostOverride;
signedUrl = url.toString();
} catch {
// ignore parse errors
}
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);
}
return NextResponse.json({ signedUrl });
@@ -2,6 +2,7 @@ import { NextResponse } from "next/server";
import { createSupabaseRouteClient } from "@/lib/supabase/server";
import type { MediaAsset } from "@/types/media";
import { extname } from "path";
import { rewriteToPublicOrigin } from "@/lib/url/rewritePublicOrigin";
export const dynamic = "force-dynamic";
@@ -28,6 +29,7 @@ export async function POST(request: Request) {
const file = formData.get("file");
const workspaceId = String(formData.get("workspaceId") ?? "");
const documentId = String(formData.get("documentId") ?? "");
const mindmapIdRaw = String(formData.get("mindmapId") ?? "").trim();
if (!(file instanceof File) || !workspaceId || !documentId) {
return NextResponse.json({ error: "缺少必要参数" }, { status: 400 });
@@ -38,7 +40,11 @@ export async function POST(request: Request) {
const buffer = Buffer.from(arrayBuffer);
const extension = extname(file.name || "").replace(/\s+/g, "");
const uniqueId = typeof crypto.randomUUID === "function" ? crypto.randomUUID() : Math.random().toString(36).slice(2);
const path = `${workspaceId}/${Date.now()}-${uniqueId}${extension}`;
// 与 /api/media/batch 的 move/rename 规则对齐:放到 workspaceId/documentId 下
const mindmapId =
mindmapIdRaw && /^[a-zA-Z0-9_-]{1,128}$/.test(mindmapIdRaw) ? mindmapIdRaw : "";
const subdir = mindmapId ? `mindmaps/${mindmapId}` : "";
const path = `${workspaceId}/${documentId}${subdir ? `/${subdir}` : ""}/${Date.now()}-${uniqueId}${extension}`;
const assetType = resolveAssetType(file.type || "");
const { error: uploadError } = await supabase.storage.from(DOC_BUCKET).upload(path, buffer, {
@@ -66,6 +72,8 @@ export async function POST(request: Request) {
signedUrl = signed?.signedUrl ?? "";
}
signedUrl = rewriteToPublicOrigin(signedUrl, process.env.NEXT_PUBLIC_SUPABASE_URL);
const { data: asset, error } = await supabase
.from("media_assets")
.insert({