Browse Source

Added new download functions to client.ts:
- downloadArchive(id, filename) - for 3MF archive downloads
- downloadSource3mf(archiveId) - for source 3MF downloads
- downloadF3d(archiveId) - for Fusion 360 file downloads
- downloadLibraryFile(id, filename) - for library file downloads
- downloadPrinterFile(printerId, path) - for printer file downloads
- exportBackup() - fixed (already existed, just added auth header)

Updated components to use auth-aware downloads:
- ArchivesPage.tsx - 8 places fixed (F3D, Source 3MF, Archive downloads)
- FileManagerPage.tsx - Library file download fixed
- FileManagerModal.tsx - Printer file download fixed

All fetch-based downloads now include the Authorization: Bearer ${token} header when auth is enabled.

maziggy 3 months ago
parent
commit
08f2dffd75

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

@@ -2063,6 +2063,32 @@ export const api = {
     }>(`/printers/${printerId}/files?path=${encodeURIComponent(path)}`),
     }>(`/printers/${printerId}/files?path=${encodeURIComponent(path)}`),
   getPrinterFileDownloadUrl: (printerId: number, path: string) =>
   getPrinterFileDownloadUrl: (printerId: number, path: string) =>
     `${API_BASE}/printers/${printerId}/files/download?path=${encodeURIComponent(path)}`,
     `${API_BASE}/printers/${printerId}/files/download?path=${encodeURIComponent(path)}`,
