UNPKG

smart-dropzone-react

Version:

🚀 A production-ready React dropzone component with smart defaults, drag & drop reordering, chunked uploads, resume functionality, and comprehensive provider support (Cloudinary, AWS S3, Supabase)

1 lines • 18.6 kB
{"version":3,"sources":["../src/providers/cloudinary.ts"],"names":["CloudinaryProvider","UploadProvider","config","__publicField","response","error","file","options","formData","folder","key","value","errorMessage","files","onProgress","uploadPromises","index","fileId","progressInterval","progress","result","baseUrl","transformations","uploadUrl","errorText","errorData","_file","timestamp","random"],"mappings":"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","file":"chunk-2FLEA56S.cjs","sourcesContent":["import { UploadProvider } from \"../core/provider\";\r\nimport type { UploadOptions, UploadResponse } from \"../types\";\r\n\r\n/**\r\n * Cloudinary-specific upload response interface\r\n */\r\ninterface CloudinaryResponse {\r\n readonly public_id: string;\r\n readonly secure_url: string;\r\n readonly original_filename: string;\r\n readonly format: string;\r\n readonly bytes: number;\r\n readonly width?: number;\r\n readonly height?: number;\r\n readonly resource_type: string;\r\n readonly created_at: string;\r\n readonly etag: string;\r\n}\r\n\r\n/**\r\n * Cloudinary provider configuration\r\n */\r\nexport interface CloudinaryConfig {\r\n readonly cloudName: string;\r\n readonly apiKey?: string;\r\n readonly apiSecret?: string;\r\n readonly uploadPreset?: string;\r\n readonly defaultFolder?: string;\r\n}\r\n\r\n/**\r\n * Cloudinary upload provider implementation\r\n */\r\nexport class CloudinaryProvider extends UploadProvider {\r\n private readonly cloudName: string;\r\n // API key and secret are kept for future server-side uploads\r\n // private readonly apiKey?: string;\r\n // private readonly apiSecret?: string;\r\n private readonly uploadPreset?: string;\r\n private readonly defaultFolder?: string;\r\n private initialized = false;\r\n\r\n constructor(config: CloudinaryConfig) {\r\n super(\"cloudinary\", config);\r\n this.cloudName = config.cloudName;\r\n // this.apiKey = config.apiKey;\r\n // this.apiSecret = config.apiSecret;\r\n this.uploadPreset = config.uploadPreset;\r\n this.defaultFolder = config.defaultFolder;\r\n }\r\n\r\n /**\r\n * Initialize the Cloudinary provider\r\n */\r\n async initialize(): Promise<void> {\r\n if (!this.cloudName) {\r\n throw new Error(\"Cloudinary cloud name is required\");\r\n }\r\n\r\n // Test connection by making a simple request\r\n try {\r\n const response = await fetch(\r\n `https://res.cloudinary.com/${this.cloudName}/image/upload/v1/`\r\n );\r\n if (response.ok) {\r\n this.initialized = true;\r\n } else {\r\n throw new Error(`Failed to connect to Cloudinary: ${response.status}`);\r\n }\r\n } catch (error) {\r\n throw new Error(\r\n `Failed to initialize Cloudinary provider: ${error instanceof Error ? error.message : \"Unknown error\"}`\r\n );\r\n }\r\n }\r\n\r\n /**\r\n * Check if the provider is properly configured\r\n */\r\n isConfigured(): boolean {\r\n return Boolean(this.cloudName && this.initialized);\r\n }\r\n\r\n /**\r\n * Upload a single file to Cloudinary\r\n */\r\n async uploadFile(\r\n file: File,\r\n options: UploadOptions\r\n ): Promise<UploadResponse> {\r\n if (!this.isConfigured()) {\r\n throw new Error(\"Cloudinary provider not properly configured\");\r\n }\r\n\r\n try {\r\n const formData = new FormData();\r\n formData.append(\"file\", file);\r\n\r\n // Use folder from options or default folder\r\n const folder = options.folder || this.defaultFolder;\r\n if (folder) {\r\n formData.append(\"folder\", folder);\r\n }\r\n\r\n // Add upload preset if available\r\n if (this.uploadPreset) {\r\n formData.append(\"upload_preset\", this.uploadPreset);\r\n }\r\n\r\n // Add metadata if provided\r\n if (options.metadata) {\r\n Object.entries(options.metadata).forEach(([key, value]) => {\r\n formData.append(`context`, `${key}=${value}`);\r\n });\r\n }\r\n\r\n // Add tags if provided\r\n if (options.tags && options.tags.length > 0) {\r\n formData.append(\"tags\", options.tags.join(\",\"));\r\n }\r\n\r\n const response = await this.performUpload(formData);\r\n return this.mapCloudinaryResponse(response, file);\r\n } catch (error) {\r\n const errorMessage =\r\n error instanceof Error ? error.message : \"Unknown upload error\";\r\n throw new Error(`Upload failed for ${file.name}: ${errorMessage}`);\r\n }\r\n }\r\n\r\n /**\r\n * Upload multiple files to Cloudinary\r\n */\r\n async uploadFiles(\r\n files: readonly File[],\r\n options: UploadOptions,\r\n onProgress?: (fileId: string, progress: number) => void\r\n ): Promise<UploadResponse[]> {\r\n if (!files.length) {\r\n return [];\r\n }\r\n\r\n const uploadPromises = files.map(async (file, index) => {\r\n const fileId = this.generateFileId(file, index);\r\n\r\n try {\r\n if (onProgress) {\r\n // Simulate progress for better UX\r\n const progressInterval = setInterval(() => {\r\n const progress = Math.min(Math.random() * 90, 85); // Random progress up to 85%\r\n onProgress(fileId, progress);\r\n }, 200);\r\n\r\n try {\r\n const result = await this.uploadFile(file, options);\r\n onProgress(fileId, 100);\r\n clearInterval(progressInterval);\r\n return result;\r\n } catch (error) {\r\n clearInterval(progressInterval);\r\n throw error;\r\n }\r\n } else {\r\n return this.uploadFile(file, options);\r\n }\r\n } catch (error) {\r\n console.error(`Failed to upload ${file.name}:`, error);\r\n throw error;\r\n }\r\n });\r\n\r\n try {\r\n return await Promise.all(uploadPromises);\r\n } catch (error) {\r\n throw new Error(\r\n `Batch upload failed: ${\r\n error instanceof Error ? error.message : \"Unknown error\"\r\n }`\r\n );\r\n }\r\n }\r\n\r\n /**\r\n * Delete a file from Cloudinary\r\n */\r\n async deleteFile(fileId: string): Promise<void> {\r\n if (!this.isConfigured()) {\r\n throw new Error(\"Cloudinary provider not properly configured\");\r\n }\r\n\r\n try {\r\n const response = await fetch(\r\n `https://api.cloudinary.com/v1_1/${this.cloudName}/delete_by_token`,\r\n {\r\n method: \"POST\",\r\n headers: {\r\n \"Content-Type\": \"application/json\",\r\n },\r\n body: JSON.stringify({\r\n token: fileId, // In Cloudinary, this would be the delete token\r\n }),\r\n }\r\n );\r\n\r\n if (!response.ok) {\r\n throw new Error(\r\n `Failed to delete file: ${response.status} ${response.statusText}`\r\n );\r\n }\r\n } catch (error) {\r\n throw new Error(\r\n `Delete failed: ${error instanceof Error ? error.message : \"Unknown error\"}`\r\n );\r\n }\r\n }\r\n\r\n /**\r\n * Get file information from Cloudinary\r\n */\r\n async getFileInfo(fileId: string): Promise<UploadResponse | null> {\r\n if (!this.isConfigured()) {\r\n throw new Error(\"Cloudinary provider not properly configured\");\r\n }\r\n\r\n try {\r\n const response = await fetch(\r\n `https://res.cloudinary.com/${this.cloudName}/image/upload/v1/${fileId}`\r\n );\r\n\r\n if (!response.ok) {\r\n return null;\r\n }\r\n\r\n // This is a simplified approach - in practice you'd use Cloudinary's Admin API\r\n return {\r\n id: fileId,\r\n url: response.url,\r\n filename: fileId,\r\n size: 0, // Would need Admin API to get actual size\r\n mimeType: \"image/*\", // Would need Admin API to get actual type\r\n createdAt: new Date(),\r\n };\r\n } catch (error) {\r\n console.warn(`Failed to get file info for ${fileId}:`, error);\r\n return null;\r\n }\r\n }\r\n\r\n /**\r\n * Generate a preview URL for a file\r\n */\r\n generatePreviewUrl(\r\n fileId: string,\r\n options: Record<string, any> = {}\r\n ): string {\r\n const baseUrl = `https://res.cloudinary.com/${this.cloudName}/image/upload`;\r\n const transformations = this.buildTransformations(options);\r\n\r\n if (transformations) {\r\n return `${baseUrl}/${transformations}/${fileId}`;\r\n } else {\r\n return `${baseUrl}/${fileId}`;\r\n }\r\n }\r\n\r\n /**\r\n * Validate provider-specific options\r\n */\r\n validateOptions(options: UploadOptions): { valid: boolean; error?: string } {\r\n // Check if folder is valid (no special characters, reasonable length)\r\n if (\r\n options.folder &&\r\n (options.folder.length > 100 || /[<>:\"|?*]/.test(options.folder))\r\n ) {\r\n return {\r\n valid: false,\r\n error:\r\n \"Invalid folder name. Folder names cannot contain special characters and must be under 100 characters.\",\r\n };\r\n }\r\n\r\n return { valid: true };\r\n }\r\n\r\n /**\r\n * Get upload statistics\r\n */\r\n async getStats(): Promise<{\r\n totalFiles: number;\r\n totalSize: number;\r\n provider: string;\r\n }> {\r\n // This would require Cloudinary Admin API access\r\n return {\r\n totalFiles: 0,\r\n totalSize: 0,\r\n provider: \"cloudinary\",\r\n };\r\n }\r\n\r\n /**\r\n * Test the provider connection\r\n */\r\n async testConnection(): Promise<boolean> {\r\n try {\r\n const response = await fetch(\r\n `https://res.cloudinary.com/${this.cloudName}/image/upload/v1/`\r\n );\r\n return response.ok;\r\n } catch {\r\n return false;\r\n }\r\n }\r\n\r\n /**\r\n * Perform the actual upload request to Cloudinary\r\n */\r\n private async performUpload(formData: FormData): Promise<CloudinaryResponse> {\r\n const uploadUrl = `https://api.cloudinary.com/v1_1/${this.cloudName}/upload`;\r\n\r\n const response = await fetch(uploadUrl, {\r\n method: \"POST\",\r\n body: formData,\r\n });\r\n\r\n if (!response.ok) {\r\n const errorText = await response.text();\r\n let errorMessage: string;\r\n\r\n try {\r\n const errorData = JSON.parse(errorText);\r\n errorMessage = errorData.error?.message || errorData.error || errorText;\r\n } catch {\r\n errorMessage = errorText;\r\n }\r\n\r\n throw new Error(\r\n `Upload failed: ${response.status} ${response.statusText} - ${errorMessage}`\r\n );\r\n }\r\n\r\n try {\r\n const result: CloudinaryResponse = await response.json();\r\n return result;\r\n } catch (error) {\r\n throw new Error(\"Failed to parse upload response\");\r\n }\r\n }\r\n\r\n /**\r\n * Map Cloudinary response to generic UploadResponse\r\n */\r\n private mapCloudinaryResponse(\r\n response: CloudinaryResponse,\r\n file: File\r\n ): UploadResponse {\r\n return {\r\n id: response.public_id,\r\n url: response.secure_url,\r\n filename: response.original_filename,\r\n size: response.bytes,\r\n mimeType: file.type,\r\n metadata: {\r\n format: response.format,\r\n width: response.width,\r\n height: response.height,\r\n resourceType: response.resource_type,\r\n etag: response.etag,\r\n },\r\n createdAt: new Date(response.created_at),\r\n };\r\n }\r\n\r\n /**\r\n * Build Cloudinary transformations string\r\n */\r\n private buildTransformations(options: Record<string, any>): string {\r\n const transformations: string[] = [];\r\n\r\n if (options.width) transformations.push(`w_${options.width}`);\r\n if (options.height) transformations.push(`h_${options.height}`);\r\n if (options.crop) transformations.push(`c_${options.crop}`);\r\n if (options.quality) transformations.push(`q_${options.quality}`);\r\n if (options.format) transformations.push(`f_${options.format}`);\r\n\r\n return transformations.length > 0 ? transformations.join(\",\") : \"\";\r\n }\r\n\r\n /**\r\n * Generate unique file ID for tracking\r\n */\r\n private generateFileId(_file: File, index: number): string {\r\n const timestamp = Date.now();\r\n const random = Math.random().toString(36).substring(2, 15);\r\n return `upload_${timestamp}_${index}_${random}`;\r\n }\r\n}\r\n"]}