dragxd's picture
Fix: Prefer Hugging Face storage in HF Spaces, handle Telegram failures gracefully
3cdbd12
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, `πŸ“¦ <b>Uploaded via API v2 (HF Hub)</b>\nπŸ”— <b>HF URL:</b> ${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', 'πŸ“¦ <b>Uploaded via API v2</b>', 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, `πŸ“¦ <b>Uploaded via API v2 (HF Hub)</b>\nπŸ”— <b>HF URL:</b> ${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', 'πŸ“¦ <b>Uploaded via API v2</b>', 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', 'πŸ“¦ <b>Uploaded via API v2</b>', 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(`🌐 <b>New API v2 Upload</b>\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(`❌ <b>API v2 Upload Error</b>\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 });
}
}