more error handling and logging in uploads mechanic

This commit is contained in:
overcuriousity 2025-08-09 10:30:11 +02:00
parent 8aa9a9b082
commit 1d10bfca2c
3 changed files with 186 additions and 25 deletions

File diff suppressed because one or more lines are too long

View File

@ -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 type { APIRoute } from 'astro';
import { withAPIAuth } from '../../../utils/auth.js'; import { withAPIAuth } from '../../../utils/auth.js';
import { apiResponse, apiError, apiServerError, apiSpecial, handleAPIRequest } from '../../../utils/api.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) { if (userLimit.count >= RATE_LIMIT_MAX) {
console.warn(`[UPLOAD] Rate limit exceeded for user: ${userEmail} (${userLimit.count}/${RATE_LIMIT_MAX})`);
return false; return false;
} }
@ -58,27 +59,37 @@ function checkUploadRateLimit(userEmail: string): boolean {
} }
function validateFile(file: File): { valid: boolean; error?: string } { 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) { if (file.size > UPLOAD_CONFIG.maxFileSize) {
return { const errorMsg = `File too large. Maximum size is ${Math.round(UPLOAD_CONFIG.maxFileSize / 1024 / 1024)}MB`;
valid: false, console.warn(`[UPLOAD] ${errorMsg} - File size: ${file.size}`);
error: `File too large. Maximum size is ${Math.round(UPLOAD_CONFIG.maxFileSize / 1024 / 1024)}MB` return { valid: false, error: errorMsg };
};
} }
if (!UPLOAD_CONFIG.allowedTypes.has(file.type)) { if (!UPLOAD_CONFIG.allowedTypes.has(file.type)) {
return { const errorMsg = `File type ${file.type} not allowed`;
valid: false, console.warn(`[UPLOAD] ${errorMsg} - Allowed types:`, Array.from(UPLOAD_CONFIG.allowedTypes));
error: `File type ${file.type} not allowed` return { valid: false, error: errorMsg };
};
} }
console.log(`[UPLOAD] File validation passed for: ${file.name}`);
return { valid: true }; return { valid: true };
} }
async function uploadToNextcloud(file: File, userEmail: string): Promise<UploadResult> { async function uploadToNextcloud(file: File, userEmail: string): Promise<UploadResult> {
console.log(`[UPLOAD] Attempting Nextcloud upload for: ${file.name} by ${userEmail}`);
try { try {
const uploader = new NextcloudUploader(); const uploader = new NextcloudUploader();
const result = await uploader.uploadFile(file, userEmail); const result = await uploader.uploadFile(file, userEmail);
console.log(`[UPLOAD] Nextcloud upload successful:`, {
filename: result.filename,
url: result.url,
size: file.size
});
return { return {
success: true, success: true,
url: result.url, url: result.url,
@ -87,7 +98,7 @@ async function uploadToNextcloud(file: File, userEmail: string): Promise<UploadR
storage: 'nextcloud' storage: 'nextcloud'
}; };
} catch (error) { } catch (error) {
console.error('Nextcloud upload failed:', error); console.error('[UPLOAD] Nextcloud upload failed:', error);
return { return {
success: false, success: false,
error: error instanceof Error ? error.message : 'Nextcloud upload failed', 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> { async function uploadToLocal(file: File, userType: string): Promise<UploadResult> {
console.log(`[UPLOAD] Attempting local upload for: ${file.name} (${userType})`);
try { try {
console.log(`[UPLOAD] Creating directory: ${UPLOAD_CONFIG.localUploadPath}`);
await fs.mkdir(UPLOAD_CONFIG.localUploadPath, { recursive: true }); await fs.mkdir(UPLOAD_CONFIG.localUploadPath, { recursive: true });
const timestamp = new Date().toISOString().replace(/[:.]/g, '-'); 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 filename = `${timestamp}-${randomString}${extension}`;
const filepath = path.join(UPLOAD_CONFIG.localUploadPath, filename); const filepath = path.join(UPLOAD_CONFIG.localUploadPath, filename);
console.log(`[UPLOAD] Writing file to: ${filepath}`);
const buffer = Buffer.from(await file.arrayBuffer()); const buffer = Buffer.from(await file.arrayBuffer());
await fs.writeFile(filepath, buffer); await fs.writeFile(filepath, buffer);
const publicUrl = `${UPLOAD_CONFIG.publicBaseUrl}/uploads/${filename}`; const publicUrl = `${UPLOAD_CONFIG.publicBaseUrl}/uploads/${filename}`;
console.log(`[UPLOAD] Local upload successful:`, {
filename,
filepath,
publicUrl,
size: file.size
});
return { return {
success: true, success: true,
url: publicUrl, url: publicUrl,
@ -119,7 +142,7 @@ async function uploadToLocal(file: File, userType: string): Promise<UploadResult
storage: 'local' storage: 'local'
}; };
} catch (error) { } catch (error) {
console.error('Local upload failed:', error); console.error('[UPLOAD] Local upload failed:', error);
return { return {
success: false, success: false,
error: error instanceof Error ? error.message : 'Local upload failed', 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 }) => { export const POST: APIRoute = async ({ request }) => {
return await handleAPIRequest(async () => { return await handleAPIRequest(async () => {
console.log('[UPLOAD] Processing upload request');
// Enhanced auth logging
const authResult = await withAPIAuth(request, 'contributions'); 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) { 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'; const userEmail = authResult.session?.email || 'anon@anon.anon';
console.log(`[UPLOAD] Processing upload for user: ${userEmail}`);
if (!checkUploadRateLimit(userEmail)) { if (!checkUploadRateLimit(userEmail)) {
return apiError.rateLimit('Upload rate limit exceeded. Please wait before uploading again.'); return apiError.rateLimit('Upload rate limit exceeded. Please wait before uploading again.');
@ -143,38 +177,60 @@ export const POST: APIRoute = async ({ request }) => {
let formData; let formData;
try { try {
console.log('[UPLOAD] Parsing form data');
formData = await request.formData(); formData = await request.formData();
console.log('[UPLOAD] Form data keys:', Array.from(formData.keys()));
} catch (error) { } 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 file = formData.get('file') as File;
const type = formData.get('type') as string; const type = formData.get('type') as string;
if (!file) { if (!file) {
console.warn('[UPLOAD] No file provided in request');
return apiSpecial.missingRequired(['file']); return apiSpecial.missingRequired(['file']);
} }
console.log(`[UPLOAD] Processing file: ${file.name}, type parameter: ${type}`);
const validation = validateFile(file); const validation = validateFile(file);
if (!validation.valid) { if (!validation.valid) {
return apiError.badRequest(validation.error!); 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; let result: UploadResult;
if (isNextcloudConfigured()) { if (nextcloudConfigured) {
console.log('[UPLOAD] Using Nextcloud as primary storage');
result = await uploadToNextcloud(file, userEmail); result = await uploadToNextcloud(file, userEmail);
if (!result.success) { 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); result = await uploadToLocal(file, type);
} }
} else { } else {
console.log('[UPLOAD] Using local storage (Nextcloud not configured)');
result = await uploadToLocal(file, type); result = await uploadToLocal(file, type);
} }
if (result.success) { 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({ return apiSpecial.uploadSuccess({
url: result.url!, url: result.url!,
@ -183,7 +239,12 @@ export const POST: APIRoute = async ({ request }) => {
storage: result.storage! storage: result.storage!
}); });
} else { } 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!); return apiSpecial.uploadFailed(result.error!);
} }
@ -193,6 +254,8 @@ export const POST: APIRoute = async ({ request }) => {
export const GET: APIRoute = async ({ request }) => { export const GET: APIRoute = async ({ request }) => {
return await handleAPIRequest(async () => { return await handleAPIRequest(async () => {
console.log('[UPLOAD] Getting upload status');
const authResult = await withAPIAuth(request); const authResult = await withAPIAuth(request);
if (authResult.authRequired && !authResult.authenticated) { if (authResult.authRequired && !authResult.authenticated) {
return apiError.unauthorized(); return apiError.unauthorized();
@ -204,12 +267,14 @@ export const GET: APIRoute = async ({ request }) => {
try { try {
await fs.access(UPLOAD_CONFIG.localUploadPath); await fs.access(UPLOAD_CONFIG.localUploadPath);
localStorageAvailable = true; localStorageAvailable = true;
console.log('[UPLOAD] Local storage accessible');
} catch { } catch {
try { try {
await fs.mkdir(UPLOAD_CONFIG.localUploadPath, { recursive: true }); await fs.mkdir(UPLOAD_CONFIG.localUploadPath, { recursive: true });
localStorageAvailable = true; localStorageAvailable = true;
console.log('[UPLOAD] Local storage created');
} catch (error) { } 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: { paths: {
uploadEndpoint: '/api/upload/media', uploadEndpoint: '/api/upload/media',
localPath: localStorageAvailable ? '/uploads' : null 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); return apiResponse.success(status);
}, 'Upload status retrieval failed'); }, 'Upload status retrieval failed');

View File

@ -237,6 +237,7 @@ class KnowledgebaseForm {
constructor() { constructor() {
this.init(); this.init();
this.checkUploadStatus();
} }
private init() { private init() {
@ -319,28 +320,71 @@ class KnowledgebaseForm {
private async uploadFile(fileId: string) { private async uploadFile(fileId: string) {
const fileItem = this.uploadedFiles.find(f => f.id === fileId); 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(); const formData = new FormData();
formData.append('file', fileItem.file); formData.append('file', fileItem.file);
formData.append('type', 'knowledgebase'); formData.append('type', 'knowledgebase');
try { try {
console.log('[UPLOAD] Sending request to /api/upload/media');
const response = await fetch('/api/upload/media', { const response = await fetch('/api/upload/media', {
method: 'POST', method: 'POST',
body: formData body: formData
}); });
console.log('[UPLOAD] Response status:', response.status);
console.log('[UPLOAD] Response headers:', Object.fromEntries(response.headers.entries()));
if (response.ok) { if (response.ok) {
const result = await response.json(); const result = await response.json();
console.log('[UPLOAD] Success result:', result);
fileItem.uploaded = true; fileItem.uploaded = true;
fileItem.url = result.url; fileItem.url = result.url;
this.renderFileList(); this.renderFileList();
this.showMessage('success', `Successfully uploaded ${fileItem.name}`);
} else { } 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) { } 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); this.removeFile(fileId);
} }
} }
@ -441,16 +485,63 @@ class KnowledgebaseForm {
this.renderFileList(); 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'); const container = document.getElementById('message-container');
if (!container) return; if (!container) return;
const messageEl = document.createElement('div'); const messageEl = document.createElement('div');
messageEl.className = `message message-${type}`; 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); 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) { public removeFileById(fileId: string) {