import { getMinioClient } from '~/server/utils/minio'; export default defineEventHandler(async (event) => { try { const query = getQuery(event); const fileName = query.fileName as string; const bucket = (query.bucket as string) || 'client-portal'; // Support bucket parameter if (!fileName) { throw createError({ statusCode: 400, statusMessage: 'File name is required', }); } // Retry logic for getting download URL and fetching file let response: Response | null = null; let lastError: any = null; for (let attempt = 0; attempt < 3; attempt++) { try { console.log(`[proxy-download] Attempting to download ${fileName} from bucket ${bucket} (attempt ${attempt + 1}/3)`); // Get the download URL from MinIO with the correct bucket const client = getMinioClient(); // Extract just the filename for the download header let filename = fileName.split('/').pop() || fileName; // Remove timestamp prefix if present const timestampMatch = filename.match(/^\d{10,}-(.+)$/); if (timestampMatch) { filename = timestampMatch[1]; } // Generate presigned URL with download headers const responseHeaders = { 'response-content-disposition': `attachment; filename="${filename}"`, }; const url = await client.presignedGetObject(bucket, fileName, 60 * 60, responseHeaders); // Fetch the file from MinIO with timeout const controller = new AbortController(); const timeout = setTimeout(() => controller.abort(), 30000); // 30 second timeout response = await fetch(url, { signal: controller.signal }); clearTimeout(timeout); if (response.ok) { break; // Success, exit retry loop } lastError = new Error(`HTTP ${response.status}: ${response.statusText}`); } catch (error: any) { lastError = error; console.error(`[proxy-download] Attempt ${attempt + 1} failed:`, error.message); // Wait before retry with exponential backoff if (attempt < 2) { await new Promise(resolve => setTimeout(resolve, (attempt + 1) * 1000)); } } } if (!response || !response.ok) { throw createError({ statusCode: response?.status || 500, statusMessage: lastError?.message || 'Failed to fetch file from storage after 3 attempts', }); } // Get the file data const arrayBuffer = await response.arrayBuffer(); const buffer = Buffer.from(arrayBuffer); // Extract clean filename (remove timestamp prefix) let cleanFileName = fileName.split('/').pop() || fileName; const timestampMatch = cleanFileName.match(/^\d{10,}-(.+)$/); if (timestampMatch) { cleanFileName = timestampMatch[1]; } // Get content type const contentType = response.headers.get('content-type') || 'application/octet-stream'; // Set headers for download setHeader(event, 'Content-Type', contentType); // 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 // Return the file buffer return buffer; } catch (error: any) { console.error('Failed to proxy download:', error); throw createError({ statusCode: 500, statusMessage: error.message || 'Failed to download file', }); } });