import { Injectable, Logger, NotFoundException } from '@nestjs/common'; import { ConfigService } from '@nestjs/config'; import ImageKit from 'imagekit'; @Injectable() export class ImagekitioService { private readonly logger = new Logger(ImagekitioService.name); private imagekit: ImageKit; constructor(private configService: ConfigService) { this.imagekit = new ImageKit({ publicKey: this.configService.get('imagekitio.publicKey') || '', privateKey: this.configService.get('imagekitio.privateKey') || '', urlEndpoint: this.configService.get('imagekitio.urlEndpoint') || '', }); } /** * 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 folderMap: Record = { IMAGE: 'images', DOCUMENT: 'documents', VIDEO: 'videos', }; return folderMap[fileType.toUpperCase()] || 'others'; } /** * Get authentication parameters for client-side upload * @param fileType - Optional file type restriction (IMAGE, DOCUMENT, VIDEO) * @returns Authentication parameters (token, expire, signature, folder) */ getAuthenticationParameters(fileType?: string) { try { // Determine folder based on file type const folder = fileType ? this.getFolderPath(fileType) : 'others'; // Generate auth params const authParams = this.imagekit.getAuthenticationParameters(); this.logger.log( `Generated authentication parameters for ImageKit upload${fileType ? ` (type: ${fileType}, folder: ${folder})` : ''}`, ); return { ...authParams, folder, // Folder where file should be uploaded ...(fileType && { fileType }), }; } catch (error) { this.logger.error('Failed to generate authentication parameters', error); throw error; } } /** * Clean URL by removing query parameters * @param url - The URL to clean * @returns Clean URL without query parameters */ private cleanUrl(url: string): string { return url.split('?')[0]; } /** * Get file ID from ImageKit URL * @param url - The ImageKit file URL * @returns File details including fileId */ async getFileIdByUrl(url: string): Promise { try { // Clean URL (remove query parameters like ?updatedAt=...) const cleanUrl = this.cleanUrl(url); const urlEndpoint = this.configService.get( 'imagekitio.urlEndpoint', ); if (!cleanUrl.startsWith(urlEndpoint || '')) { throw new NotFoundException( 'Invalid ImageKit URL - does not match your URL endpoint', ); } // Extract file path from URL const filePath = cleanUrl.replace(urlEndpoint || '', ''); // Extract filename from path (last part after /) const fileName = filePath.split('/').pop() || ''; if (!fileName) { throw new NotFoundException('Could not extract filename from URL'); } // Search by filename const files = await this.imagekit.listFiles({ searchQuery: `name="${fileName}"`, }); if (!files || files.length === 0) { this.logger.warn(`File not found: ${fileName}`); throw new NotFoundException('File not found or already deleted'); } // Find exact match by comparing clean URLs const exactMatch = files.find( (file: any) => this.cleanUrl(file.url) === cleanUrl, ); if (!exactMatch) { this.logger.warn(`No exact match found for: ${fileName}`); throw new NotFoundException('File not found or already deleted'); } this.logger.log(`Found file: ${fileName}`); return exactMatch; } catch (error) { if (error instanceof NotFoundException) { throw error; } this.logger.error(`Failed to get file ID for URL: ${url}`, error); throw new NotFoundException('File not found or already deleted'); } } /** * Purge CDN cache for a URL * @param url - The ImageKit file URL to purge from cache * @returns Purge result */ async purgeCdnCache(url: string): Promise { try { const cleanUrl = this.cleanUrl(url); const result = await this.imagekit.purgeCache(cleanUrl); this.logger.log(`Successfully purged CDN cache for: ${cleanUrl}`); return result; } catch (error) { this.logger.error(`Failed to purge CDN cache for URL: ${url}`, error); throw error; } } /** * Delete a file from ImageKit by URL and purge CDN cache * @param url - The ImageKit file URL * @returns Deletion result */ async deleteFileByUrl(url: string): Promise { // First, get the file ID from the URL (this will throw NotFoundException if not found) const fileDetails = await this.getFileIdByUrl(url); if (!fileDetails || !fileDetails.fileId) { throw new NotFoundException('Could not retrieve file ID from URL'); } // Delete the file using the file ID const result = await this.deleteFile(fileDetails.fileId); // Purge CDN cache to make deletion immediate try { await this.purgeCdnCache(url); this.logger.log( `Successfully deleted file and purged cache for URL: ${url}`, ); } catch { this.logger.warn( `File deleted but CDN cache purge failed for: ${url}. Cache will expire naturally.`, ); } return { ...result, deletedUrl: url, fileId: fileDetails.fileId, }; } /** * Delete a file from ImageKit (private method used internally) * @param fileId - The file ID to delete * @returns Deletion result */ private async deleteFile(fileId: string): Promise { try { const result = await this.imagekit.deleteFile(fileId); this.logger.log(`Successfully deleted file with ID: ${fileId}`); return result; } catch (error) { this.logger.error(`Failed to delete file with ID: ${fileId}`, error); throw error; } } }