Akshar2325 commited on
Commit
3b3ab98
·
1 Parent(s): 2d96a3f

feat(imagekit): add folder management and cdn cache purging

Browse files

- Organize uploads into type-specific folders (images, documents, etc.).
- Automatically purge cdn cache upon file deletion for immediate effect.

refactor(imagekit): streamline api endpoints and service logic
- Rename endpoints for clarity (`/auth` to `/signed-upload`, `/by-url`).
- Remove unused endpoints and service methods for file details and deletion by id.
- Simplify swagger documentation for controller endpoints.

fix(imagekit): make file lookup by url more robust
- Clean urls by removing query parameters before searching.
- Prevents lookup failures for urls with versioning or tracking params.

src/shared/modules/imagekitio/imagekitio.controller.ts CHANGED
@@ -2,18 +2,11 @@ import {
2
  Controller,
3
  Get,
4
  Delete,
5
- Param,
6
  Query,
7
  HttpCode,
8
  HttpStatus,
9
  } from '@nestjs/common';
10
- import {
11
- ApiTags,
12
- ApiOperation,
13
- ApiResponse,
14
- ApiParam,
15
- ApiQuery,
16
- } from '@nestjs/swagger';
17
  import { ImagekitioService } from './imagekitio.service';
18
  import { FileType } from './enums/file-type.enum';
19
 
@@ -22,97 +15,22 @@ import { FileType } from './enums/file-type.enum';
22
  export class ImagekitioController {
23
  constructor(private readonly imagekitioService: ImagekitioService) {}
24
 
25
- @Get('auth')
26
- @ApiOperation({
27
- summary: 'Get authentication parameters for client-side upload',
28
- description:
29
- 'Returns token, expire, and signature required for direct upload to ImageKit from frontend. Optionally restrict by file type.',
30
- })
31
- @ApiQuery({
32
- name: 'type',
33
- required: false,
34
- enum: FileType,
35
- description: 'File type restriction',
36
- example: FileType.IMAGE,
37
- })
38
- @ApiResponse({
39
- status: 200,
40
- description: 'Authentication parameters generated successfully',
41
- schema: {
42
- example: {
43
- token: 'unique-token-string',
44
- expire: 1234567890,
45
- signature: 'generated-signature',
46
- fileType: 'image',
47
- },
48
- },
49
- })
50
- getAuthParams(@Query('type') type?: FileType) {
51
  return this.imagekitioService.getAuthenticationParameters(type);
52
  }
53
 
54
- @Get('file-id-by-url')
55
- @ApiOperation({
56
- summary: 'Get file ID from ImageKit URL',
57
- description:
58
- 'Retrieves the file ID and details from an ImageKit URL. Useful before deleting a file.',
59
- })
60
- @ApiQuery({
61
- name: 'url',
62
- required: true,
63
- description: 'The ImageKit file URL',
64
- example:
65
- 'https://ik.imagekit.io/eihvofxmgsdfb/my-images/krishna/krishna_KvmHprPX7.jpg',
66
- })
67
- @ApiResponse({
68
- status: 200,
69
- description: 'File ID retrieved successfully',
70
- schema: {
71
- example: {
72
- fileId: '66c91e205ae88a1234d90abc',
73
- name: 'krishna.jpg',
74
- url: 'https://ik.imagekit.io/eihvofxmgsdfb/my-images/krishna/krishna_KvmHprPX7.jpg',
75
- },
76
- },
77
- })
78
- @ApiResponse({
79
- status: 404,
80
- description: 'File not found or already deleted',
81
- })
82
- async getFileIdByUrl(@Query('url') url: string): Promise<any> {
83
  return this.imagekitioService.getFileIdByUrl(url);
84
  }
85
 
86
  @Delete('by-url')
87
  @HttpCode(HttpStatus.OK)
88
- @ApiOperation({
89
- summary: 'Delete a file from ImageKit by URL',
90
- description:
91
- 'Deletes a file from ImageKit storage using the file URL. This endpoint first retrieves the file ID from the URL, then deletes the file.',
92
- })
93
- @ApiQuery({
94
- name: 'url',
95
- required: true,
96
- description: 'The ImageKit file URL to delete',
97
- example:
98
- 'https://ik.imagekit.io/eihvofxmgsdfb/my-images/krishna/krishna_KvmHprPX7.jpg',
99
- })
100
- @ApiResponse({
101
- status: 200,
102
- description: 'File deleted successfully',
103
- schema: {
104
- example: {
105
- message: 'File deleted successfully',
106
- deletedUrl:
107
- 'https://ik.imagekit.io/eihvofxmgsdfb/my-images/krishna/krishna_KvmHprPX7.jpg',
108
- fileId: '66c91e205ae88a1234d90abc',
109
- },
110
- },
111
- })
112
- @ApiResponse({
113
- status: 404,
114
- description: 'File not found or already deleted',
115
- })
116
  async deleteFileByUrl(@Query('url') url: string) {
117
  const result = await this.imagekitioService.deleteFileByUrl(url);
118
  return {
@@ -120,49 +38,4 @@ export class ImagekitioController {
120
  ...result,
121
  };
122
  }
123
-
124
- @Delete(':fileId')
125
- @HttpCode(HttpStatus.OK)
126
- @ApiOperation({
127
- summary: 'Delete a file from ImageKit by file ID',
128
- description: 'Deletes a file from ImageKit storage using the file ID',
129
- })
130
- @ApiParam({
131
- name: 'fileId',
132
- description: 'The ImageKit file ID to delete',
133
- example: '5f8e7d6c5b4a3f2e1d0c9b8a',
134
- })
135
- @ApiResponse({
136
- status: 200,
137
- description: 'File deleted successfully',
138
- })
139
- @ApiResponse({
140
- status: 404,
141
- description: 'File not found',
142
- })
143
- async deleteFile(@Param('fileId') fileId: string) {
144
- await this.imagekitioService.deleteFile(fileId);
145
- return {
146
- message: 'File deleted successfully',
147
- fileId,
148
- };
149
- }
150
-
151
- @Get('file/:fileId')
152
- @ApiOperation({
153
- summary: 'Get file details by file ID',
154
- description: 'Retrieves details of a specific file from ImageKit',
155
- })
156
- @ApiParam({
157
- name: 'fileId',
158
- description: 'The ImageKit file ID',
159
- example: '5f8e7d6c5b4a3f2e1d0c9b8a',
160
- })
161
- @ApiResponse({
162
- status: 200,
163
- description: 'File details retrieved successfully',
164
- })
165
- async getFileDetails(@Param('fileId') fileId: string): Promise<any> {
166
- return this.imagekitioService.getFileDetails(fileId);
167
- }
168
  }
 
2
  Controller,
3
  Get,
4
  Delete,
 
5
  Query,
6
  HttpCode,
7
  HttpStatus,
8
  } from '@nestjs/common';
