Fix file downloads for Safari with proper filename handling

Implement browser-specific download methods to ensure files download with correct filenames across all browsers. Safari now uses window.location.href while other browsers use blob URLs. Add Content-Disposition header to proxy endpoint for proper filename preservation.
This commit is contained in:
Matt 2025-06-04 19:13:09 +02:00
parent b7544d82f3
commit 4d3935e863
3 changed files with 74 additions and 34 deletions

View File

@ -199,33 +199,51 @@ const handlePreviewError = (event: any) => {
loading.value = false; loading.value = false;
}; };
// Download file // Download file (with special handling for Safari)
const downloadFile = async () => { const downloadFile = async () => {
if (!props.file) return; if (!props.file) return;
try { try {
// Use proxy download endpoint for better mobile compatibility
const proxyUrl = `/api/files/proxy-download?fileName=${encodeURIComponent(props.file.name)}`;
// Create a link element
const link = document.createElement('a');
link.href = proxyUrl;
// Extract clean filename for download // Extract clean filename for download
let filename = props.file.displayName; let filename = props.file.displayName;
if (!filename.includes('.') && props.file.extension) { if (!filename.includes('.') && props.file.extension) {
filename += '.' + props.file.extension; filename += '.' + props.file.extension;
} }
link.download = filename; // Check if Safari (iOS or desktop)
link.style.display = 'none'; const isSafari = /^((?!chrome|android).)*safari/i.test(navigator.userAgent);
document.body.appendChild(link);
link.click();
// Clean up if (isSafari) {
setTimeout(() => { // For Safari, use location.href to force proper filename handling
document.body.removeChild(link); const downloadUrl = `/api/files/proxy-download?fileName=${encodeURIComponent(props.file.name)}`;
}, 100); window.location.href = downloadUrl;
} else {
// For other browsers, use blob approach
const response = await fetch(`/api/files/proxy-download?fileName=${encodeURIComponent(props.file.name)}`);
if (!response.ok) {
throw new Error('Failed to download file');
}
const blob = await response.blob();
// Create object URL from blob
const downloadUrl = URL.createObjectURL(blob);
// Create a link element
const link = document.createElement('a');
link.href = downloadUrl;
link.download = filename;
link.style.display = 'none';
document.body.appendChild(link);
link.click();
// Clean up
setTimeout(() => {
document.body.removeChild(link);
URL.revokeObjectURL(downloadUrl);
}, 100);
}
} catch (err) { } catch (err) {
console.error('Failed to download file:', err); console.error('Failed to download file:', err);
} }

View File

@ -499,37 +499,58 @@ const createNewFolder = async () => {
} }
}; };
// Download file (using proxy for Safari compatibility) // Download file (with special handling for Safari)
const downloadFile = async (file: FileItem) => { const downloadFile = async (file: FileItem) => {
downloadingFiles.value[file.name] = true; downloadingFiles.value[file.name] = true;
try { try {
// Use proxy download endpoint for better mobile compatibility
const proxyUrl = `/api/files/proxy-download?fileName=${encodeURIComponent(file.name)}`;
// Create a link element
const link = document.createElement('a');
link.href = proxyUrl;
// Extract clean filename for download // Extract clean filename for download
let filename = file.displayName; let filename = file.displayName;
if (!filename.includes('.') && file.extension) { if (!filename.includes('.') && file.extension) {
filename += '.' + file.extension; filename += '.' + file.extension;
} }
link.download = filename; // Check if Safari (iOS or desktop)
link.style.display = 'none'; const isSafari = /^((?!chrome|android).)*safari/i.test(navigator.userAgent);
document.body.appendChild(link);
link.click();
// Clean up if (isSafari) {
setTimeout(() => { // For Safari, open in new window to force proper filename handling
document.body.removeChild(link); const downloadUrl = `/api/files/proxy-download?fileName=${encodeURIComponent(file.name)}`;
}, 100); window.location.href = downloadUrl;
} else {
// For other browsers, use blob approach
const response = await fetch(`/api/files/proxy-download?fileName=${encodeURIComponent(file.name)}`);
if (!response.ok) {
throw new Error('Failed to download file');
}
const blob = await response.blob();
// Create object URL from blob
const objectUrl = URL.createObjectURL(blob);
// Create a link element
const link = document.createElement('a');
link.href = objectUrl;
link.download = filename;
link.style.display = 'none';
document.body.appendChild(link);
link.click();
// Clean up
setTimeout(() => {
document.body.removeChild(link);
URL.revokeObjectURL(objectUrl);
}, 100);
}
} catch (error) { } catch (error) {
toast.error('Failed to download file'); toast.error('Failed to download file');
} finally { } finally {
downloadingFiles.value[file.name] = false; // Add delay for Safari to prevent immediate loading state removal
setTimeout(() => {
downloadingFiles.value[file.name] = false;
}, 1000);
} }
}; };

View File

@ -41,7 +41,8 @@ export default defineEventHandler(async (event) => {
// Set headers for download // Set headers for download
setHeader(event, 'Content-Type', contentType); setHeader(event, 'Content-Type', contentType);
setHeader(event, 'Content-Disposition', `attachment; filename="${cleanFileName}"`); // Use both filename and filename* for better compatibility
setHeader(event, 'Content-Disposition', `attachment; filename="${cleanFileName}"; filename*=UTF-8''${encodeURIComponent(cleanFileName)}`);
// Content-Length header is set automatically by Nitro when returning a buffer // Content-Length header is set automatically by Nitro when returning a buffer
// Return the file buffer // Return the file buffer