2025-06-15 16:34:27 +02:00
|
|
|
import { requireAuth } from '~/server/utils/auth';
|
2025-06-10 15:21:42 +02:00
|
|
|
import { getMinioClient } from '~/server/utils/minio';
|
2025-06-04 18:37:41 +02:00
|
|
|
|
|
|
|
|
export default defineEventHandler(async (event) => {
|
2025-06-15 16:34:27 +02:00
|
|
|
// Check authentication (x-tag header OR Keycloak session)
|
|
|
|
|
await requireAuth(event);
|
|
|
|
|
|
2025-06-04 18:37:41 +02:00
|
|
|
try {
|
|
|
|
|
const query = getQuery(event);
|
|
|
|
|
const fileName = query.fileName as string;
|
2025-06-10 15:21:42 +02:00
|
|
|
const bucket = (query.bucket as string) || 'client-portal'; // Support bucket parameter
|
2025-06-04 18:37:41 +02:00
|
|
|
|
|
|
|
|
if (!fileName) {
|
|
|
|
|
throw createError({
|
|
|
|
|
statusCode: 400,
|
|
|
|
|
statusMessage: 'File name is required',
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
|
2025-06-10 13:59:09 +02:00
|
|
|
// Retry logic for getting download URL and fetching file
|
|
|
|
|
let response: Response | null = null;
|
|
|
|
|
let lastError: any = null;
|
2025-06-04 18:37:41 +02:00
|
|
|
|
2025-06-10 13:59:09 +02:00
|
|
|
for (let attempt = 0; attempt < 3; attempt++) {
|
|
|
|
|
try {
|
2025-06-10 15:21:42 +02:00
|
|
|
console.log(`[proxy-download] Attempting to download ${fileName} from bucket ${bucket} (attempt ${attempt + 1}/3)`);
|
2025-06-10 13:59:09 +02:00
|
|
|
|
2025-06-10 15:21:42 +02:00
|
|
|
// 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);
|
2025-06-10 13:59:09 +02:00
|
|
|
|
|
|
|
|
// 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));
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
2025-06-04 18:37:41 +02:00
|
|
|
|
2025-06-10 13:59:09 +02:00
|
|
|
if (!response || !response.ok) {
|
2025-06-04 18:37:41 +02:00
|
|
|
throw createError({
|
2025-06-10 13:59:09 +02:00
|
|
|
statusCode: response?.status || 500,
|
|
|
|
|
statusMessage: lastError?.message || 'Failed to fetch file from storage after 3 attempts',
|
2025-06-04 18:37:41 +02:00
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// 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);
|
2025-06-04 19:13:09 +02:00
|
|
|
// Use both filename and filename* for better compatibility
|
|
|
|
|
setHeader(event, 'Content-Disposition', `attachment; filename="${cleanFileName}"; filename*=UTF-8''${encodeURIComponent(cleanFileName)}`);
|
2025-06-04 18:37:41 +02:00
|
|
|
// 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',
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
});
|