// src/pages/api/upload/media.ts - Enhanced with detailed logging and error handling import type { APIRoute } from 'astro'; import { withAPIAuth } from '../../../utils/auth.js'; import { apiResponse, apiError, apiServerError, apiSpecial, handleAPIRequest } from '../../../utils/api.js'; import { NextcloudUploader, isNextcloudConfigured } from '../../../utils/nextcloud.js'; import { promises as fs } from 'fs'; import path from 'path'; import crypto from 'crypto'; export const prerender = false; interface UploadResult { success: boolean; url?: string; filename?: string; size?: number; error?: string; storage?: 'nextcloud' | 'local'; } const UPLOAD_CONFIG = { maxFileSize: 50 * 1024 * 1024, // 50MB allowedTypes: new Set([ // Images 'image/jpeg', 'image/jpg', 'image/png', 'image/gif', 'image/webp', 'image/svg+xml', // Videos 'video/mp4', 'video/webm', 'video/ogg', 'video/avi', 'video/mov', // Documents 'application/pdf', 'application/msword', 'application/vnd.openxmlformats-officedocument.wordprocessingml.document', 'application/vnd.ms-excel', 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet', 'application/vnd.ms-powerpoint', 'application/vnd.openxmlformats-officedocument.presentationml.presentation', // Text files 'text/plain', 'text/csv', 'text/markdown', // Added markdown 'text/x-markdown', // Alternative markdown MIME type 'application/json', 'application/xml', 'text/xml', 'text/html', // Archives 'application/zip', 'application/x-tar', 'application/gzip', 'application/x-gzip', 'application/x-zip-compressed', 'application/x-rar-compressed', 'application/x-7z-compressed', // Additional useful formats 'application/rtf', // Rich Text Format 'text/richtext', 'application/x-yaml', // YAML files 'text/yaml', 'application/yaml' ]), localUploadPath: process.env.LOCAL_UPLOAD_PATH || './public/uploads', publicBaseUrl: process.env.PUBLIC_BASE_URL || 'http://localhost:4321' }; const uploadRateLimit = new Map(); const RATE_LIMIT_WINDOW = 60 * 60 * 1000; // 1 hour const RATE_LIMIT_MAX = 10; // Max 10 uploads per hour per user function checkUploadRateLimit(userEmail: string): boolean { const now = Date.now(); const userLimit = uploadRateLimit.get(userEmail); if (!userLimit || now > userLimit.resetTime) { uploadRateLimit.set(userEmail, { count: 1, resetTime: now + RATE_LIMIT_WINDOW }); return true; } if (userLimit.count >= RATE_LIMIT_MAX) { console.warn(`[UPLOAD] Rate limit exceeded for user: ${userEmail} (${userLimit.count}/${RATE_LIMIT_MAX})`); return false; } userLimit.count++; return true; } function validateFile(file: File): { valid: boolean; error?: string } { console.log(`[UPLOAD] Validating file: ${file.name}, size: ${file.size}, type: ${file.type}`); if (file.size > UPLOAD_CONFIG.maxFileSize) { const errorMsg = `File too large. Maximum size is ${Math.round(UPLOAD_CONFIG.maxFileSize / 1024 / 1024)}MB`; console.warn(`[UPLOAD] ${errorMsg} - File size: ${file.size}`); return { valid: false, error: errorMsg }; } if (!UPLOAD_CONFIG.allowedTypes.has(file.type)) { const errorMsg = `File type ${file.type} not allowed`; console.warn(`[UPLOAD] ${errorMsg} - Allowed types:`, Array.from(UPLOAD_CONFIG.allowedTypes)); return { valid: false, error: errorMsg }; } console.log(`[UPLOAD] File validation passed for: ${file.name}`); return { valid: true }; } async function uploadToNextcloud(file: File, userEmail: string): Promise { console.log(`[UPLOAD] Attempting Nextcloud upload for: ${file.name} by ${userEmail}`); try { const uploader = new NextcloudUploader(); const result = await uploader.uploadFile(file, userEmail); console.log(`[UPLOAD] Nextcloud upload successful:`, { filename: result.filename, url: result.url, size: file.size }); return { success: true, url: result.url, filename: result.filename, size: file.size, storage: 'nextcloud' }; } catch (error) { console.error('[UPLOAD] Nextcloud upload failed:', error); return { success: false, error: error instanceof Error ? error.message : 'Nextcloud upload failed', storage: 'nextcloud' }; } } async function uploadToLocal(file: File, userType: string): Promise { console.log(`[UPLOAD] Attempting local upload for: ${file.name} (${userType})`); try { console.log(`[UPLOAD] Creating directory: ${UPLOAD_CONFIG.localUploadPath}`); await fs.mkdir(UPLOAD_CONFIG.localUploadPath, { recursive: true }); const timestamp = new Date().toISOString().replace(/[:.]/g, '-'); const randomString = crypto.randomBytes(8).toString('hex'); const extension = path.extname(file.name); const filename = `${timestamp}-${randomString}${extension}`; const filepath = path.join(UPLOAD_CONFIG.localUploadPath, filename); console.log(`[UPLOAD] Writing file to: ${filepath}`); const buffer = Buffer.from(await file.arrayBuffer()); await fs.writeFile(filepath, buffer); const publicUrl = `${UPLOAD_CONFIG.publicBaseUrl}/uploads/${filename}`; console.log(`[UPLOAD] Local upload successful:`, { filename, filepath, publicUrl, size: file.size }); return { success: true, url: publicUrl, filename: filename, size: file.size, storage: 'local' }; } catch (error) { console.error('[UPLOAD] Local upload failed:', error); return { success: false, error: error instanceof Error ? error.message : 'Local upload failed', storage: 'local' }; } } export const POST: APIRoute = async ({ request }) => { return await handleAPIRequest(async () => { console.log('[UPLOAD] Processing upload request'); // Enhanced auth logging const authResult = await withAPIAuth(request, 'contributions'); console.log('[UPLOAD] Auth result:', { authenticated: authResult.authenticated, authRequired: authResult.authRequired, userId: authResult.userId }); if (authResult.authRequired && !authResult.authenticated) { console.warn('[UPLOAD] Upload rejected - authentication required but user not authenticated'); return apiError.unauthorized('Authentication required for file uploads'); } const userEmail = authResult.session?.email || 'anon@anon.anon'; console.log(`[UPLOAD] Processing upload for user: ${userEmail}`); if (!checkUploadRateLimit(userEmail)) { return apiError.rateLimit('Upload rate limit exceeded. Please wait before uploading again.'); } let formData; try { console.log('[UPLOAD] Parsing form data'); formData = await request.formData(); console.log('[UPLOAD] Form data keys:', Array.from(formData.keys())); } catch (error) { console.error('[UPLOAD] Failed to parse form data:', error); return apiError.badRequest('Invalid form data - could not parse request'); } const file = formData.get('file') as File; const type = formData.get('type') as string; if (!file) { console.warn('[UPLOAD] No file provided in request'); return apiSpecial.missingRequired(['file']); } console.log(`[UPLOAD] Processing file: ${file.name}, type parameter: ${type}`); const validation = validateFile(file); if (!validation.valid) { return apiError.badRequest(validation.error!); } // Enhanced environment logging const nextcloudConfigured = isNextcloudConfigured(); console.log('[UPLOAD] Environment check:', { nextcloudConfigured, localUploadPath: UPLOAD_CONFIG.localUploadPath, publicBaseUrl: UPLOAD_CONFIG.publicBaseUrl, nodeEnv: process.env.NODE_ENV }); let result: UploadResult; if (nextcloudConfigured) { console.log('[UPLOAD] Using Nextcloud as primary storage'); result = await uploadToNextcloud(file, userEmail); if (!result.success) { console.warn('[UPLOAD] Nextcloud upload failed, trying local fallback:', result.error); result = await uploadToLocal(file, type); } } else { console.log('[UPLOAD] Using local storage (Nextcloud not configured)'); result = await uploadToLocal(file, type); } if (result.success) { console.log(`[UPLOAD] Upload completed successfully:`, { filename: result.filename, storage: result.storage, url: result.url, user: userEmail }); return apiSpecial.uploadSuccess({ url: result.url!, filename: result.filename!, size: result.size!, storage: result.storage! }); } else { console.error(`[UPLOAD] Upload failed completely:`, { filename: file.name, error: result.error, storage: result.storage, user: userEmail }); return apiSpecial.uploadFailed(result.error!); } }, 'Media upload processing failed'); }; export const GET: APIRoute = async ({ request }) => { return await handleAPIRequest(async () => { console.log('[UPLOAD] Getting upload status'); const authResult = await withAPIAuth(request); if (authResult.authRequired && !authResult.authenticated) { return apiError.unauthorized(); } const nextcloudConfigured = isNextcloudConfigured(); let localStorageAvailable = false; try { await fs.access(UPLOAD_CONFIG.localUploadPath); localStorageAvailable = true; console.log('[UPLOAD] Local storage accessible'); } catch { try { await fs.mkdir(UPLOAD_CONFIG.localUploadPath, { recursive: true }); localStorageAvailable = true; console.log('[UPLOAD] Local storage created'); } catch (error) { console.warn('[UPLOAD] Local upload directory not accessible:', error); } } const status = { storage: { nextcloud: { configured: nextcloudConfigured, primary: nextcloudConfigured }, local: { available: localStorageAvailable, fallback: nextcloudConfigured, primary: !nextcloudConfigured } }, limits: { maxFileSize: UPLOAD_CONFIG.maxFileSize, maxFileSizeMB: Math.round(UPLOAD_CONFIG.maxFileSize / 1024 / 1024), allowedTypes: Array.from(UPLOAD_CONFIG.allowedTypes), rateLimit: { maxPerHour: RATE_LIMIT_MAX, windowMs: RATE_LIMIT_WINDOW } }, paths: { uploadEndpoint: '/api/upload/media', localPath: localStorageAvailable ? '/uploads' : null }, environment: { nodeEnv: process.env.NODE_ENV, publicBaseUrl: UPLOAD_CONFIG.publicBaseUrl } }; console.log('[UPLOAD] Status check completed:', status); return apiResponse.success(status); }, 'Upload status retrieval failed'); };