import { getR2Bucket } from '@/lib/db' export interface R2UploadResponse { success: boolean url?: string key?: string error?: string } export interface BulkUploadResult { successful: FileUploadResult[] failed: { filename: string; error: string }[] total: number } export interface FileUploadResult { filename: string url: string key: string size: number mimeType: string } export interface FileUploadProgress { id: string filename: string progress: number status: 'uploading' | 'processing' | 'complete' | 'error' url?: string error?: string } /** * File Upload Manager for Cloudflare R2 */ export class FileUploadManager { private bucket: R2Bucket private baseUrl: string constructor() { this.bucket = getR2Bucket() // R2 public URL format: https://.r2.cloudflarestorage.com/ this.baseUrl = process.env.R2_PUBLIC_URL || '' } /** * Upload a single file to R2 */ async uploadFile( file: File | Buffer, key: string, options?: { contentType?: string metadata?: Record } ): Promise { try { const fileBuffer = file instanceof File ? await file.arrayBuffer() : file.buffer as ArrayBuffer const contentType = options?.contentType || (file instanceof File ? file.type : 'application/octet-stream') // Upload to R2 await this.bucket.put(key, fileBuffer, { httpMetadata: { contentType, }, customMetadata: options?.metadata || {}, }) const url = `${this.baseUrl}/${key}` return { success: true, url, key, } } catch (error) { console.error('R2 upload error:', error) return { success: false, error: error instanceof Error ? error.message : 'Upload failed', } } } /** * Upload multiple files to R2 */ async bulkUpload( files: File[], keyPrefix: string = 'uploads' ): Promise { const successful: FileUploadResult[] = [] const failed: { filename: string; error: string }[] = [] for (const file of files) { try { const key = `${keyPrefix}/${Date.now()}-${file.name}` const result = await this.uploadFile(file, key, { contentType: file.type, metadata: { originalName: file.name, uploadedAt: new Date().toISOString(), }, }) if (result.success && result.url && result.key) { successful.push({ filename: file.name, url: result.url, key: result.key, size: file.size, mimeType: file.type, }) } else { failed.push({ filename: file.name, error: result.error || 'Upload failed', }) } } catch (error) { failed.push({ filename: file.name, error: error instanceof Error ? error.message : 'Upload failed', }) } } return { successful, failed, total: files.length, } } /** * Delete a file from R2 */ async deleteFile(key: string): Promise { try { await this.bucket.delete(key) return true } catch (error) { console.error('R2 delete error:', error) return false } } /** * Get file metadata from R2 */ async getFileMetadata(key: string): Promise { try { return await this.bucket.get(key) } catch (error) { console.error('R2 metadata error:', error) return null } } /** * Generate a presigned URL for direct upload */ async generatePresignedUrl( key: string, expiresIn: number = 3600 ): Promise { try { // Note: R2 presigned URLs require additional setup // For now, we'll use direct upload through our API return null } catch (error) { console.error('Presigned URL error:', error) return null } } /** * Validate file before upload */ validateFile(file: File, options?: { maxSize?: number allowedTypes?: string[] }): { valid: boolean; error?: string } { const maxSize = options?.maxSize || 10 * 1024 * 1024 // 10MB default const allowedTypes = options?.allowedTypes || [ 'image/jpeg', 'image/png', 'image/webp', 'image/gif', ] if (file.size > maxSize) { return { valid: false, error: `File size exceeds ${Math.round(maxSize / 1024 / 1024)}MB limit`, } } if (!allowedTypes.includes(file.type)) { return { valid: false, error: `File type ${file.type} not allowed`, } } return { valid: true } } /** * Generate a unique key for file upload */ generateFileKey(filename: string, prefix: string = 'uploads'): string { const timestamp = Date.now() const randomString = Math.random().toString(36).substring(2, 15) const extension = filename.split('.').pop() const baseName = filename.replace(/\.[^/.]+$/, '').replace(/[^a-zA-Z0-9]/g, '-') return `${prefix}/${timestamp}-${randomString}-${baseName}.${extension}` } } /** * Convenience functions for common upload operations */ export async function uploadToR2( file: File, key?: string, options?: { contentType?: string metadata?: Record } ): Promise { const manager = new FileUploadManager() const uploadKey = key || manager.generateFileKey(file.name) return await manager.uploadFile(file, uploadKey, options) } export async function bulkUploadToR2( files: File[], keyPrefix?: string ): Promise { const manager = new FileUploadManager() return await manager.bulkUpload(files, keyPrefix) } export async function deleteFromR2(key: string): Promise { const manager = new FileUploadManager() return await manager.deleteFile(key) } export function validateUploadFile( file: File, options?: { maxSize?: number allowedTypes?: string[] } ): { valid: boolean; error?: string } { const manager = new FileUploadManager() return manager.validateFile(file, options) } /** * Portfolio image specific upload functions */ export async function uploadPortfolioImage( file: File, artistId: string, options?: { caption?: string tags?: string[] } ): Promise { const manager = new FileUploadManager() // Validate image file const validation = manager.validateFile(file, { maxSize: 5 * 1024 * 1024, // 5MB for portfolio images allowedTypes: ['image/jpeg', 'image/png', 'image/webp'], }) if (!validation.valid) { return { success: false, error: validation.error, } } // Generate key for portfolio image const key = manager.generateFileKey(file.name, `portfolio/${artistId}`) // Upload to R2 const uploadResult = await manager.uploadFile(file, key, { contentType: file.type, metadata: { artistId, originalName: file.name, caption: options?.caption || '', tags: JSON.stringify(options?.tags || []), uploadedAt: new Date().toISOString(), }, }) if (uploadResult.success) { return { ...uploadResult, portfolioData: { artistId, url: uploadResult.url, caption: options?.caption, tags: options?.tags || [], }, } } return uploadResult } /** * Artist profile image upload */ export async function uploadArtistProfileImage( file: File, artistId: string ): Promise { const manager = new FileUploadManager() // Validate image file const validation = manager.validateFile(file, { maxSize: 2 * 1024 * 1024, // 2MB for profile images allowedTypes: ['image/jpeg', 'image/png', 'image/webp'], }) if (!validation.valid) { return { success: false, error: validation.error, } } // Generate key for profile image const key = `profiles/${artistId}/profile-${Date.now()}.${file.name.split('.').pop()}` return await manager.uploadFile(file, key, { contentType: file.type, metadata: { artistId, type: 'profile', originalName: file.name, uploadedAt: new Date().toISOString(), }, }) }