9
+ import { ApiTags, ApiOperation, ApiQuery } from '@nestjs/swagger';
 
 
 
 
 
 
10
  import { ImagekitioService } from './imagekitio.service';
11
  import { FileType } from './enums/file-type.enum';
12
 
 
15
  export class ImagekitioController {
16
  constructor(private readonly imagekitioService: ImagekitioService) {}
17
 
18
+ @Get('signed-upload')
19
+ @ApiOperation({ summary: 'Get signed upload parameters' })
20
+ @ApiQuery({ name: 'type', enum: FileType, required: false })
21
+ getSignedUploadParams(@Query('type') type?: FileType) {
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
22
  return this.imagekitioService.getAuthenticationParameters(type);
23
  }
24
 
25
+ @Get('by-url')
26
+ @ApiOperation({ summary: 'Get file details by URL' })
27
+ async getFileByUrl(@Query('url') url: string): Promise<any> {
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
28
  return this.imagekitioService.getFileIdByUrl(url);
29
  }
30
 
31
  @Delete('by-url')
32
  @HttpCode(HttpStatus.OK)
33
+ @ApiOperation({ summary: 'Delete file by URL' })
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
34
  async deleteFileByUrl(@Query('url') url: string) {
35
  const result = await this.imagekitioService.deleteFileByUrl(url);
36
  return {
 
38
  ...result,
39
  };
40
  }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
41
  }
src/shared/modules/imagekitio/imagekitio.service.ts CHANGED
@@ -16,20 +16,41 @@ export class ImagekitioService {
16
  });
17
  }
18
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
19
  /**
20
  * Get authentication parameters for client-side upload
21
- * @param fileType - Optional file type restriction (image, document, video, audio, all)
22
- * @returns Authentication parameters (token, expire, signature)
23
  */
