File size: 7,221 Bytes
606bfb2
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
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');
    }
  }
}