+  downloadPrinterFile: async (printerId: number, path: string): Promise<void> => {
+    const headers: Record<string, string> = {};
+    if (authToken) {
+      headers['Authorization'] = `Bearer ${authToken}`;
+    }
+    const response = await fetch(
+      `${API_BASE}/printers/${printerId}/files/download?path=${encodeURIComponent(path)}`,
+      { headers }
+    );
+    if (!response.ok) {
+      const error = await response.json().catch(() => ({}));
+      throw new Error(error.detail || `HTTP ${response.status}`);
+    }
+    const disposition = response.headers.get('Content-Disposition');
+    const filenameMatch = disposition?.match(/filename="?([^";\n]+)"?/);
+    const filename = filenameMatch?.[1] || path.split('/').pop() || 'download';
+    const blob = await response.blob();
+    const url = window.URL.createObjectURL(blob);
+    const a = document.createElement('a');
+    a.href = url;
+    a.download = filename;
+    document.body.appendChild(a);
+    a.click();
+    document.body.removeChild(a);
+    window.URL.revokeObjectURL(url);
+  },
   downloadPrinterFilesAsZip: async (printerId: number, paths: string[]): Promise<Blob> => {
   downloadPrinterFilesAsZip: async (printerId: number, paths: string[]): Promise<Blob> => {
     const headers: Record<string, string> = { 'Content-Type': 'application/json' };
     const headers: Record<string, string> = { 'Content-Type': 'application/json' };
     if (authToken) {
     if (authToken) {
@@ -2241,6 +2267,29 @@ export const api = {
   getArchivePlateThumbnail: (id: number, plateIndex: number) =>
   getArchivePlateThumbnail: (id: number, plateIndex: number) =>
     `${API_BASE}/archives/${id}/plate-thumbnail/${plateIndex}`,
     `${API_BASE}/archives/${id}/plate-thumbnail/${plateIndex}`,
   getArchiveDownload: (id: number) => `${API_BASE}/archives/${id}/download`,
   getArchiveDownload: (id: number) => `${API_BASE}/archives/${id}/download`,
+  downloadArchive: async (id: number, filename?: string): Promise<void> => {
+    const headers: Record<string, string> = {};
+    if (authToken) {
+      headers['Authorization'] = `Bearer ${authToken}`;
+    }
+    const response = await fetch(`${API_BASE}/archives/${id}/download`, { headers });
+    if (!response.ok) {
+      const error = await response.json().catch(() => ({}));
+      throw new Error(error.detail || `HTTP ${response.status}`);
+    }
+    const disposition = response.headers.get('Content-Disposition');
+    const filenameMatch = disposition?.match(/filename="?([^";\n]+)"?/);
+    const downloadFilename = filenameMatch?.[1] || filename || `archive_${id}.3mf`;
+    const blob = await response.blob();
+    const url = window.URL.createObjectURL(blob);
+    const a = document.createElement('a');
+    a.href = url;
+    a.download = downloadFilename;
+    document.body.appendChild(a);
+    a.click();
+    document.body.removeChild(a);
+    window.URL.revokeObjectURL(url);
+  },
   getArchiveGcode: (id: number) => `${API_BASE}/archives/${id}/gcode`,
   getArchiveGcode: (id: number) => `${API_BASE}/archives/${id}/gcode`,
   getArchivePlatePreview: (id: number) => `${API_BASE}/archives/${id}/plate-preview`,
   getArchivePlatePreview: (id: number) => `${API_BASE}/archives/${id}/plate-preview`,
   getArchiveTimelapse: (id: number) => `${API_BASE}/archives/${id}/timelapse?v=${Date.now()}`,
   getArchiveTimelapse: (id: number) => `${API_BASE}/archives/${id}/timelapse?v=${Date.now()}`,
@@ -2359,6 +2408,29 @@ export const api = {
   // Source 3MF (original slicer project file)
   // Source 3MF (original slicer project file)
   getSource3mfDownloadUrl: (archiveId: number) =>
   getSource3mfDownloadUrl: (archiveId: number) =>
     `${API_BASE}/archives/${archiveId}/source`,
     `${API_BASE}/archives/${archiveId}/source`,
+  downloadSource3mf: async (archiveId: number): Promise<void> => {
+    const headers: Record<string, string> = {};
+    if (authToken) {
+      headers['Authorization'] = `Bearer ${authToken}`;
+    }
+    const response = await fetch(`${API_BASE}/archives/${archiveId}/source`, { headers });
+    if (!response.ok) {
+      const error = await response.json().catch(() => ({}));
+      throw new Error(error.detail || `HTTP ${response.status}`);
+    }
+    const disposition = response.headers.get('Content-Disposition');
+    const filenameMatch = disposition?.match(/filename="?([^";\n]+)"?/);
+    const filename = filenameMatch?.[1] || `source_${archiveId}.3mf`;
+    const blob = await response.blob();
+    const url = window.URL.createObjectURL(blob);
+    const a = document.createElement('a');
+    a.href = url;
+    a.download = filename;
+    document.body.appendChild(a);
+    a.click();
+    document.body.removeChild(a);
+    window.URL.revokeObjectURL(url);
+  },
   getSource3mfForSlicer: (archiveId: number, filename: string) =>
   getSource3mfForSlicer: (archiveId: number, filename: string) =>
     `${API_BASE}/archives/${archiveId}/source/${encodeURIComponent(filename.endsWith('.3mf') ? filename : filename + '.3mf')}`,
     `${API_BASE}/archives/${archiveId}/source/${encodeURIComponent(filename.endsWith('.3mf') ? filename : filename + '.3mf')}`,
   uploadSource3mf: async (archiveId: number, file: File): Promise<{ status: string; filename: string }> => {
   uploadSource3mf: async (archiveId: number, file: File): Promise<{ status: string; filename: string }> => {
@@ -2386,6 +2458,29 @@ export const api = {
   // F3D (Fusion 360 design file)
   // F3D (Fusion 360 design file)
   getF3dDownloadUrl: (archiveId: number) =>
   getF3dDownloadUrl: (archiveId: number) =>
     `${API_BASE}/archives/${archiveId}/f3d`,
     `${API_BASE}/archives/${archiveId}/f3d`,
+  downloadF3d: async (archiveId: number): Promise<void> => {
+    const headers: Record<string, string> = {};
+    if (authToken) {
+      headers['Authorization'] = `Bearer ${authToken}`;
+    }
+    const response = await fetch(`${API_BASE}/archives/${archiveId}/f3d`, { headers });
+    if (!response.ok) {
+      const error = await response.json().catch(() => ({}));
+      throw new Error(error.detail || `HTTP ${response.status}`);
+    }
+    const disposition = response.headers.get('Content-Disposition');
+    const filenameMatch = disposition?.match(/filename="?([^";\n]+)"?/);
+    const filename = filenameMatch?.[1] || `archive_${archiveId}.f3d`;
+    const blob = await response.blob();
+    const url = window.URL.createObjectURL(blob);
+    const a = document.createElement('a');
+    a.href = url;
+    a.download = filename;
+    document.body.appendChild(a);
+    a.click();
+    document.body.removeChild(a);
+    window.URL.revokeObjectURL(url);
+  },
   uploadF3d: async (archiveId: number, file: File): Promise<{ status: string; filename: string }> => {
   uploadF3d: async (archiveId: number, file: File): Promise<{ status: string; filename: string }> => {
     const formData = new FormData();
     const formData = new FormData();
     formData.append('file', file);
     formData.append('file', file);
@@ -2574,7 +2669,11 @@ export const api = {
   exportBackup: async (): Promise<{ blob: Blob; filename: string }> => {
   exportBackup: async (): Promise<{ blob: Blob; filename: string }> => {
     // New simplified backup - complete database + all files
     // New simplified backup - complete database + all files
     const url = `${API_BASE}/settings/backup`;
     const url = `${API_BASE}/settings/backup`;
-    const response = await fetch(url);
+    const headers: Record<string, string> = {};
+    if (authToken) {
+      headers['Authorization'] = `Bearer ${authToken}`;
+    }
+    const response = await fetch(url, { headers });
 
 
     // Check for errors
     // Check for errors
     if (!response.ok) {
     if (!response.ok) {
@@ -3392,6 +3491,29 @@ export const api = {
   deleteLibraryFile: (id: number) =>
   deleteLibraryFile: (id: number) =>
     request<{ status: string; message: string }>(`/library/files/${id}`, { method: 'DELETE' }),
     request<{ status: string; message: string }>(`/library/files/${id}`, { method: 'DELETE' }),
   getLibraryFileDownloadUrl: (id: number) => `${API_BASE}/library/files/${id}/download`,
   getLibraryFileDownloadUrl: (id: number) => `${API_BASE}/library/files/${id}/download`,
+  downloadLibraryFile: async (id: number, filename?: string): Promise<void> => {
+    const headers: Record<string, string> = {};
+    if (authToken) {
+      headers['Authorization'] = `Bearer ${authToken}`;
+    }
+    const response = await fetch(`${API_BASE}/library/files/${id}/download`, { headers });
+    if (!response.ok) {
+      const error = await response.json().catch(() => ({}));
+      throw new Error(error.detail || `HTTP ${response.status}`);
+    }
+    const disposition = response.headers.get('Content-Disposition');
+    const filenameMatch = disposition?.match(/filename="?([^";\n]+)"?/);
+    const downloadFilename = filenameMatch?.[1] || filename || `file_${id}`;
+    const blob = await response.blob();
+    const url = window.URL.createObjectURL(blob);
+    const a = document.createElement('a');
+    a.href = url;
+    a.download = downloadFilename;
+    document.body.appendChild(a);
+    a.click();
+    document.body.removeChild(a);
+    window.URL.revokeObjectURL(url);
+  },
   getLibraryFileThumbnailUrl: (id: number) => `${API_BASE}/library/files/${id}/thumbnail`,
   getLibraryFileThumbnailUrl: (id: number) => `${API_BASE}/library/files/${id}/thumbnail`,
   getLibraryFilePlateThumbnail: (id: number, plateIndex: number) =>
   getLibraryFilePlateThumbnail: (id: number, plateIndex: number) =>
     `${API_BASE}/library/files/${id}/plate-thumbnail/${plateIndex}`,
     `${API_BASE}/library/files/${id}/plate-thumbnail/${plateIndex}`,

+ 4 - 2
frontend/src/components/FileManagerModal.tsx

@@ -175,8 +175,10 @@ export function FileManagerModal({ printerId, printerName, onClose }: FileManage
     const paths = Array.from(selectedFiles);
     const paths = Array.from(selectedFiles);
 
 
     if (paths.length === 1) {
     if (paths.length === 1) {
-      // Single file - direct download
-      window.open(api.getPrinterFileDownloadUrl(printerId, paths[0]), '_blank');
+      // Single file - direct download with auth
+      api.downloadPrinterFile(printerId, paths[0]).catch((err) => {
+        console.error('Printer file download failed:', err);
+      });
       setSelectedFiles(new Set());
       setSelectedFiles(new Set());
       return;
       return;
     }
     }

+ 27 - 33
frontend/src/pages/ArchivesPage.tsx

@@ -356,10 +356,9 @@ function ArchiveCard({
       icon: <FileCode className="w-4 h-4" />,
       icon: <FileCode className="w-4 h-4" />,
       onClick: () => {
       onClick: () => {
         if (archive.source_3mf_path) {
         if (archive.source_3mf_path) {
-          const link = document.createElement('a');
-          link.href = api.getSource3mfDownloadUrl(archive.id);
-          link.download = `${archive.print_name || archive.filename}_source.3mf`;
-          link.click();
+          api.downloadSource3mf(archive.id).catch((err) => {
+            console.error('Source 3MF download failed:', err);
+          });
         } else {
         } else {
           source3mfInputRef.current?.click();
           source3mfInputRef.current?.click();
         }
         }
@@ -393,10 +392,9 @@ function ArchiveCard({
       label: t('archives.menu.downloadF3d'),
       label: t('archives.menu.downloadF3d'),
       icon: <Download className="w-4 h-4" />,
       icon: <Download className="w-4 h-4" />,
       onClick: () => {
       onClick: () => {
-        const link = document.createElement('a');
-        link.href = api.getF3dDownloadUrl(archive.id);
-        link.download = `${archive.print_name || archive.filename}.f3d`;
-        link.click();
+        api.downloadF3d(archive.id).catch((err) => {
+          console.error('F3D download failed:', err);
+        });
       },
       },
     },
     },
     {
     {
@@ -412,10 +410,9 @@ function ArchiveCard({
       label: t('archives.menu.download'),
       label: t('archives.menu.download'),
       icon: <Download className="w-4 h-4" />,
       icon: <Download className="w-4 h-4" />,
       onClick: () => {
       onClick: () => {
-        const link = document.createElement('a');
-        link.href = api.getArchiveDownload(archive.id);
-        link.download = `${archive.print_name || archive.filename}.3mf`;
-        link.click();
+        api.downloadArchive(archive.id, `${archive.print_name || archive.filename}.3mf`).catch((err) => {
+          console.error('Archive download failed:', err);
+        });
       },
       },
       disabled: !hasPermission('archives:read'),
       disabled: !hasPermission('archives:read'),
       title: !hasPermission('archives:read') ? t('archives.permission.noDownload') : undefined,
       title: !hasPermission('archives:read') ? t('archives.permission.noDownload') : undefined,
@@ -709,7 +706,9 @@ function ArchiveCard({
             onClick={(e) => {
             onClick={(e) => {
               e.stopPropagation();
               e.stopPropagation();
               // Download F3D file
               // Download F3D file
-              window.location.href = api.getF3dDownloadUrl(archive.id);
+              api.downloadF3d(archive.id).catch((err) => {
+                console.error('F3D download failed:', err);
+              });
             }}
             }}
             title={t('archives.card.downloadF3d')}
             title={t('archives.card.downloadF3d')}
           >
           >
@@ -997,10 +996,9 @@ function ArchiveCard({
             size="sm"
             size="sm"
             className="min-w-0 p-1 sm:p-1.5"
             className="min-w-0 p-1 sm:p-1.5"
             onClick={() => {
             onClick={() => {
-              const link = document.createElement('a');
-              link.href = api.getArchiveDownload(archive.id);
-              link.download = `${archive.print_name || archive.filename}.3mf`;
-              link.click();
+              api.downloadArchive(archive.id, `${archive.print_name || archive.filename}.3mf`).catch((err) => {
+                console.error('Archive download failed:', err);
+              });
             }}
             }}
             title={t('archives.card.download')}
             title={t('archives.card.download')}
           >
           >
@@ -1490,10 +1488,9 @@ function ArchiveListRow({
       icon: <FileCode className="w-4 h-4" />,
       icon: <FileCode className="w-4 h-4" />,
       onClick: () => {
       onClick: () => {
         if (archive.source_3mf_path) {
         if (archive.source_3mf_path) {
-          const link = document.createElement('a');
-          link.href = api.getSource3mfDownloadUrl(archive.id);
-          link.download = `${archive.print_name || archive.filename}_source.3mf`;
-          link.click();
+          api.downloadSource3mf(archive.id).catch((err) => {
+            console.error('Source 3MF download failed:', err);
+          });
         } else {
         } else {
           source3mfInputRef.current?.click();
           source3mfInputRef.current?.click();
         }
         }
@@ -1527,10 +1524,9 @@ function ArchiveListRow({
       label: t('archives.menu.downloadF3d'),
       label: t('archives.menu.downloadF3d'),
       icon: <Download className="w-4 h-4" />,
       icon: <Download className="w-4 h-4" />,
       onClick: () => {
       onClick: () => {
-        const link = document.createElement('a');
-        link.href = api.getF3dDownloadUrl(archive.id);
-        link.download = `${archive.print_name || archive.filename}.f3d`;
-        link.click();
+        api.downloadF3d(archive.id).catch((err) => {
+          console.error('F3D download failed:', err);
+        });
       },
       },
     },
     },
     {
     {
@@ -1546,10 +1542,9 @@ function ArchiveListRow({
       label: t('archives.menu.download'),
       label: t('archives.menu.download'),
       icon: <Download className="w-4 h-4" />,
       icon: <Download className="w-4 h-4" />,
       onClick: () => {
       onClick: () => {
-        const link = document.createElement('a');
-        link.href = api.getArchiveDownload(archive.id);
-        link.download = `${archive.print_name || archive.filename}.3mf`;
-        link.click();
+        api.downloadArchive(archive.id, `${archive.print_name || archive.filename}.3mf`).catch((err) => {
+          console.error('Archive download failed:', err);
+        });
       },
       },
       disabled: !hasPermission('archives:read'),
       disabled: !hasPermission('archives:read'),
       title: !hasPermission('archives:read') ? t('archives.permission.noDownload') : undefined,
       title: !hasPermission('archives:read') ? t('archives.permission.noDownload') : undefined,
@@ -1785,10 +1780,9 @@ function ArchiveListRow({
             variant="ghost"
             variant="ghost"
             size="sm"
             size="sm"
             onClick={() => {
             onClick={() => {
-              const link = document.createElement('a');
-              link.href = api.getArchiveDownload(archive.id);
-              link.download = `${archive.print_name || archive.filename}.3mf`;
-              link.click();
+              api.downloadArchive(archive.id, `${archive.print_name || archive.filename}.3mf`).catch((err) => {
+                console.error('Archive download failed:', err);
+              });
             }}
             }}
             title={t('archives.card.download')}
             title={t('archives.card.download')}
           >
           >

+ 3 - 1
frontend/src/pages/FileManagerPage.tsx

@@ -1466,7 +1466,9 @@ export function FileManagerPage() {
   };
   };
 
 
   const handleDownload = (id: number) => {
   const handleDownload = (id: number) => {
-    window.open(api.getLibraryFileDownloadUrl(id), '_blank');
+    api.downloadLibraryFile(id).catch((err) => {
+      console.error('Library file download failed:', err);
+    });
   };
   };
 
 
   const handleDeleteConfirm = () => {
   const handleDeleteConfirm = () => {

+ 3 - 0
test_all.sh

@@ -0,0 +1,3 @@
+#!/bin/bash
+
+./test_frontend.sh && ./test_backend.sh && ./test_docker.sh