overcuriousity daa468c535 uploadFile
2025-08-09 10:42:59 +02:00

346 lines
11 KiB
TypeScript

// 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<string, { count: number; resetTime: number }>();
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<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,
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<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, '-');
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');
};