24
  getAuthenticationParameters(fileType?: string) {
25
  try {
 
 
 
 
26
  const authParams = this.imagekit.getAuthenticationParameters();
 
27
  this.logger.log(
28
- `Generated authentication parameters for ImageKit upload${fileType && fileType !== 'all' ? ` (type: ${fileType})` : ''}`,
29
  );
 
30
  return {
31
  ...authParams,
32
- ...(fileType && fileType !== 'all' && { fileType }),
 
33
  };
34
  } catch (error) {
35
  this.logger.error('Failed to generate authentication parameters', error);
@@ -37,6 +58,15 @@ export class ImagekitioService {
37
  }
38
  }
39
 
 
 
 
 
 
 
 
 
 
40
  /**
41
  * Get file ID from ImageKit URL
42
  * @param url - The ImageKit file URL
@@ -44,21 +74,22 @@ export class ImagekitioService {
44
  */
45
  async getFileIdByUrl(url: string): Promise<any> {
46
  try {
47
- // Extract the file path from the URL
48
- // Example: https://ik.imagekit.io/eihvofxmgsdfb/krishna_QnwprENpT.jpg
49
- // Extract: /krishna_QnwprENpT.jpg
50
  const urlEndpoint = this.configService.get<string>(
51
  'imagekitio.urlEndpoint',
52
  );
53
- if (!url.startsWith(urlEndpoint || '')) {
54
  throw new NotFoundException(
55
  'Invalid ImageKit URL - does not match your URL endpoint',
56
  );
57
  }
58
 
59
- const filePath = url.replace(urlEndpoint || '', '');
 
60
 
61
- // Extract filename from path
62
  const fileName = filePath.split('/').pop() || '';
63
 
64
  if (!fileName) {
@@ -71,24 +102,21 @@ export class ImagekitioService {
71
  });
72
 
73
  if (!files || files.length === 0) {
74
- this.logger.warn(
75
- `File not found with URL: ${url} - it may have been already deleted`,
76
- );
77
  throw new NotFoundException('File not found or already deleted');
78
  }
79
 
80
- // Find the exact match by comparing the full URL
81
- const exactMatch = files.find((file: any) => file.url === url);
 
 
82
 
83
  if (!exactMatch) {
84
- // If no exact match, the file might have been deleted
85
- this.logger.warn(
86
- `No exact URL match found for: ${url} - file may have been already deleted`,
87
- );
88
  throw new NotFoundException('File not found or already deleted');
89
  }
90
 
91
- this.logger.log(`Found file ID for URL: ${url}`);
92
  return exactMatch;
93
  } catch (error) {
94
  if (error instanceof NotFoundException) {
@@ -100,7 +128,24 @@ export class ImagekitioService {
100
  }
101
 
102
  /**
103
- * Delete a file from ImageKit by URL
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
104
  * @param url - The ImageKit file URL
105
  * @returns Deletion result
106
  */
@@ -112,9 +157,20 @@ export class ImagekitioService {
112
  throw new NotFoundException('Could not retrieve file ID from URL');
113
  }
114
 
115
- // Then delete the file using the file ID
116
  const result = await this.deleteFile(fileDetails.fileId);
117
- this.logger.log(`Successfully deleted file from URL: ${url}`);
 
 
 
 
 
 
 
 
 
 
 
118
 
119
  return {
120
  ...result,
@@ -124,11 +180,11 @@ export class ImagekitioService {
124
  }
125
 
126
  /**
127
- * Delete a file from ImageKit
128
  * @param fileId - The file ID to delete
129
  * @returns Deletion result
130
  */
131
- async deleteFile(fileId: string): Promise<any> {
132
  try {
133
  const result = await this.imagekit.deleteFile(fileId);
134
  this.logger.log(`Successfully deleted file with ID: ${fileId}`);
@@ -138,36 +194,4 @@ export class ImagekitioService {
138
  throw error;
139
  }
140
  }
141
-
142
- /**
143
- * Get file details from ImageKit
144
- * @param fileId - The file ID
145
- * @returns File details
146
- */
147
- async getFileDetails(fileId: string): Promise<any> {
148
- try {
149
- const fileDetails = await this.imagekit.getFileDetails(fileId);
150
- return fileDetails;
151
- } catch (error) {
152
- this.logger.error(`Failed to get file details for ID: ${fileId}`, error);
153
- throw error;
154
- }
155
- }
156
-
157
- /**
158
- * List files from ImageKit
159
- * @param options - List options (skip, limit, etc.)
160
- * @returns List of files
161
- */
162
- async listFiles(
163
- options: { skip?: number; limit?: number } = {},
164
- ): Promise<any> {
165
- try {
166
- const files = await this.imagekit.listFiles(options);
167
- return files;
168
- } catch (error) {
169
- this.logger.error('Failed to list files', error);
170
- throw error;
171
- }
172
- }
173
  }
 
16
  });
17
  }
18
 
19
+ /**
20
+ * Get folder path based on file type
21
+ * @param fileType - The file type (IMAGE, DOCUMENT, VIDEO)
22
+ * @returns Folder path for the file type
23
+ */
24
+ private getFolderPath(fileType: string): string {
25
+ const folderMap: Record<string, string> = {
26
+ IMAGE: 'images',
27
+ DOCUMENT: 'documents',
28
+ VIDEO: 'videos',
29
+ };
30
+ return folderMap[fileType.toUpperCase()] || 'others';
31
+ }
32
+
33
  /**
34
  * Get authentication parameters for client-side upload
35
+ * @param fileType - Optional file type restriction (IMAGE, DOCUMENT, VIDEO)
36
+ * @returns Authentication parameters (token, expire, signature, folder)
37
  */
38
  getAuthenticationParameters(fileType?: string) {
39
  try {
40
+ // Determine folder based on file type
41
+ const folder = fileType ? this.getFolderPath(fileType) : 'others';
42
+
43
+ // Generate auth params
44
  const authParams = this.imagekit.getAuthenticationParameters();
45
+
46
  this.logger.log(
47
+ `Generated authentication parameters for ImageKit upload${fileType ? ` (type: ${fileType}, folder: ${folder})` : ''}`,
48
  );
49
+
50
  return {
51
  ...authParams,
52
+ folder, // Folder where file should be uploaded
53
+ ...(fileType && { fileType }),
54
  };
55
  } catch (error) {
56
  this.logger.error('Failed to generate authentication parameters', error);
 
58
  }
59
  }
60
 
61
+ /**
62
+ * Clean URL by removing query parameters
63
+ * @param url - The URL to clean
64
+ * @returns Clean URL without query parameters
65
+ */
66
+ private cleanUrl(url: string): string {
67
+ return url.split('?')[0];
68
+ }
69
+
70
  /**
71
  * Get file ID from ImageKit URL
72
  * @param url - The ImageKit file URL
 
74
  */
75
  async getFileIdByUrl(url: string): Promise<any> {
76
  try {
77
+ // Clean URL (remove query parameters like ?updatedAt=...)
78
+ const cleanUrl = this.cleanUrl(url);
79
+
80
  const urlEndpoint = this.configService.get<string>(
81
  'imagekitio.urlEndpoint',
82
  );
83
+ if (!cleanUrl.startsWith(urlEndpoint || '')) {
84
  throw new NotFoundException(
85
  'Invalid ImageKit URL - does not match your URL endpoint',
86
  );
87
  }
88
 
89
+ // Extract file path from URL
90
+ const filePath = cleanUrl.replace(urlEndpoint || '', '');
91
 
92
+ // Extract filename from path (last part after /)
93
  const fileName = filePath.split('/').pop() || '';
94
 
95
  if (!fileName) {
 
102
  });
103
 
104
  if (!files || files.length === 0) {
105
+ this.logger.warn(`File not found: ${fileName}`);
 
 
106
  throw new NotFoundException('File not found or already deleted');
107
  }
108
 
109
+ // Find exact match by comparing clean URLs
110
+ const exactMatch = files.find(
111
+ (file: any) => this.cleanUrl(file.url) === cleanUrl,
112
+ );
113
 
114
  if (!exactMatch) {
115
+ this.logger.warn(`No exact match found for: ${fileName}`);
 
 
 
116
  throw new NotFoundException('File not found or already deleted');
117
  }
118
 
119
+ this.logger.log(`Found file: ${fileName}`);
120
  return exactMatch;
121
  } catch (error) {
122
  if (error instanceof NotFoundException) {
 
128
  }
129
 
130
  /**
131
+ * Purge CDN cache for a URL
132
+ * @param url - The ImageKit file URL to purge from cache
133
+ * @returns Purge result
134
+ */
135
+ async purgeCdnCache(url: string): Promise<any> {
136
+ try {
137
+ const cleanUrl = this.cleanUrl(url);
138
+ const result = await this.imagekit.purgeCache(cleanUrl);
139
+ this.logger.log(`Successfully purged CDN cache for: ${cleanUrl}`);
140
+ return result;
141
+ } catch (error) {
142
+ this.logger.error(`Failed to purge CDN cache for URL: ${url}`, error);
143
+ throw error;
144
+ }
145
+ }
146
+
147
+ /**
148
+ * Delete a file from ImageKit by URL and purge CDN cache
149
  * @param url - The ImageKit file URL
150
  * @returns Deletion result
151
  */
 
157
  throw new NotFoundException('Could not retrieve file ID from URL');
158
  }
159
 
160
+ // Delete the file using the file ID
161
  const result = await this.deleteFile(fileDetails.fileId);
162
+
163
+ // Purge CDN cache to make deletion immediate
164
+ try {
165
+ await this.purgeCdnCache(url);
166
+ this.logger.log(
167
+ `Successfully deleted file and purged cache for URL: ${url}`,
168
+ );
169
+ } catch {
170
+ this.logger.warn(
171
+ `File deleted but CDN cache purge failed for: ${url}. Cache will expire naturally.`,
172
+ );
173
+ }
174
 
175
  return {
176
  ...result,
 
180
  }
181
 
182
  /**
183
+ * Delete a file from ImageKit (private method used internally)
184
  * @param fileId - The file ID to delete
185
  * @returns Deletion result
186
  */
187
+ private async deleteFile(fileId: string): Promise<any> {
188
  try {
189
  const result = await this.imagekit.deleteFile(fileId);
190
  this.logger.log(`Successfully deleted file with ID: ${fileId}`);
 
194
  throw error;
195
  }
196
  }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
197
  }