Compare commits

...
Author SHA1 Message Date
RuoJi6 0ed68d9143 Merge pull request #100 from RuoJi6/codex/fix-export-utf8-filenames
修复导出中文文件名乱码
2026-10-30 00:08:44 -07:00
ji ruo 542d7e9ca6 fix: preserve UTF-8 filenames for exports 2026-10-30 15:06:37 +08:00
RuoJi6 35a33b20f9 Merge pull request #98 from RuoJi6/codex/fix-image-drag-ghost
修复截图拖动时的原生拖拽重影
2026-10-29 22:13:34 -07:00
2 changed files with 26 additions and 4 deletions

No files matched your search

+4 -1
View File
@@ -2332,7 +2332,10 @@ func (s *Server) findingsExport(w http.ResponseWriter, r *http.Request) {
stamp := now.Format("20060102-150405") stamp := now.Format("20060102-150405")
setDownload := func(contentType, filename string) { setDownload := func(contentType, filename string) {
w.Header().Set("Content-Type", contentType) w.Header().Set("Content-Type", contentType)
w.Header().Set("Content-Disposition", fmt.Sprintf(`attachment; filename="%s"`, filename)) // Keep an ASCII fallback for older clients and provide the real UTF-8
// name through RFC 5987. Browsers otherwise commonly decode Chinese
// names using the platform legacy code page.
w.Header().Set("Content-Disposition", fmt.Sprintf(`attachment; filename="%s"; filename*=UTF-8''%s`, sanitizeFilename(filename), url.PathEscape(filename)))
} }
switch format { switch format {
+22 -3
View File
@@ -119,6 +119,24 @@ import type {
SystemMetrics, SystemMetrics,
} from "@/lib/types"; } from "@/lib/types";
/**
* Read a download name from Content-Disposition. RFC 5987's filename* is
* preferred because it preserves UTF-8 names; older filename remains the
* fallback for clients that do not understand it.
*/
export function downloadFilename(contentDisposition: string, fallback: string): string {
const encoded = contentDisposition.match(/(?:^|;)\s*filename\*\s*=\s*UTF-8''([^;]+)/i)?.[1];
if (encoded) {
try {
return decodeURIComponent(encoded.replace(/^"|"$/g, ""));
} catch {
// Fall through to the legacy token when a malformed header is received.
}
}
const legacy = contentDisposition.match(/(?:^|;)\s*filename\s*=\s*(?:"([^"]*)"|([^;\s]+))/i);
return (legacy?.[1] ?? legacy?.[2] ?? fallback).trim() || fallback;
}
function getToken(): string | null { function getToken(): string | null {
if (typeof window === "undefined") return null; if (typeof window === "undefined") return null;
return localStorage.getItem("artex_token"); return localStorage.getItem("artex_token");
@@ -534,6 +552,7 @@ export const api = {
}, },
workspaceDownload: async (path: string) => { workspaceDownload: async (path: string) => {
let blob: Blob; let blob: Blob;
let contentDisposition = "";
if (MOCK) { if (MOCK) {
blob = new Blob([`(demo)${path} 的下载内容示例。`], { type: "text/plain" }); blob = new Blob([`(demo)${path} 的下载内容示例。`], { type: "text/plain" });
} else { } else {
@@ -542,12 +561,13 @@ export const api = {
headers: { ...(token ? { Authorization: `Bearer ${token}` } : {}) }, headers: { ...(token ? { Authorization: `Bearer ${token}` } : {}) },
}); });
if (!r.ok) throw new Error(`下载失败: ${r.status}`); if (!r.ok) throw new Error(`下载失败: ${r.status}`);
contentDisposition = r.headers.get("Content-Disposition") ?? "";
blob = await r.blob(); blob = await r.blob();
} }
const objUrl = URL.createObjectURL(blob); const objUrl = URL.createObjectURL(blob);
const a = document.createElement("a"); const a = document.createElement("a");
a.href = objUrl; a.href = objUrl;
a.download = path.split("/").pop() || "download"; a.download = downloadFilename(contentDisposition, path.split("/").pop() || "download");
document.body.appendChild(a); document.body.appendChild(a);
a.click(); a.click();
a.remove(); a.remove();
@@ -703,8 +723,7 @@ export const api = {
const blob = await r.blob(); const blob = await r.blob();
// 文件名优先取后端 Content-Disposition,取不到则用默认名。 // 文件名优先取后端 Content-Disposition,取不到则用默认名。
const disp = r.headers.get("Content-Disposition") ?? ""; const disp = r.headers.get("Content-Disposition") ?? "";
const m = disp.match(/filename="?([^"]+)"?/); const filename = downloadFilename(disp, `findings-export`);
const filename = m?.[1] ?? `findings-export`;
const url = URL.createObjectURL(blob); const url = URL.createObjectURL(blob);
const a = document.createElement("a"); const a = document.createElement("a");
a.href = url; a.href = url;