Spaces:
Runtime error
Runtime error
| import { Injectable, Logger, NotFoundException } from '@nestjs/common'; | |
| import { ConfigService } from '@nestjs/config'; | |
| import { v2 as cloudinary } from 'cloudinary'; | |
| () | |
| export class CloudinaryService { | |
| private readonly logger = new Logger(CloudinaryService.name); | |
| constructor(private configService: ConfigService) { | |
| cloudinary.config({ | |
| cloud_name: this.configService.get<string>('cloudinary.cloudName'), | |
| api_key: this.configService.get<string>('cloudinary.apiKey'), | |
| api_secret: this.configService.get<string>('cloudinary.apiSecret'), | |
| }); | |
| } | |
| /** | |
| * Get folder path based on file type | |
| * @param fileType - The file type (IMAGE, DOCUMENT, VIDEO) | |
| * @returns Folder path for the file type | |
| */ | |
| private getFolderPath(fileType: string): string { | |
| const baseFolder = | |
| this.configService.get<string>('cloudinary.uploadFolder') || 'StreamFlix'; | |
| const folderMap: Record<string, string> = { | |
| IMAGE: `${baseFolder}/images`, | |
| DOCUMENT: `${baseFolder}/documents`, | |
| VIDEO: `${baseFolder}/videos`, | |
| }; | |
| return folderMap[fileType.toUpperCase()] || `${baseFolder}/others`; | |
| } | |
| /** | |
| * Get resource type based on file type | |
| * @param fileType - The file type (IMAGE, DOCUMENT, VIDEO) | |
| * @returns Cloudinary resource type | |
| */ | |
| private getResourceType(fileType: string): string { | |
| const resourceMap: Record<string, string> = { | |
| IMAGE: 'image', | |
| DOCUMENT: 'raw', | |
| VIDEO: 'video', | |
| }; | |
| return resourceMap[fileType.toUpperCase()] || 'auto'; | |
| } | |
| /** | |
| * Get signed upload parameters for client-side upload | |
| * @param fileType - Optional file type restriction (IMAGE, DOCUMENT, VIDEO) | |
| * @param publicId - Optional custom public_id for the file | |
| * @returns Signed upload parameters (signature, timestamp, folder, etc.) | |
| */ | |
| getSignedUploadParams(fileType?: string, publicId?: string) { | |
| try { | |
| const timestamp = Math.round(new Date().getTime() / 1000); | |
| const folder = fileType | |
| ? this.getFolderPath(fileType) | |
| : this.configService.get<string>('cloudinary.uploadFolder') || | |
| 'StreamFlix'; | |
| const resourceType = fileType ? this.getResourceType(fileType) : 'auto'; | |
| // Parameters to sign (MUST match what frontend sends) | |
| const paramsToSign: Record<string, any> = { | |
| timestamp, | |
| folder, | |
| }; | |
| // If publicId is provided, include it in signature | |
| if (publicId) { | |
| paramsToSign.public_id = publicId; | |
| } | |
| // Generate signature | |
| const signature = cloudinary.utils.api_sign_request( | |
| paramsToSign, | |
| this.configService.get<string>('cloudinary.apiSecret') || '', | |
| ); | |
| this.logger.log( | |
| `Generated signed upload parameters for Cloudinary${fileType ? ` (type: ${fileType}, folder: ${folder})` : ''}${publicId ? `, publicId: ${publicId}` : ''}`, | |
| ); | |
| return { | |
| signature, | |
| timestamp, | |
| folder, | |
| resourceType, | |
| ...(publicId && { publicId }), | |
| ...(fileType && { fileType }), | |
| }; | |
| } catch (error) { | |
| this.logger.error('Failed to generate signed upload parameters', error); | |
| throw error; | |
| } | |
| } | |
| /** | |
| * Delete a file from Cloudinary by public ID | |
| * @param publicId - The Cloudinary public ID | |
| * @param resourceType - The resource type (image, video, raw) | |
| * @returns Deletion result | |
| */ | |
| async deleteFile( | |
| publicId: string, | |
| resourceType: string = 'image', | |
| ): Promise<any> { | |
| try { | |
| const result = await cloudinary.uploader.destroy(publicId, { | |
| resource_type: resourceType, | |
| invalidate: true, // Invalidate CDN cache | |
| }); | |
| this.logger.log(`Successfully deleted file: ${publicId}`); | |
| return result; | |
| } catch (error) { | |
| this.logger.error(`Failed to delete file: ${publicId}`, error); | |
| throw error; | |
| } | |
| } | |
| /** | |
| * Delete a file from Cloudinary by URL | |
| * @param url - The Cloudinary file URL | |
| * @returns Deletion result | |
| */ | |
| async deleteFileByUrl(url: string): Promise<any> { | |
| try { | |
| // Extract public ID from URL | |
| // Example: https://res.cloudinary.com/dcvbvmpfu/image/upload/v1234567890/StreamFlix/images/file.jpg | |
| // Extract: StreamFlix/images/file | |
| const publicId = this.extractPublicIdFromUrl(url); | |
| if (!publicId) { | |
| throw new NotFoundException('Could not extract public ID from URL'); | |
| } | |
| // Determine resource type from URL | |
| const resourceType = this.extractResourceTypeFromUrl(url); | |
| const result = await this.deleteFile(publicId, resourceType); | |
| this.logger.log(`Successfully deleted file from URL: ${url}`); | |
| return { | |
| ...result, | |
| deletedUrl: url, | |
| publicId, | |
| }; | |
| } catch (error) { | |
| if (error instanceof NotFoundException) { | |
| throw error; | |
| } | |
| this.logger.error(`Failed to delete file by URL: ${url}`, error); | |
| throw new NotFoundException('File not found or already deleted'); | |
| } | |
| } | |
| /** | |
| * Extract public ID from Cloudinary URL | |
| * @param url - The Cloudinary URL | |
| * @returns Public ID | |
| */ | |
| private extractPublicIdFromUrl(url: string): string | null { | |
| try { | |
| // Match pattern: /upload/v{version}/{public_id}.{extension} | |
| const match = url.match(/\/upload\/(?:v\d+\/)?(.+)\.\w+$/); | |
| return match ? match[1] : null; | |
| } catch { | |
| return null; | |
| } | |
| } | |
| /** | |
| * Extract resource type from Cloudinary URL | |
| * @param url - The Cloudinary URL | |
| * @returns Resource type (image, video, raw) | |
| */ | |
| private extractResourceTypeFromUrl(url: string): string { | |
| if (url.includes('/image/upload/')) return 'image'; | |
| if (url.includes('/video/upload/')) return 'video'; | |
| if (url.includes('/raw/upload/')) return 'raw'; | |
| return 'image'; // default | |
| } | |
| /** | |
| * Get file details from Cloudinary | |
| * @param publicId - The Cloudinary public ID | |
| * @param resourceType - The resource type | |
| * @returns File details | |
| */ | |
| async getFileDetails( | |
| publicId: string, | |
| resourceType: string = 'image', | |
| ): Promise<any> { | |
| try { | |
| const result = await cloudinary.api.resource(publicId, { | |
| resource_type: resourceType, | |
| }); | |
| return result; | |
| } catch (error) { | |
| this.logger.error(`Failed to get file details for: ${publicId}`, error); | |
| throw new NotFoundException('File not found'); | |
| } | |
| } | |
| /** | |
| * Get file details from Cloudinary by URL | |
| * @param url - The Cloudinary file URL | |
| * @returns File details | |
| */ | |
| async getFileDetailsByUrl(url: string): Promise<any> { | |
| try { | |
| const publicId = this.extractPublicIdFromUrl(url); | |
| if (!publicId) { | |
| throw new NotFoundException('Could not extract public ID from URL'); | |
| } | |
| const resourceType = this.extractResourceTypeFromUrl(url); | |
| const result = await this.getFileDetails(publicId, resourceType); | |
| this.logger.log(`Retrieved file details for URL: ${url}`); | |
| return result; | |
| } catch (error) { | |
| if (error instanceof NotFoundException) { | |
| throw error; | |
| } | |
| this.logger.error(`Failed to get file details by URL: ${url}`, error); | |
| throw new NotFoundException('File not found'); | |
| } | |
| } | |
| } | |