File size: 6,134 Bytes
9f0826d
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
3b3ab98
 
 
 
 
 
 
 
 
 
 
 
 
 
9f0826d
 
3b3ab98
 
9f0826d
 
 
3b3ab98
 
 
 
9f0826d
3b3ab98
9f0826d
3b3ab98
9f0826d
3b3ab98
9f0826d
 
3b3ab98
 
9f0826d
 
 
 
 
 
 
3b3ab98
 
 
 
 
 
 
 
 
9f0826d
 
 
 
 
 
 
3b3ab98
 
 
9f0826d
 
 
3b3ab98
9f0826d
 
 
 
 
3b3ab98
 
9f0826d
3b3ab98
9f0826d
 
 
 
 
 
 
 
 
 
 
 
3b3ab98
9f0826d
 
 
3b3ab98
 
 
 
9f0826d
 
3b3ab98
9f0826d
 
 
3b3ab98
9f0826d
 
 
 
 
 
 
 
 
 
 
3b3ab98
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
9f0826d
 
 
 
 
 
 
 
 
 
 
3b3ab98
9f0826d
3b3ab98
 
 
 
 
 
 
 
 
 
 
 
9f0826d
 
 
 
 
 
 
 
 
3b3ab98
9f0826d
 
 
3b3ab98
9f0826d
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
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<string>('imagekitio.publicKey') || '',
      privateKey: this.configService.get<string>('imagekitio.privateKey') || '',
      urlEndpoint:
        this.configService.get<string>('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<string, string> = {
      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<any> {
    try {
      // Clean URL (remove query parameters like ?updatedAt=...)
      const cleanUrl = this.cleanUrl(url);

      const urlEndpoint = this.configService.get<string>(
        '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<any> {
    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<any> {
    // 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<any> {
    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;
    }
  }
}