import { NextRequest, NextResponse } from 'next/server'; import { uploadToTelegram, sendLog } from '@/lib/telegram'; import { uploadToHuggingFace, isHuggingFaceConfigured } from '@/lib/huggingface'; import { saveImage, generateId, rateLimit, getUserWebhooks, triggerWebhook } from '@/lib/db'; import { authenticateRequest } from '@/lib/auth'; export async function POST(req: NextRequest) { // Authenticate request (optional - supports both API key and JWT) const auth = await authenticateRequest(req); // Determine rate limit based on authentication const ip = req.headers.get('x-forwarded-for') || 'anonymous'; let rateLimitKey: string; let rateLimitValue: number; let rateLimitWindow: number; if (auth?.apiKey) { // API key authenticated - higher limits rateLimitKey = `upload:apikey:${auth.apiKey.id}`; rateLimitValue = auth.apiKey.rate_limit || 100; rateLimitWindow = 60; // 1 minute } else if (auth?.userId) { // JWT authenticated - medium limits rateLimitKey = `upload:user:${auth.userId}`; rateLimitValue = 50; rateLimitWindow = 60; } else { // Anonymous - lower limits rateLimitKey = `upload:${ip}`; rateLimitValue = 20; rateLimitWindow = 60; } const limit = await rateLimit(rateLimitKey, rateLimitValue, rateLimitWindow); if (!limit.success) { return NextResponse.json({ success: false, error: { code: 'RATE_LIMIT_EXCEEDED', message: `Too many uploads. Try again in ${limit.remaining === 0 ? 'a minute' : 'a moment'}.` } }, { status: 429, headers: { 'X-RateLimit-Limit': (limit.limit ?? rateLimitValue).toString(), 'X-RateLimit-Remaining': (limit.remaining ?? 0).toString() } }); } try { const formData = await req.formData(); const file = formData.get('file') as Blob; const customId = formData.get('customId') as string; if (!file) { return NextResponse.json({ success: false, error: { code: 'MISSING_FILE', message: 'No file provided in request' } }, { status: 400 }); } // Enforce 2GB limit const MAX_SIZE = 2 * 1024 * 1024 * 1024; // 2GB if (file.size > MAX_SIZE) { return NextResponse.json({ success: false, error: { code: 'FILE_TOO_LARGE', message: 'File too large. Max size is 2GB.' } }, { status: 400 }); } // 2. Generate ID const id = customId ? customId.toLowerCase().replace(/[^a-z0-9-]/g, '-') : generateId(); // Determine storage based on configuration // Prefer Hugging Face if configured (especially in HF Spaces environment) const LARGE_FILE_THRESHOLD = 50 * 1024 * 1024; // 50MB const isLargeFile = file.size > LARGE_FILE_THRESHOLD; const useHFForLargeFiles = isHuggingFaceConfigured() && process.env.USE_HF_FOR_LARGE_FILES === 'true'; const useHFStorage = isHuggingFaceConfigured() && (process.env.USE_HF_STORAGE === 'true' || process.env.SPACE_ID); // Auto-enable in HF Spaces const isHFEnvironment = !!process.env.SPACE_ID; // Detect Hugging Face Spaces environment // 1. Upload to storage (prefer Hugging Face if configured, especially in HF Spaces) let storageResult: { file_id: string; file_url?: string }; let storageType: 'telegram' | 'huggingface' = 'telegram'; // Priority: HF Storage > HF for large files > Telegram (with graceful fallback) if (useHFStorage) { // Use HF Hub for all files if configured try { const fileName = customId || `upload-${id}`; const hfResult = await uploadToHuggingFace(file, fileName, id); storageResult = { file_id: hfResult.file_id, file_url: hfResult.file_url }; storageType = 'huggingface'; // Also forward to Telegram chat (for backup/notification) - optional try { let mediaType: 'photo' | 'animation' | 'video' = 'photo'; if (file.type.startsWith('video/')) mediaType = 'video'; if (file.type === 'image/gif') mediaType = 'animation'; await uploadToTelegram(file, fileName, `šŸ“¦ Uploaded via API v2 (HF Hub)\nšŸ”— HF URL: ${hfResult.file_url}`, mediaType); } catch (tgError) { console.error('Failed to forward HF upload to Telegram chat (optional):', tgError); // Don't fail the upload if Telegram forwarding fails } } catch (hfError: any) { console.error('HF upload failed:', hfError); // In HF Spaces, don't fallback to Telegram (it won't work) if (isHFEnvironment) { throw new Error(`Hugging Face upload failed: ${hfError.message}. Please check HF_TOKEN and HF_REPO_ID configuration.`); } // Fallback to Telegram only if not in HF Spaces try { let mediaType: 'photo' | 'animation' | 'video' = 'photo'; if (file.type.startsWith('video/')) mediaType = 'video'; if (file.type === 'image/gif') mediaType = 'animation'; const telegramResult = await uploadToTelegram(file, 'upload', 'šŸ“¦ Uploaded via API v2', mediaType); storageResult = { file_id: telegramResult.file_id }; } catch (tgError: any) { throw new Error(`Both HF and Telegram uploads failed. HF: ${hfError.message}, Telegram: ${tgError.message}`); } } } else if (useHFForLargeFiles && isLargeFile) { // Use Hugging Face Hub for large files only try { const fileName = customId || `upload-${id}`; const hfResult = await uploadToHuggingFace(file, fileName, id); storageResult = { file_id: hfResult.file_id, file_url: hfResult.file_url }; storageType = 'huggingface'; // Also forward to Telegram chat (for backup/notification) - optional try { let mediaType: 'photo' | 'animation' | 'video' = 'photo'; if (file.type.startsWith('video/')) mediaType = 'video'; if (file.type === 'image/gif') mediaType = 'animation'; await uploadToTelegram(file, fileName, `šŸ“¦ Uploaded via API v2 (HF Hub)\nšŸ”— HF URL: ${hfResult.file_url}`, mediaType); } catch (tgError) { console.error('Failed to forward HF upload to Telegram chat (optional):', tgError); // Don't fail the upload if Telegram forwarding fails } } catch (hfError: any) { console.error('HF upload failed, falling back to Telegram:', hfError); // Fallback to Telegram if HF fails try { let mediaType: 'photo' | 'animation' | 'video' = 'photo'; if (file.type.startsWith('video/')) mediaType = 'video'; if (file.type === 'image/gif') mediaType = 'animation'; const telegramResult = await uploadToTelegram(file, 'upload', 'šŸ“¦ Uploaded via API v2', mediaType); storageResult = { file_id: telegramResult.file_id }; } catch (tgError: any) { throw new Error(`Upload failed. HF: ${hfError.message}, Telegram: ${tgError.message}`); } } } else { // Use Telegram (default) - but check if we're in HF Spaces if (isHFEnvironment && !isHuggingFaceConfigured()) { throw new Error('Telegram API is not accessible in Hugging Face Spaces. Please configure HF_TOKEN and HF_REPO_ID to use Hugging Face storage.'); } try { let mediaType: 'photo' | 'animation' | 'video' = 'photo'; if (file.type.startsWith('video/')) mediaType = 'video'; if (file.type === 'image/gif') mediaType = 'animation'; const telegramResult = await uploadToTelegram(file, 'upload', 'šŸ“¦ Uploaded via API v2', mediaType); storageResult = { file_id: telegramResult.file_id }; } catch (tgError: any) { // If Telegram fails and HF is configured, try HF as fallback if (isHuggingFaceConfigured()) { console.error('Telegram upload failed, trying HF as fallback:', tgError); try { const fileName = customId || `upload-${id}`; const hfResult = await uploadToHuggingFace(file, fileName, id); storageResult = { file_id: hfResult.file_id, file_url: hfResult.file_url }; storageType = 'huggingface'; } catch (hfError: any) { throw new Error(`Both Telegram and HF uploads failed. Telegram: ${tgError.message}, HF: ${hfError.message}`); } } else { throw new Error(`Telegram upload failed: ${tgError.message}. Consider configuring Hugging Face storage.`); } } } const record: any = { id, telegram_file_id: storageResult.file_id, storage_type: storageType, storage_url: storageResult.file_url, created_at: Date.now(), metadata: { size: file.size, type: file.type, version: 'v2' } }; // Add user_id if authenticated if (auth?.userId) { record.user_id = auth.userId; } // Save to DB await saveImage(record, 'web', auth?.userId); const baseUrl = process.env.NEXT_PUBLIC_BASE_URL || (req.headers.get('host') ? `http://${req.headers.get('host')}` : ''); const publicUrl = `${baseUrl}/i/${id}`; // Log to Telegram (optional - don't fail if it doesn't work) try { const authInfo = auth?.apiKey ? `API Key: ${auth.apiKey.prefix}...` : auth?.userId ? `User: ${auth.userId}` : 'Anonymous'; await sendLog(`🌐 New API v2 Upload\n\n${authInfo}\nType: ${file.type}\nSize: ${(file.size / 1024 / 1024).toFixed(2)} MB\nLink: ${publicUrl}`); } catch (logError) { console.error('Failed to send log to Telegram (optional):', logError); } // Trigger webhooks if user has any if (auth?.userId) { const webhooks = await getUserWebhooks(auth.userId); for (const webhook of webhooks) { await triggerWebhook(webhook, 'upload', { id, url: publicUrl, size: file.size, type: file.type, created_at: record.created_at }); } } return NextResponse.json({ success: true, data: { id, url: `${baseUrl}/i/${id}`, direct_url: `${baseUrl}/i/${id}.jpg`, timestamp: record.created_at, authenticated: !!auth } }, { headers: { 'X-RateLimit-Limit': (limit.limit ?? rateLimitValue).toString(), 'X-RateLimit-Remaining': (limit.remaining ?? 0).toString() } }); } catch (error: any) { console.error('Upload API Error:', error); // Try to log error to Telegram (optional) try { await sendLog(`āŒ API v2 Upload Error\n\nError: ${error.message || error}`); } catch (logError) { console.error('Failed to send error log to Telegram (optional):', logError); } return NextResponse.json({ success: false, error: { code: 'INTERNAL_ERROR', message: error.message || 'Server processed request failed' } }, { status: 500 }); } }