streamflix-api / src /shared /modules /upload /cloudinary /cloudinary.service.ts
Akshar2325
refactor(upload): reorganize storage modules under unified upload namespace
f75a84b
Raw
History Blame
7.22 kB
import { Injectable, Logger, NotFoundException } from '@nestjs/common';
import { ConfigService } from '@nestjs/config';
import { v2 as cloudinary } from 'cloudinary';
@Injectable()
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');
}
}
}