From 08f2dffd758f0e5c9af5bf6d6bedddcb4a79cecd Mon Sep 17 00:00:00 2001 From: maziggy Date: Tue, 3 Feb 2026 12:04:02 +0100 Subject: [PATCH] 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. --- frontend/src/api/client.ts | 124 ++++++++++++++++++- frontend/src/components/FileManagerModal.tsx | 6 +- frontend/src/pages/ArchivesPage.tsx | 60 ++++----- frontend/src/pages/FileManagerPage.tsx | 4 +- test_all.sh | 3 + 5 files changed, 160 insertions(+), 37 deletions(-) create mode 100755 test_all.sh diff --git a/frontend/src/api/client.ts b/frontend/src/api/client.ts index 3a557e666..aeeb2218f 100644 --- a/frontend/src/api/client.ts +++ b/frontend/src/api/client.ts @@ -2063,6 +2063,32 @@ export const api = { }>(`/printers/${printerId}/files?path=${encodeURIComponent(path)}`), getPrinterFileDownloadUrl: (printerId: number, path: string) => `${API_BASE}/printers/${printerId}/files/download?path=${encodeURIComponent(path)}`, + downloadPrinterFile: async (printerId: number, path: string): Promise => { + const headers: Record = {}; + 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 => { const headers: Record = { 'Content-Type': 'application/json' }; if (authToken) { @@ -2241,6 +2267,29 @@ export const api = { getArchivePlateThumbnail: (id: number, plateIndex: number) => `${API_BASE}/archives/${id}/plate-thumbnail/${plateIndex}`, getArchiveDownload: (id: number) => `${API_BASE}/archives/${id}/download`, + downloadArchive: async (id: number, filename?: string): Promise => { + const headers: Record = {}; + 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`, getArchivePlatePreview: (id: number) => `${API_BASE}/archives/${id}/plate-preview`, getArchiveTimelapse: (id: number) => `${API_BASE}/archives/${id}/timelapse?v=${Date.now()}`, @@ -2359,6 +2408,29 @@ export const api = { // Source 3MF (original slicer project file) getSource3mfDownloadUrl: (archiveId: number) => `${API_BASE}/archives/${archiveId}/source`, + downloadSource3mf: async (archiveId: number): Promise => { + const headers: Record = {}; + 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) => `${API_BASE}/archives/${archiveId}/source/${encodeURIComponent(filename.endsWith('.3mf') ? filename : filename + '.3mf')}`, uploadSource3mf: async (archiveId: number, file: File): Promise<{ status: string; filename: string }> => { @@ -2386,6 +2458,29 @@ export const api = { // F3D (Fusion 360 design file) getF3dDownloadUrl: (archiveId: number) => `${API_BASE}/archives/${archiveId}/f3d`, + downloadF3d: async (archiveId: number): Promise => { + const headers: Record = {}; + 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 }> => { const formData = new FormData(); formData.append('file', file); @@ -2574,7 +2669,11 @@ export const api = { exportBackup: async (): Promise<{ blob: Blob; filename: string }> => { // New simplified backup - complete database + all files const url = `${API_BASE}/settings/backup`; - const response = await fetch(url); + const headers: Record = {}; + if (authToken) { + headers['Authorization'] = `Bearer ${authToken}`; + } + const response = await fetch(url, { headers }); // Check for errors if (!response.ok) { @@ -3392,6 +3491,29 @@ export const api = { deleteLibraryFile: (id: number) => request<{ status: string; message: string }>(`/library/files/${id}`, { method: 'DELETE' }), getLibraryFileDownloadUrl: (id: number) => `${API_BASE}/library/files/${id}/download`, + downloadLibraryFile: async (id: number, filename?: string): Promise => { + const headers: Record = {}; + 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`, getLibraryFilePlateThumbnail: (id: number, plateIndex: number) => `${API_BASE}/library/files/${id}/plate-thumbnail/${plateIndex}`, diff --git a/frontend/src/components/FileManagerModal.tsx b/frontend/src/components/FileManagerModal.tsx index 0ae47026c..0b3abd30a 100644 --- a/frontend/src/components/FileManagerModal.tsx +++ b/frontend/src/components/FileManagerModal.tsx @@ -175,8 +175,10 @@ export function FileManagerModal({ printerId, printerName, onClose }: FileManage const paths = Array.from(selectedFiles); 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()); return; } diff --git a/frontend/src/pages/ArchivesPage.tsx b/frontend/src/pages/ArchivesPage.tsx index ce32d9f36..67a39f760 100644 --- a/frontend/src/pages/ArchivesPage.tsx +++ b/frontend/src/pages/ArchivesPage.tsx @@ -356,10 +356,9 @@ function ArchiveCard({ icon: , onClick: () => { 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 { source3mfInputRef.current?.click(); } @@ -393,10 +392,9 @@ function ArchiveCard({ label: t('archives.menu.downloadF3d'), icon: , 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'), icon: , 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'), title: !hasPermission('archives:read') ? t('archives.permission.noDownload') : undefined, @@ -709,7 +706,9 @@ function ArchiveCard({ onClick={(e) => { e.stopPropagation(); // 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')} > @@ -997,10 +996,9 @@ function ArchiveCard({ size="sm" className="min-w-0 p-1 sm:p-1.5" 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')} > @@ -1490,10 +1488,9 @@ function ArchiveListRow({ icon: , onClick: () => { 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 { source3mfInputRef.current?.click(); } @@ -1527,10 +1524,9 @@ function ArchiveListRow({ label: t('archives.menu.downloadF3d'), icon: , 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'), icon: , 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'), title: !hasPermission('archives:read') ? t('archives.permission.noDownload') : undefined, @@ -1785,10 +1780,9 @@ function ArchiveListRow({ variant="ghost" size="sm" 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')} > diff --git a/frontend/src/pages/FileManagerPage.tsx b/frontend/src/pages/FileManagerPage.tsx index b0eb52cfd..72f2c9e6c 100644 --- a/frontend/src/pages/FileManagerPage.tsx +++ b/frontend/src/pages/FileManagerPage.tsx @@ -1466,7 +1466,9 @@ export function FileManagerPage() { }; const handleDownload = (id: number) => { - window.open(api.getLibraryFileDownloadUrl(id), '_blank'); + api.downloadLibraryFile(id).catch((err) => { + console.error('Library file download failed:', err); + }); }; const handleDeleteConfirm = () => { diff --git a/test_all.sh b/test_all.sh new file mode 100755 index 000000000..553fae081 --- /dev/null +++ b/test_all.sh @@ -0,0 +1,3 @@ +#!/bin/bash + +./test_frontend.sh && ./test_backend.sh && ./test_docker.sh