Browse Source

Merge pull request #1416 from benhalverson/fix/open-in-slicer

Fix library Open in Slicer URLs for extensionless filenames
Ben Halverson 1 week ago
parent
commit
feb44a9dc1
2 changed files with 22 additions and 1 deletions
  1. 16 0
      frontend/src/__tests__/api/client.test.ts
  2. 6 1
      frontend/src/api/client.ts

+ 16 - 0
frontend/src/__tests__/api/client.test.ts

@@ -198,6 +198,22 @@ describe('API Client Auth Header', () => {
   });
   });
 });
 });
 
 
+describe('Slicer download URLs', () => {
+  it('keeps library slicer URLs ending in .3mf when the display name has no extension', () => {
+    const path = api.getLibrarySlicerDownloadUrl(12, 'token-abc', 'Mecha Mewtwo No AMS Multi Color Parted Statue');
+
+    expect(path).toBe(
+      '/api/v1/library/files/12/dl/token-abc/Mecha%20Mewtwo%20No%20AMS%20Multi%20Color%20Parted%20Statue.3mf'
+    );
+  });
+
+  it('sanitizes library slicer URL filenames before encoding them', () => {
+    const path = api.getLibrarySlicerDownloadUrl(12, 'token-abc', 'folder/model?bad#name.3mf');
+
+    expect(path).toBe('/api/v1/library/files/12/dl/token-abc/folder_model_bad_name.3mf');
+  });
+});
+
 describe('FormData requests include auth header', () => {
 describe('FormData requests include auth header', () => {
   it('importProjectFile includes Authorization header', async () => {
   it('importProjectFile includes Authorization header', async () => {
     // Mock fetch directly for FormData requests (MSW can be flaky with multipart in some environments)
     // Mock fetch directly for FormData requests (MSW can be flaky with multipart in some environments)

+ 6 - 1
frontend/src/api/client.ts

@@ -85,6 +85,11 @@ function parseContentDispositionFilename(header: string | null): string | null {
   return standardMatch?.[1] || null;
   return standardMatch?.[1] || null;
 }
 }
 
 
+function buildSlicerUrlFilename(filename: string): string {
+  const safe = filename.replace(/[/\\?#]/g, '_');
+  return safe.toLowerCase().endsWith('.3mf') ? safe : `${safe}.3mf`;
+}
+
 async function request<T>(
 async function request<T>(
   endpoint: string,
   endpoint: string,
   options: RequestInit = {}
   options: RequestInit = {}
@@ -5437,7 +5442,7 @@ export const api = {
   createLibrarySlicerToken: (fileId: number) =>
   createLibrarySlicerToken: (fileId: number) =>
     request<{ token: string }>(`/library/files/${fileId}/slicer-token`, { method: 'POST' }),
     request<{ token: string }>(`/library/files/${fileId}/slicer-token`, { method: 'POST' }),
   getLibrarySlicerDownloadUrl: (fileId: number, token: string, filename: string) =>
   getLibrarySlicerDownloadUrl: (fileId: number, token: string, filename: string) =>
-    `${API_BASE}/library/files/${fileId}/dl/${token}/${encodeURIComponent(filename)}`,
+    `${API_BASE}/library/files/${fileId}/dl/${token}/${encodeURIComponent(buildSlicerUrlFilename(filename))}`,
   downloadLibraryFile: async (id: number, filename?: string): Promise<void> => {
   downloadLibraryFile: async (id: number, filename?: string): Promise<void> => {
     const headers: Record<string, string> = {};
     const headers: Record<string, string> = {};
     if (authToken) {
     if (authToken) {