346 lines
11 KiB
TypeScript
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');
|
|
}; |