소스 검색

Fix "Open in Slicer" failing for filenames with special characters

Filenames containing / \ ? or # (e.g. "Abzweigdose/Verteilerdose 70mm")
broke the slicer protocol handler. BambuStudio/OrcaSlicer url_decode()
the entire protocol URL before downloading, which decoded %2F back to /,
creating extra path segments that returned 404. Sanitize these characters
to _ in slicer download URLs — the filename is cosmetic (backend resolves
files by archive ID).
maziggy 6 달 전
부모
커밋
d92803b557
4개의 변경된 파일19개의 추가작업 그리고 9개의 파일을 삭제
  1. 1 0
      CHANGELOG.md
  2. 17 8
      frontend/src/api/client.ts
  3. 0 0
      static/assets/index-CIrY3KrR.js
  4. 1 1
      static/index.html

+ 1 - 0
CHANGELOG.md

@@ -9,6 +9,7 @@ All notable changes to Bambuddy will be documented in this file.
 - **Timestamps Off by Timezone Offset in Non-UTC Docker Containers** ([#504](https://github.com/maziggy/bambuddy/issues/504)) — All backend timestamps used `datetime.now()` (server local time) or the deprecated `datetime.utcnow()`. The frontend's `parseUTCDate()` assumes timestamps without timezone indicators are UTC and appends `'Z'`, so when the container's timezone wasn't UTC, every stored timestamp was off by the timezone offset. Replaced all database and comparison timestamps with `datetime.now(timezone.utc)` across 16 backend files (~80 call sites). On the frontend, replaced 13 `new Date(backendTimestamp)` calls with `parseUTCDate()` across 8 files to correctly interpret UTC timestamps. Cosmetic timestamps (filenames, user-facing local time formatting) are intentionally left as local time.
 - **"Power Off Printer" Option Not Gated by Control Permission** ([#500](https://github.com/maziggy/bambuddy/issues/500)) — The "Power off printer when done" checkbox in the print modal and the auto power off toggle in the bulk edit modal were accessible to all users regardless of permissions. Users without the `printers:control` permission can now no longer enable auto power off — the checkbox and tri-state toggle are disabled and visually dimmed.
 - **Created Admin Users Can't See Settings Button** ([#503](https://github.com/maziggy/bambuddy/issues/503)) — The sidebar hid the Settings link based on a hardcoded `role === 'user'` check instead of the actual `settings:read` permission, so newly created admin users who had the permission still couldn't see the button. Also, after login the auth state was set directly from the login response instead of re-fetching the full auth status, which could miss permission data. Now uses `hasPermission('settings:read')` for the sidebar check and calls `checkAuthStatus()` after login to load the complete user state including permissions.
+- **"Open in Slicer" Fails for Filenames Containing Special Characters** — Filenames with `/`, `\`, `?`, or `#` (e.g., `Abzweigdose/Verteilerdose 70mm`) caused the slicer protocol handler to fail. The filename is placed in the download URL path and `encodeURIComponent`-encoded, but BambuStudio and OrcaSlicer call `url_decode()` on the entire protocol handler URL before downloading. This decoded `%2F` back to `/`, creating extra path segments that resulted in a 404. The URL filename is purely cosmetic (the backend resolves files by archive ID, not filename), so now sanitizes `/`, `\`, `?`, and `#` to `_` in slicer download URLs.
 
 ## [0.2.1b3] - 2026-02-23
 

+ 17 - 8
frontend/src/api/client.ts

@@ -2808,12 +2808,17 @@ export const api = {
     document.body.removeChild(a);
     window.URL.revokeObjectURL(url);
   },
-  getSource3mfForSlicer: (archiveId: number, filename: string) =>
-    `${API_BASE}/archives/${archiveId}/source/${encodeURIComponent(filename.endsWith('.3mf') ? filename : filename + '.3mf')}`,
+  getSource3mfForSlicer: (archiveId: number, filename: string) => {
+    // Sanitize: slicers url_decode() the entire URL, so / \ ? # in filenames break path routing
+    const safe = filename.replace(/[/\\?#]/g, '_');
+    return `${API_BASE}/archives/${archiveId}/source/${encodeURIComponent(safe.endsWith('.3mf') ? safe : safe + '.3mf')}`;
+  },
   createSourceSlicerToken: (archiveId: number) =>
     request<{ token: string }>(`/archives/${archiveId}/source-slicer-token`, { method: 'POST' }),
-  getSourceSlicerDownloadUrl: (archiveId: number, token: string, filename: string) =>
-    `${API_BASE}/archives/${archiveId}/source-dl/${token}/${encodeURIComponent(filename.endsWith('.3mf') ? filename : filename + '.3mf')}`,
+  getSourceSlicerDownloadUrl: (archiveId: number, token: string, filename: string) => {
+    const safe = filename.replace(/[/\\?#]/g, '_');
+    return `${API_BASE}/archives/${archiveId}/source-dl/${token}/${encodeURIComponent(safe.endsWith('.3mf') ? safe : safe + '.3mf')}`;
+  },
   uploadSource3mf: async (archiveId: number, file: File): Promise<{ status: string; filename: string }> => {
     const formData = new FormData();
     formData.append('file', file);
@@ -2934,12 +2939,16 @@ export const api = {
     }),
   getArchiveProjectImageUrl: (archiveId: number, imagePath: string) =>
     `${API_BASE}/archives/${archiveId}/project-image/${encodeURIComponent(imagePath)}`,
-  getArchiveForSlicer: (id: number, filename: string) =>
-    `${API_BASE}/archives/${id}/file/${encodeURIComponent(filename.endsWith('.3mf') ? filename : filename + '.3mf')}`,
+  getArchiveForSlicer: (id: number, filename: string) => {
+    const safe = filename.replace(/[/\\?#]/g, '_');
+    return `${API_BASE}/archives/${id}/file/${encodeURIComponent(safe.endsWith('.3mf') ? safe : safe + '.3mf')}`;
+  },
   createArchiveSlicerToken: (archiveId: number) =>
     request<{ token: string }>(`/archives/${archiveId}/slicer-token`, { method: 'POST' }),
-  getArchiveSlicerDownloadUrl: (archiveId: number, token: string, filename: string) =>
-    `${API_BASE}/archives/${archiveId}/dl/${token}/${encodeURIComponent(filename.endsWith('.3mf') ? filename : filename + '.3mf')}`,
+  getArchiveSlicerDownloadUrl: (archiveId: number, token: string, filename: string) => {
+    const safe = filename.replace(/[/\\?#]/g, '_');
+    return `${API_BASE}/archives/${archiveId}/dl/${token}/${encodeURIComponent(safe.endsWith('.3mf') ? safe : safe + '.3mf')}`;
+  },
   getArchivePlates: (archiveId: number) =>
     request<ArchivePlatesResponse>(`/archives/${archiveId}/plates`),
   getArchiveFilamentRequirements: (archiveId: number, plateId?: number) =>

파일 크기가 너무 크기때문에 변경 상태를 표시하지 않습니다.
+ 0 - 0
static/assets/index-CIrY3KrR.js


+ 1 - 1
static/index.html

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

이 변경점에서 너무 많은 파일들이 변경되어 몇몇 파일들은 표시되지 않았습니다.