more error handling and logging in uploads mechanic
This commit is contained in:
		
							parent
							
								
									8aa9a9b082
								
							
						
					
					
						commit
						1d10bfca2c
					
				
										
											
												File diff suppressed because one or more lines are too long
											
										
									
								
							@ -1,4 +1,4 @@
 | 
			
		||||
// src/pages/api/upload/media.ts 
 | 
			
		||||
// 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';
 | 
			
		||||
@ -50,6 +50,7 @@ function checkUploadRateLimit(userEmail: string): boolean {
 | 
			
		||||
  }
 | 
			
		||||
  
 | 
			
		||||
  if (userLimit.count >= RATE_LIMIT_MAX) {
 | 
			
		||||
    console.warn(`[UPLOAD] Rate limit exceeded for user: ${userEmail} (${userLimit.count}/${RATE_LIMIT_MAX})`);
 | 
			
		||||
    return false;
 | 
			
		||||
  }
 | 
			
		||||
  
 | 
			
		||||
@ -58,27 +59,37 @@ function checkUploadRateLimit(userEmail: string): boolean {
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
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) {
 | 
			
		||||
    return { 
 | 
			
		||||
      valid: false, 
 | 
			
		||||
      error: `File too large. Maximum size is ${Math.round(UPLOAD_CONFIG.maxFileSize / 1024 / 1024)}MB` 
 | 
			
		||||
    };
 | 
			
		||||
    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)) {
 | 
			
		||||
    return { 
 | 
			
		||||
      valid: false, 
 | 
			
		||||
      error: `File type ${file.type} not allowed` 
 | 
			
		||||
    };
 | 
			
		||||
    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<UploadResult> {
 | 
			
		||||
  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,
 | 
			
		||||
@ -87,7 +98,7 @@ async function uploadToNextcloud(file: File, userEmail: string): Promise<UploadR
 | 
			
		||||
      storage: 'nextcloud'
 | 
			
		||||
    };
 | 
			
		||||
  } catch (error) {
 | 
			
		||||
    console.error('Nextcloud upload failed:', error);
 | 
			
		||||
    console.error('[UPLOAD] Nextcloud upload failed:', error);
 | 
			
		||||
    return {
 | 
			
		||||
      success: false,
 | 
			
		||||
      error: error instanceof Error ? error.message : 'Nextcloud upload failed',
 | 
			
		||||
@ -97,7 +108,10 @@ async function uploadToNextcloud(file: File, userEmail: string): Promise<UploadR
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
async function uploadToLocal(file: File, userType: string): Promise<UploadResult> {
 | 
			
		||||
  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, '-');
 | 
			
		||||
@ -106,11 +120,20 @@ async function uploadToLocal(file: File, userType: string): Promise<UploadResult
 | 
			
		||||
    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,
 | 
			
		||||
@ -119,7 +142,7 @@ async function uploadToLocal(file: File, userType: string): Promise<UploadResult
 | 
			
		||||
      storage: 'local'
 | 
			
		||||
    };
 | 
			
		||||
  } catch (error) {
 | 
			
		||||
    console.error('Local upload failed:', error);
 | 
			
		||||
    console.error('[UPLOAD] Local upload failed:', error);
 | 
			
		||||
    return {
 | 
			
		||||
      success: false,
 | 
			
		||||
      error: error instanceof Error ? error.message : 'Local upload failed',
 | 
			
		||||
@ -130,12 +153,23 @@ async function uploadToLocal(file: File, userType: string): Promise<UploadResult
 | 
			
		||||
 | 
			
		||||
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) {
 | 
			
		||||
      return apiError.unauthorized();
 | 
			
		||||
      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.');
 | 
			
		||||
@ -143,38 +177,60 @@ export const POST: APIRoute = async ({ request }) => {
 | 
			
		||||
 | 
			
		||||
    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) {
 | 
			
		||||
      return apiError.badRequest('Invalid form data');
 | 
			
		||||
      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 (isNextcloudConfigured()) {
 | 
			
		||||
    if (nextcloudConfigured) {
 | 
			
		||||
      console.log('[UPLOAD] Using Nextcloud as primary storage');
 | 
			
		||||
      result = await uploadToNextcloud(file, userEmail);
 | 
			
		||||
      
 | 
			
		||||
      if (!result.success) {
 | 
			
		||||
        console.warn('Nextcloud upload failed, trying local fallback:', result.error);
 | 
			
		||||
        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(`[MEDIA UPLOAD] ${file.name} (${file.size} bytes) by ${userEmail} -> ${result.storage}: ${result.url}`);
 | 
			
		||||
      console.log(`[UPLOAD] Upload completed successfully:`, {
 | 
			
		||||
        filename: result.filename,
 | 
			
		||||
        storage: result.storage,
 | 
			
		||||
        url: result.url,
 | 
			
		||||
        user: userEmail
 | 
			
		||||
      });
 | 
			
		||||
      
 | 
			
		||||
      return apiSpecial.uploadSuccess({
 | 
			
		||||
        url: result.url!,
 | 
			
		||||
@ -183,7 +239,12 @@ export const POST: APIRoute = async ({ request }) => {
 | 
			
		||||
        storage: result.storage!
 | 
			
		||||
      });
 | 
			
		||||
    } else {
 | 
			
		||||
      console.error(`[MEDIA UPLOAD FAILED] ${file.name} by ${userEmail}: ${result.error}`);
 | 
			
		||||
      console.error(`[UPLOAD] Upload failed completely:`, {
 | 
			
		||||
        filename: file.name,
 | 
			
		||||
        error: result.error,
 | 
			
		||||
        storage: result.storage,
 | 
			
		||||
        user: userEmail
 | 
			
		||||
      });
 | 
			
		||||
      
 | 
			
		||||
      return apiSpecial.uploadFailed(result.error!);
 | 
			
		||||
    }
 | 
			
		||||
@ -193,6 +254,8 @@ export const POST: APIRoute = async ({ request }) => {
 | 
			
		||||
 | 
			
		||||
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();
 | 
			
		||||
@ -204,12 +267,14 @@ export const GET: APIRoute = async ({ request }) => {
 | 
			
		||||
    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('Local upload directory not accessible:', error);
 | 
			
		||||
        console.warn('[UPLOAD] Local upload directory not accessible:', error);
 | 
			
		||||
      }
 | 
			
		||||
    }
 | 
			
		||||
    
 | 
			
		||||
@ -237,9 +302,14 @@ export const GET: APIRoute = async ({ request }) => {
 | 
			
		||||
      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');
 | 
			
		||||
 | 
			
		||||
@ -237,6 +237,7 @@ class KnowledgebaseForm {
 | 
			
		||||
 | 
			
		||||
  constructor() {
 | 
			
		||||
    this.init();
 | 
			
		||||
    this.checkUploadStatus();
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  private init() {
 | 
			
		||||
@ -319,28 +320,71 @@ class KnowledgebaseForm {
 | 
			
		||||
 | 
			
		||||
  private async uploadFile(fileId: string) {
 | 
			
		||||
    const fileItem = this.uploadedFiles.find(f => f.id === fileId);
 | 
			
		||||
    if (!fileItem) return;
 | 
			
		||||
    if (!fileItem) {
 | 
			
		||||
      console.error('[UPLOAD] File item not found for ID:', fileId);
 | 
			
		||||
      return;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    console.log('[UPLOAD] Starting upload for:', fileItem.name, 'Size:', fileItem.file.size, 'Type:', fileItem.file.type);
 | 
			
		||||
 | 
			
		||||
    const formData = new FormData();
 | 
			
		||||
    formData.append('file', fileItem.file);
 | 
			
		||||
    formData.append('type', 'knowledgebase');
 | 
			
		||||
 | 
			
		||||
    try {
 | 
			
		||||
      console.log('[UPLOAD] Sending request to /api/upload/media');
 | 
			
		||||
      
 | 
			
		||||
      const response = await fetch('/api/upload/media', {
 | 
			
		||||
        method: 'POST',
 | 
			
		||||
        body: formData
 | 
			
		||||
      });
 | 
			
		||||
 | 
			
		||||
      console.log('[UPLOAD] Response status:', response.status);
 | 
			
		||||
      console.log('[UPLOAD] Response headers:', Object.fromEntries(response.headers.entries()));
 | 
			
		||||
 | 
			
		||||
      if (response.ok) {
 | 
			
		||||
        const result = await response.json();
 | 
			
		||||
        console.log('[UPLOAD] Success result:', result);
 | 
			
		||||
        
 | 
			
		||||
        fileItem.uploaded = true;
 | 
			
		||||
        fileItem.url = result.url;
 | 
			
		||||
        this.renderFileList();
 | 
			
		||||
        
 | 
			
		||||
        this.showMessage('success', `Successfully uploaded ${fileItem.name}`);
 | 
			
		||||
      } else {
 | 
			
		||||
        throw new Error('Upload failed');
 | 
			
		||||
        // Enhanced error handling - read the actual error from response
 | 
			
		||||
        let errorMessage = `Upload failed with status ${response.status}`;
 | 
			
		||||
        
 | 
			
		||||
        try {
 | 
			
		||||
          const errorData = await response.json();
 | 
			
		||||
          console.error('[UPLOAD] Error response data:', errorData);
 | 
			
		||||
          
 | 
			
		||||
          if (errorData.error) {
 | 
			
		||||
            errorMessage = errorData.error;
 | 
			
		||||
          }
 | 
			
		||||
          
 | 
			
		||||
          // Log additional details if available
 | 
			
		||||
          if (errorData.details) {
 | 
			
		||||
            console.error('[UPLOAD] Error details:', errorData.details);
 | 
			
		||||
            errorMessage += ` (Details: ${errorData.details.join(', ')})`;
 | 
			
		||||
          }
 | 
			
		||||
        } catch (parseError) {
 | 
			
		||||
          console.error('[UPLOAD] Could not parse error response:', parseError);
 | 
			
		||||
          const errorText = await response.text();
 | 
			
		||||
          console.error('[UPLOAD] Raw error response:', errorText);
 | 
			
		||||
          errorMessage += ` (Raw: ${errorText.substring(0, 100)})`;
 | 
			
		||||
        }
 | 
			
		||||
        
 | 
			
		||||
        throw new Error(errorMessage);
 | 
			
		||||
      }
 | 
			
		||||
    } catch (error) {
 | 
			
		||||
      this.showMessage('error', `Failed to upload ${fileItem.name}`);
 | 
			
		||||
      console.error('[UPLOAD] Upload error for', fileItem.name, ':', error);
 | 
			
		||||
      
 | 
			
		||||
      const errorMessage = error instanceof Error 
 | 
			
		||||
        ? error.message 
 | 
			
		||||
        : 'Unknown upload error';
 | 
			
		||||
      
 | 
			
		||||
      this.showMessage('error', `Failed to upload ${fileItem.name}: ${errorMessage}`);
 | 
			
		||||
      this.removeFile(fileId);
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
@ -441,16 +485,63 @@ class KnowledgebaseForm {
 | 
			
		||||
    this.renderFileList();
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  private showMessage(type: 'success' | 'error' | 'warning', message: string) {
 | 
			
		||||
  private showMessage(type: 'success' | 'error' | 'warning', message: string, details?: string) {
 | 
			
		||||
    const container = document.getElementById('message-container');
 | 
			
		||||
    if (!container) return;
 | 
			
		||||
 | 
			
		||||
    const messageEl = document.createElement('div');
 | 
			
		||||
    messageEl.className = `message message-${type}`;
 | 
			
		||||
    messageEl.textContent = message;
 | 
			
		||||
    
 | 
			
		||||
    // Create message content with optional details
 | 
			
		||||
    const messageContent = document.createElement('div');
 | 
			
		||||
    messageContent.className = 'message-content';
 | 
			
		||||
    messageContent.textContent = message;
 | 
			
		||||
    
 | 
			
		||||
    messageEl.appendChild(messageContent);
 | 
			
		||||
    
 | 
			
		||||
    // Add details if provided
 | 
			
		||||
    if (details) {
 | 
			
		||||
      const detailsEl = document.createElement('div');
 | 
			
		||||
      detailsEl.className = 'message-details';
 | 
			
		||||
      detailsEl.textContent = details;
 | 
			
		||||
      messageEl.appendChild(detailsEl);
 | 
			
		||||
    }
 | 
			
		||||
    
 | 
			
		||||
    // Add close button
 | 
			
		||||
    const closeBtn = document.createElement('button');
 | 
			
		||||
    closeBtn.className = 'message-close';
 | 
			
		||||
    closeBtn.innerHTML = '×';
 | 
			
		||||
    closeBtn.onclick = () => messageEl.remove();
 | 
			
		||||
    messageEl.appendChild(closeBtn);
 | 
			
		||||
 | 
			
		||||
    container.appendChild(messageEl);
 | 
			
		||||
    setTimeout(() => messageEl.remove(), 5000);
 | 
			
		||||
    
 | 
			
		||||
    // Auto-remove after 10 seconds for errors (to give time to read), 5 seconds for others
 | 
			
		||||
    const timeout = type === 'error' ? 10000 : 5000;
 | 
			
		||||
    setTimeout(() => {
 | 
			
		||||
      if (messageEl.parentNode) {
 | 
			
		||||
        messageEl.remove();
 | 
			
		||||
      }
 | 
			
		||||
    }, timeout);
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  private async checkUploadStatus() {
 | 
			
		||||
    try {
 | 
			
		||||
      console.log('[DIAGNOSTIC] Checking upload endpoint status');
 | 
			
		||||
      const response = await fetch('/api/upload/media', { method: 'GET' });
 | 
			
		||||
      const status = await response.json();
 | 
			
		||||
      console.log('[DIAGNOSTIC] Upload status:', status);
 | 
			
		||||
      
 | 
			
		||||
      if (!status.storage?.nextcloud?.configured && !status.storage?.local?.available) {
 | 
			
		||||
        this.showMessage('warning', 'No upload storage configured. Check server configuration.');
 | 
			
		||||
      }
 | 
			
		||||
      
 | 
			
		||||
      return status;
 | 
			
		||||
    } catch (error) {
 | 
			
		||||
      console.error('[DIAGNOSTIC] Failed to check upload status:', error);
 | 
			
		||||
      this.showMessage('error', 'Could not connect to upload service');
 | 
			
		||||
      return null;
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  public removeFileById(fileId: string) {
 | 
			
		||||
 | 
			
		||||
		Loading…
	
	
			
			x
			
			
		
	
		Reference in New Issue
	
	Block a user