Преглед изворни кода

Fix file downloads showing generic filenames for special characters (#334)

Content-Disposition parser now handles RFC 5987 percent-encoded filenames
(filename*=utf-8''...) used by FastAPI for non-ASCII characters. Applied
to all 7 download endpoints.

Closes #334
maziggy пре 6 месеци
родитељ
комит
13de96127e
4 измењених фајлова са 22 додато и 16 уклоњено
  1. 1 0
      CHANGELOG.md
  2. 20 15
      frontend/src/api/client.ts
  3. 0 0
      static/assets/index-Cqn1dtJ3.js
  4. 1 1
      static/index.html

+ 1 - 0
CHANGELOG.md

@@ -7,6 +7,7 @@ All notable changes to Bambuddy will be documented in this file.
 ### Fixed
 ### Fixed
 - **External Camera Not Used for Snapshot + Stream Dropping** ([#325](https://github.com/maziggy/bambuddy/issues/325)) — The snapshot endpoint (`/camera/snapshot`) always used the internal printer camera even when an external camera was configured. Now checks for external camera first, matching the existing stream endpoint behavior. Also fixed external MJPEG and RTSP streams silently dropping every ~60 seconds due to missing reconnect logic — the underlying stream generators exit on read timeout, and the caller now retries up to 3 times with a 2-second delay instead of ending the stream.
 - **External Camera Not Used for Snapshot + Stream Dropping** ([#325](https://github.com/maziggy/bambuddy/issues/325)) — The snapshot endpoint (`/camera/snapshot`) always used the internal printer camera even when an external camera was configured. Now checks for external camera first, matching the existing stream endpoint behavior. Also fixed external MJPEG and RTSP streams silently dropping every ~60 seconds due to missing reconnect logic — the underlying stream generators exit on read timeout, and the caller now retries up to 3 times with a 2-second delay instead of ending the stream.
 - **H2C Nozzle Rack Text Unreadable on Light Filament Colors** ([#300](https://github.com/maziggy/bambuddy/issues/300)) — Nozzle rack slots use the loaded filament color as background, but white/light filaments made the white "0.4" text nearly invisible. Now uses a luminance check to switch to dark text on light backgrounds.
 - **H2C Nozzle Rack Text Unreadable on Light Filament Colors** ([#300](https://github.com/maziggy/bambuddy/issues/300)) — Nozzle rack slots use the loaded filament color as background, but white/light filaments made the white "0.4" text nearly invisible. Now uses a luminance check to switch to dark text on light backgrounds.
+- **File Downloads Show Generic Filenames** ([#334](https://github.com/maziggy/bambuddy/issues/334)) — Downloaded files with special characters in their names (spaces, umlauts, parentheses) were saved as generic `file_1`, `file_2` instead of the original filename. The `Content-Disposition` header parser now handles RFC 5987 percent-encoded filenames (`filename*=utf-8''...`) used by FastAPI for non-ASCII characters. Fix applied to all download endpoints (library files, archives, source files, F3D files, project exports, support bundles, printer files).
 
 
 ### Improved
 ### Improved
 - **Additional Currency Options** ([#329](https://github.com/maziggy/bambuddy/issues/329), [#333](https://github.com/maziggy/bambuddy/issues/333)) — Added 17 additional currencies to the cost tracking dropdown: HKD, INR, KRW, SEK, NOK, DKK, PLN, BRL, TWD, SGD, NZD, MXN, CZK, THB, ZAR, RUB.
 - **Additional Currency Options** ([#329](https://github.com/maziggy/bambuddy/issues/329), [#333](https://github.com/maziggy/bambuddy/issues/333)) — Added 17 additional currencies to the cost tracking dropdown: HKD, INR, KRW, SEK, NOK, DKK, PLN, BRL, TWD, SGD, NZD, MXN, CZK, THB, ZAR, RUB.

+ 20 - 15
frontend/src/api/client.ts

@@ -18,6 +18,18 @@ export function getAuthToken(): string | null {
   return authToken;
   return authToken;
 }
 }
 
 
+function parseContentDispositionFilename(header: string | null): string | null {
+  if (!header) return null;
+  // RFC 5987: filename*=utf-8''percent-encoded-name
+  const rfc5987Match = header.match(/filename\*=(?:UTF-8|utf-8)''(.+?)(?:;|$)/);
+  if (rfc5987Match) {
+    try { return decodeURIComponent(rfc5987Match[1]); } catch { /* fall through */ }
+  }
+  // Standard: filename="name" or filename=name
+  const standardMatch = header.match(/filename="?([^";\n]+)"?/);
+  return standardMatch?.[1] || null;
+}
+
 async function request<T>(
 async function request<T>(
   endpoint: string,
   endpoint: string,
   options: RequestInit = {}
   options: RequestInit = {}
@@ -2019,7 +2031,7 @@ export const api = {
     request<{ message: string; auth_enabled: boolean }>('/auth/disable', {
     request<{ message: string; auth_enabled: boolean }>('/auth/disable', {
       method: 'POST',
       method: 'POST',
     }),
     }),
-  
+
   // Advanced Authentication
   // Advanced Authentication
   testSMTP: (data: TestSMTPRequest) =>
   testSMTP: (data: TestSMTPRequest) =>
     request<TestSMTPResponse>('/auth/smtp/test', {
     request<TestSMTPResponse>('/auth/smtp/test', {
@@ -2263,8 +2275,7 @@ export const api = {
       throw new Error(error.detail || `HTTP ${response.status}`);
       throw new Error(error.detail || `HTTP ${response.status}`);
     }
     }
     const disposition = response.headers.get('Content-Disposition');
     const disposition = response.headers.get('Content-Disposition');
-    const filenameMatch = disposition?.match(/filename="?([^";\n]+)"?/);
-    const filename = filenameMatch?.[1] || path.split('/').pop() || 'download';
+    const filename = parseContentDispositionFilename(disposition) || path.split('/').pop() || 'download';
     const blob = await response.blob();
     const blob = await response.blob();
     const url = window.URL.createObjectURL(blob);
     const url = window.URL.createObjectURL(blob);
     const a = document.createElement('a');
     const a = document.createElement('a');
@@ -2464,8 +2475,7 @@ export const api = {
       throw new Error(error.detail || `HTTP ${response.status}`);
       throw new Error(error.detail || `HTTP ${response.status}`);
     }
     }
     const disposition = response.headers.get('Content-Disposition');
     const disposition = response.headers.get('Content-Disposition');
-    const filenameMatch = disposition?.match(/filename="?([^";\n]+)"?/);
-    const downloadFilename = filenameMatch?.[1] || filename || `archive_${id}.3mf`;
+    const downloadFilename = parseContentDispositionFilename(disposition) || filename || `archive_${id}.3mf`;
     const blob = await response.blob();
     const blob = await response.blob();
     const url = window.URL.createObjectURL(blob);
     const url = window.URL.createObjectURL(blob);
     const a = document.createElement('a');
     const a = document.createElement('a');
@@ -2605,8 +2615,7 @@ export const api = {
       throw new Error(error.detail || `HTTP ${response.status}`);
       throw new Error(error.detail || `HTTP ${response.status}`);
     }
     }
     const disposition = response.headers.get('Content-Disposition');
     const disposition = response.headers.get('Content-Disposition');
-    const filenameMatch = disposition?.match(/filename="?([^";\n]+)"?/);
-    const filename = filenameMatch?.[1] || `source_${archiveId}.3mf`;
+    const filename = parseContentDispositionFilename(disposition) || `source_${archiveId}.3mf`;
     const blob = await response.blob();
     const blob = await response.blob();
     const url = window.URL.createObjectURL(blob);
     const url = window.URL.createObjectURL(blob);
     const a = document.createElement('a');
     const a = document.createElement('a');
@@ -2655,8 +2664,7 @@ export const api = {
       throw new Error(error.detail || `HTTP ${response.status}`);
       throw new Error(error.detail || `HTTP ${response.status}`);
     }
     }
     const disposition = response.headers.get('Content-Disposition');
     const disposition = response.headers.get('Content-Disposition');
-    const filenameMatch = disposition?.match(/filename="?([^";\n]+)"?/);
-    const filename = filenameMatch?.[1] || `archive_${archiveId}.f3d`;
+    const filename = parseContentDispositionFilename(disposition) || `archive_${archiveId}.f3d`;
     const blob = await response.blob();
     const blob = await response.blob();
     const url = window.URL.createObjectURL(blob);
     const url = window.URL.createObjectURL(blob);
     const a = document.createElement('a');
     const a = document.createElement('a');
@@ -3539,8 +3547,7 @@ export const api = {
       throw new Error(error.detail || `HTTP ${response.status}`);
       throw new Error(error.detail || `HTTP ${response.status}`);
     }
     }
     const contentDisposition = response.headers.get('Content-Disposition');
     const contentDisposition = response.headers.get('Content-Disposition');
-    const filenameMatch = contentDisposition?.match(/filename="(.+)"/);
-    const filename = filenameMatch?.[1] || `project_${projectId}.zip`;
+    const filename = parseContentDispositionFilename(contentDisposition) || `project_${projectId}.zip`;
     const blob = await response.blob();
     const blob = await response.blob();
     return { blob, filename };
     return { blob, filename };
   },
   },
@@ -3668,8 +3675,7 @@ export const api = {
       throw new Error(error.detail || `HTTP ${response.status}`);
       throw new Error(error.detail || `HTTP ${response.status}`);
     }
     }
     const disposition = response.headers.get('Content-Disposition');
     const disposition = response.headers.get('Content-Disposition');
-    const filenameMatch = disposition?.match(/filename="?([^";\n]+)"?/);
-    const downloadFilename = filenameMatch?.[1] || filename || `file_${id}`;
+    const downloadFilename = parseContentDispositionFilename(disposition) || filename || `file_${id}`;
     const blob = await response.blob();
     const blob = await response.blob();
     const url = window.URL.createObjectURL(blob);
     const url = window.URL.createObjectURL(blob);
     const a = document.createElement('a');
     const a = document.createElement('a');
@@ -4348,8 +4354,7 @@ export const supportApi = {
     }
     }
     // Get filename from Content-Disposition header or use default
     // Get filename from Content-Disposition header or use default
     const disposition = response.headers.get('Content-Disposition');
     const disposition = response.headers.get('Content-Disposition');
-    const filenameMatch = disposition?.match(/filename=(.+)/);
-    const filename = filenameMatch ? filenameMatch[1] : 'bambuddy-support.zip';
+    const filename = parseContentDispositionFilename(disposition) || 'bambuddy-support.zip';
 
 
     // Download the blob
     // Download the blob
     const blob = await response.blob();
     const blob = await response.blob();

Разлика између датотеке није приказан због своје велике величине
+ 0 - 0
static/assets/index-Cqn1dtJ3.js


+ 1 - 1
static/index.html

@@ -23,7 +23,7 @@
 
 
     <!-- Splash screens for iOS -->
     <!-- Splash screens for iOS -->
     <link rel="apple-touch-startup-image" href="/img/android-chrome-512x512.png" />
     <link rel="apple-touch-startup-image" href="/img/android-chrome-512x512.png" />
-    <script type="module" crossorigin src="/assets/index-8p-dzNdz.js"></script>
+    <script type="module" crossorigin src="/assets/index-Cqn1dtJ3.js"></script>
     <link rel="stylesheet" crossorigin href="/assets/index-DLgJjh2G.css">
     <link rel="stylesheet" crossorigin href="/assets/index-DLgJjh2G.css">
   </head>
   </head>
   <body>
   <body>

Неке датотеке нису приказане због велике количине промена