@ackplus/nest-file-storage 1.1.22 → 2.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (86) hide show
  1. package/CHANGELOG.md +72 -0
  2. package/MIGRATION.md +220 -0
  3. package/README.md +353 -547
  4. package/dist/index.d.ts +1 -4
  5. package/dist/index.js +1 -4
  6. package/dist/index.js.map +1 -1
  7. package/dist/lib/constants.d.ts +3 -1
  8. package/dist/lib/constants.js +4 -2
  9. package/dist/lib/constants.js.map +1 -1
  10. package/dist/lib/driver-registry.d.ts +34 -0
  11. package/dist/lib/driver-registry.js +118 -0
  12. package/dist/lib/driver-registry.js.map +1 -0
  13. package/dist/lib/drivers/azure.driver.d.ts +21 -0
  14. package/dist/lib/drivers/azure.driver.js +91 -0
  15. package/dist/lib/drivers/azure.driver.js.map +1 -0
  16. package/dist/lib/drivers/driver.interface.d.ts +40 -0
  17. package/dist/lib/drivers/driver.interface.js +3 -0
  18. package/dist/lib/drivers/driver.interface.js.map +1 -0
  19. package/dist/lib/drivers/driver.util.d.ts +2 -0
  20. package/dist/lib/drivers/driver.util.js +15 -0
  21. package/dist/lib/drivers/driver.util.js.map +1 -0
  22. package/dist/lib/drivers/index.d.ts +7 -0
  23. package/dist/lib/drivers/index.js +39 -0
  24. package/dist/lib/drivers/index.js.map +1 -0
  25. package/dist/lib/drivers/local.driver.d.ts +15 -0
  26. package/dist/lib/drivers/local.driver.js +110 -0
  27. package/dist/lib/drivers/local.driver.js.map +1 -0
  28. package/dist/lib/drivers/s3.driver.d.ts +22 -0
  29. package/dist/lib/drivers/s3.driver.js +103 -0
  30. package/dist/lib/drivers/s3.driver.js.map +1 -0
  31. package/dist/lib/file-storage.service.d.ts +16 -5
  32. package/dist/lib/file-storage.service.js +60 -22
  33. package/dist/lib/file-storage.service.js.map +1 -1
  34. package/dist/lib/index.d.ts +9 -2
  35. package/dist/lib/index.js +15 -2
  36. package/dist/lib/index.js.map +1 -1
  37. package/dist/lib/interceptor/file-storage.interceptor.d.ts +7 -10
  38. package/dist/lib/interceptor/file-storage.interceptor.js +119 -112
  39. package/dist/lib/interceptor/file-storage.interceptor.js.map +1 -1
  40. package/dist/lib/multer/driver-multer-engine.d.ts +18 -0
  41. package/dist/lib/multer/driver-multer-engine.js +91 -0
  42. package/dist/lib/multer/driver-multer-engine.js.map +1 -0
  43. package/dist/lib/nest-file-storage.module.d.ts +3 -3
  44. package/dist/lib/nest-file-storage.module.js +81 -44
  45. package/dist/lib/nest-file-storage.module.js.map +1 -1
  46. package/dist/lib/registry-holder.d.ts +6 -0
  47. package/dist/lib/registry-holder.js +26 -0
  48. package/dist/lib/registry-holder.js.map +1 -0
  49. package/dist/lib/tenant/tenant-from.d.ts +14 -0
  50. package/dist/lib/tenant/tenant-from.js +71 -0
  51. package/dist/lib/tenant/tenant-from.js.map +1 -0
  52. package/dist/lib/tenant/tenant.types.d.ts +20 -0
  53. package/dist/lib/tenant/tenant.types.js +3 -0
  54. package/dist/lib/tenant/tenant.types.js.map +1 -0
  55. package/dist/lib/types.d.ts +45 -35
  56. package/dist/lib/types.js.map +1 -1
  57. package/dist/lib/validation.d.ts +22 -0
  58. package/dist/lib/validation.js +98 -0
  59. package/dist/lib/validation.js.map +1 -0
  60. package/dist/tsconfig.build.tsbuildinfo +1 -1
  61. package/examples/1-basic-local-storage.example.ts +11 -7
  62. package/examples/10-testing.example.ts +60 -196
  63. package/examples/11-custom-driver.example.ts +82 -0
  64. package/examples/12-multi-tenant.example.ts +93 -0
  65. package/examples/2-s3-storage.example.ts +18 -16
  66. package/examples/3-azure-storage.example.ts +14 -12
  67. package/examples/4-upload-controller.example.ts +20 -55
  68. package/examples/5-custom-configuration.example.ts +37 -57
  69. package/examples/6-file-service.example.ts +34 -91
  70. package/examples/7-user-avatar.example.ts +37 -92
  71. package/examples/8-document-management.example.ts +45 -196
  72. package/examples/9-dynamic-storage.example.ts +29 -147
  73. package/examples/README.md +25 -107
  74. package/package.json +17 -4
  75. package/dist/lib/storage/azure.storage.d.ts +0 -18
  76. package/dist/lib/storage/azure.storage.js +0 -210
  77. package/dist/lib/storage/azure.storage.js.map +0 -1
  78. package/dist/lib/storage/local.storage.d.ts +0 -20
  79. package/dist/lib/storage/local.storage.js +0 -212
  80. package/dist/lib/storage/local.storage.js.map +0 -1
  81. package/dist/lib/storage/s3.storage.d.ts +0 -19
  82. package/dist/lib/storage/s3.storage.js +0 -241
  83. package/dist/lib/storage/s3.storage.js.map +0 -1
  84. package/dist/lib/storage.factory.d.ts +0 -8
  85. package/dist/lib/storage.factory.js +0 -46
  86. package/dist/lib/storage.factory.js.map +0 -1
@@ -1,53 +1,40 @@
1
1
  /**
2
2
  * Example 4: File Upload Controller
3
- *
4
- * This example demonstrates different file upload scenarios using the FileStorageInterceptor.
3
+ *
4
+ * Different upload shapes with the FileStorageInterceptor, plus declarative validation.
5
5
  */
6
6
 
7
- import { Controller, Post, UseInterceptors, Body, BadRequestException } from '@nestjs/common';
7
+ import { Controller, Post, UseInterceptors, Body } from '@nestjs/common';
8
8
  import { FileStorageInterceptor } from '@ackplus/nest-file-storage';
9
9
 
10
10
  @Controller('upload')
11
11
  export class UploadController {
12
12
  /**
13
- * Single file upload
14
- * POST /upload/single
15
- * Form field: "file"
13
+ * Single file upload — POST /upload/single, form field "file".
16
14
  */
17
15
  @Post('single')
18
16
  @UseInterceptors(FileStorageInterceptor('file'))
19
17
  uploadSingle(@Body() body: any) {
20
- return {
21
- message: 'File uploaded successfully',
22
- fileKey: body.file, // File key is automatically added to body
23
- };
18
+ return { message: 'File uploaded successfully', fileKey: body.file };
24
19
  }
25
20
 
26
21
  /**
27
- * Multiple files upload (same field name)
28
- * POST /upload/multiple
29
- * Form field: "files" (multiple files)
22
+ * Multiple files in one field — POST /upload/multiple, form field "files".
30
23
  */
31
24
  @Post('multiple')
32
25
  @UseInterceptors(
33
- FileStorageInterceptor({
34
- type: 'array',
35
- fieldName: 'files',
36
- maxCount: 10, // Maximum 10 files
37
- })
26
+ FileStorageInterceptor({ type: 'array', fieldName: 'files', maxCount: 10 })
38
27
  )
39
28
  uploadMultiple(@Body() body: any) {
40
29
  return {
41
30
  message: 'Files uploaded successfully',
42
- fileKeys: body.files, // Array of file keys
31
+ fileKeys: body.files, // string[]
43
32
  count: body.files.length,
44
33
  };
45
34
  }
46
35
 
47
36
  /**
48
- * Multiple fields with different files
49
- * POST /upload/fields
50
- * Form fields: "avatar" (1 file), "photos" (up to 5 files)
37
+ * Multiple named fields POST /upload/fields, fields "avatar" and "photos".
51
38
  */
52
39
  @Post('fields')
53
40
  @UseInterceptors(
@@ -62,56 +49,34 @@ export class UploadController {
62
49
  uploadFields(@Body() body: any) {
63
50
  return {
64
51
  message: 'Files uploaded successfully',
65
- avatar: body.avatar, // Single file key
66
- photos: body.photos, // Array of file keys
52
+ avatar: body.avatar, // string[] (always an array in 'fields' mode)
53
+ photos: body.photos, // string[]
67
54
  };
68
55
  }
69
56
 
70
57
  /**
71
- * Upload with custom file information
72
- * Returns full file object instead of just the key
58
+ * Return the full file object instead of just the key.
73
59
  */
74
60
  @Post('with-details')
75
- @UseInterceptors(
76
- FileStorageInterceptor('file', {
77
- mapToRequestBody: (file) => {
78
- // Return the full file object
79
- return file;
80
- },
81
- })
82
- )
61
+ @UseInterceptors(FileStorageInterceptor('file', { mapToRequestBody: (file) => file }))
83
62
  uploadWithDetails(@Body() body: any) {
84
- return {
85
- message: 'File uploaded successfully',
86
- file: body.file, // Full file object with key, url, size, etc.
87
- };
63
+ return { message: 'File uploaded successfully', file: body.file };
88
64
  }
89
65
 
90
66
  /**
91
- * Upload with validation
67
+ * Declarative validation (v2) — type + size checks become typed 400s.
92
68
  */
93
69
  @Post('image')
94
70
  @UseInterceptors(
95
71
  FileStorageInterceptor('image', {
96
- fileName: (file) => {
97
- // Validate file type
98
- const allowedTypes = ['image/jpeg', 'image/png', 'image/gif', 'image/webp'];
99
- if (!allowedTypes.includes(file.mimetype)) {
100
- throw new BadRequestException('Only image files are allowed');
101
- }
102
-
103
- // Generate filename with timestamp
104
- const timestamp = Date.now();
105
- const ext = file.originalname.split('.').pop();
106
- return `image-${timestamp}.${ext}`;
72
+ validation: {
73
+ allowedMimeTypes: ['image/jpeg', 'image/png', 'image/gif', 'image/webp'],
74
+ maxSize: 5 * 1024 * 1024, // 5 MB
107
75
  },
76
+ fileName: (file) => `image-${Date.now()}.${file.originalname.split('.').pop()}`,
108
77
  })
109
78
  )
110
79
  uploadImage(@Body() body: any) {
111
- return {
112
- message: 'Image uploaded successfully',
113
- imageKey: body.image,
114
- };
80
+ return { message: 'Image uploaded successfully', imageKey: body.image };
115
81
  }
116
82
  }
117
-
@@ -1,56 +1,40 @@
1
1
  /**
2
- * Example 5: Custom Configuration
3
- *
4
- * This example shows advanced configuration options including custom file naming,
5
- * directory structure, and file transformations.
2
+ * Example 5: Custom Key Generation
3
+ *
4
+ * Set default fileName/fileDist on the driver so every upload is organized the same way.
5
+ * Per-route overrides (on the interceptor) still take precedence.
6
6
  */
7
7
 
8
8
  import { Module } from '@nestjs/common';
9
- import { NestFileStorageModule, FileStorageEnum } from '@ackplus/nest-file-storage';
9
+ import { NestFileStorageModule, localDriver } from '@ackplus/nest-file-storage';
10
10
  import { v4 as uuidv4 } from 'uuid';
11
11
  import * as path from 'path';
12
12
 
13
13
  @Module({
14
14
  imports: [
15
15
  NestFileStorageModule.forRoot({
16
- storage: FileStorageEnum.LOCAL,
17
- localConfig: {
18
- rootPath: './uploads',
19
- baseUrl: 'http://localhost:3000/uploads',
20
-
21
- // Custom file naming function
22
- fileName: (file, req) => {
23
- // Generate unique filename: uuid-originalname.ext
24
- const uuid = uuidv4();
25
- const ext = path.extname(file.originalname);
26
- const name = path.basename(file.originalname, ext);
27
- return `${uuid}-${name}${ext}`;
28
- },
29
-
30
- // Custom directory structure: year/month/day
31
- fileDist: (file, req) => {
32
- const date = new Date();
33
- const year = date.getFullYear();
34
- const month = String(date.getMonth() + 1).padStart(2, '0');
35
- const day = String(date.getDate()).padStart(2, '0');
36
-
37
- // Optional: organize by file type
38
- const isImage = file.mimetype?.startsWith('image/');
39
- const type = isImage ? 'images' : 'documents';
40
-
41
- return path.join(type, String(year), month, day);
42
- },
43
-
44
- // Transform uploaded file object (return only necessary fields)
45
- transformUploadedFileObject: (file) => {
46
- return {
47
- key: file.key,
48
- url: file.url,
49
- size: file.size,
50
- mimetype: file.mimetype,
51
- originalName: file.originalName,
52
- };
53
- },
16
+ default: 'local',
17
+ drivers: {
18
+ local: localDriver({
19
+ rootPath: './uploads',
20
+ baseUrl: 'http://localhost:3000/uploads',
21
+
22
+ // Default filename: uuid-originalname.ext
23
+ fileName: (file) => {
24
+ const ext = path.extname(file.originalname);
25
+ const name = path.basename(file.originalname, ext);
26
+ return `${uuidv4()}-${name}${ext}`;
27
+ },
28
+
29
+ // Default directory (relative): <type>/year/month/day
30
+ fileDist: (file) => {
31
+ const d = new Date();
32
+ const type = file.mimetype?.startsWith('image/') ? 'images' : 'documents';
33
+ const mm = String(d.getMonth() + 1).padStart(2, '0');
34
+ const dd = String(d.getDate()).padStart(2, '0');
35
+ return path.posix.join(type, String(d.getFullYear()), mm, dd);
36
+ },
37
+ }),
54
38
  },
55
39
  }),
56
40
  ],
@@ -58,18 +42,14 @@ import * as path from 'path';
58
42
  export class AppModule {}
59
43
 
60
44
  /**
61
- * Result directory structure:
62
- * uploads/
63
- * ├── images/
64
- * │ └── 2024/
65
- * │ └── 01/
66
- * │ └── 15/
67
- * │ ├── uuid1-photo1.jpg
68
- * │ └── uuid2-photo2.png
69
- * └── documents/
70
- * └── 2024/
71
- * └── 01/
72
- * └── 15/
73
- * └── uuid3-document.pdf
45
+ * Resulting keys:
46
+ * images/2026/06/12/<uuid>-photo.jpg
47
+ * documents/2026/06/12/<uuid>-report.pdf
48
+ *
49
+ * Note (v2): the v1 `transformUploadedFileObject` hook is removed. To reshape what lands in
50
+ * the controller body, use the interceptor's `mapToRequestBody`:
51
+ *
52
+ * FileStorageInterceptor('file', {
53
+ * mapToRequestBody: (file) => ({ key: file.key, url: file.url, size: file.size }),
54
+ * })
74
55
  */
75
-
@@ -1,8 +1,7 @@
1
1
  /**
2
2
  * Example 6: File Service
3
- *
4
- * This example demonstrates how to use FileStorageService to perform
5
- * file operations programmatically.
3
+ *
4
+ * Use the injectable FileStorageService to work with files programmatically.
6
5
  */
7
6
 
8
7
  import { Injectable, NotFoundException } from '@nestjs/common';
@@ -11,114 +10,58 @@ import * as fs from 'fs';
11
10
 
12
11
  @Injectable()
13
12
  export class FileService {
14
- /**
15
- * Get file content as Buffer
16
- */
13
+ // v2: inject the service (no more static FileStorageService.getStorage()).
14
+ constructor(private readonly fileStorage: FileStorageService) {}
15
+
16
+ /** Read file content as a Buffer. */
17
17
  async getFile(key: string): Promise<Buffer> {
18
18
  try {
19
- const storage = await FileStorageService.getStorage();
20
- return await storage.getFile(key);
21
- } catch (error) {
19
+ return await this.fileStorage.getFile(key);
20
+ } catch {
22
21
  throw new NotFoundException(`File not found: ${key}`);
23
22
  }
24
23
  }
25
24
 
26
- /**
27
- * Delete a file
28
- */
29
- async deleteFile(key: string): Promise<void> {
30
- const storage = await FileStorageService.getStorage();
31
- await storage.deleteFile(key);
25
+ /** Delete a file. */
26
+ deleteFile(key: string): Promise<void> {
27
+ return this.fileStorage.deleteFile(key);
32
28
  }
33
29
 
34
- /**
35
- * Copy a file to a new location
36
- */
37
- async copyFile(oldKey: string, newKey: string) {
38
- const storage = await FileStorageService.getStorage();
39
- return await storage.copyFile(oldKey, newKey);
30
+ /** Copy a file to a new key. */
31
+ copyFile(oldKey: string, newKey: string) {
32
+ return this.fileStorage.copyFile(oldKey, newKey);
40
33
  }
41
34
 
42
- /**
43
- * Upload a file from local filesystem
44
- */
35
+ /** Upload a file from the local filesystem. */
45
36
  async uploadFromLocal(localPath: string, storageKey: string) {
46
- const fileBuffer = await fs.promises.readFile(localPath);
47
- const storage = await FileStorageService.getStorage();
48
- return await storage.putFile(fileBuffer, storageKey);
49
- }
50
-
51
- /**
52
- * Get public URL for a file
53
- */
54
- async getFileUrl(key: string): Promise<string> {
55
- const storage = await FileStorageService.getStorage();
56
- return storage.getUrl(key);
37
+ const buffer = await fs.promises.readFile(localPath);
38
+ return this.fileStorage.putFile(buffer, storageKey);
57
39
  }
58
40
 
59
- /**
60
- * Get signed URL (for S3)
61
- * Useful for temporary access to private files
62
- */
63
- async getSignedUrl(key: string, expiresIn: number = 3600): Promise<string> {
64
- const storage = await FileStorageService.getStorage();
65
-
66
- // Check if storage supports signed URLs (S3)
67
- if ('getSignedUrl' in storage) {
68
- return await storage.getSignedUrl(key, { expiresIn });
69
- }
70
-
71
- // Fallback to regular URL for other storage types
72
- return storage.getUrl(key);
41
+ /** Public URL for a key. */
42
+ getFileUrl(key: string): Promise<string> {
43
+ return this.fileStorage.getUrl(key);
73
44
  }
74
45
 
75
- /**
76
- * Upload file from Buffer
77
- */
78
- async uploadBuffer(buffer: Buffer, key: string, mimetype?: string) {
79
- const storage = await FileStorageService.getStorage();
80
- const result = await storage.putFile(buffer, key);
81
-
82
- return {
83
- key: result.key,
84
- url: result.url,
85
- size: result.size,
86
- mimetype,
87
- };
46
+ /** Time-limited signed URL (S3/Azure); falls back to the public URL otherwise. */
47
+ getSignedUrl(key: string, expiresIn = 3600): Promise<string> {
48
+ return this.fileStorage.getSignedUrl(key, { expiresIn });
88
49
  }
89
50
 
90
- /**
91
- * Check if file exists
92
- */
93
- async fileExists(key: string): Promise<boolean> {
94
- try {
95
- const storage = await FileStorageService.getStorage();
96
- await storage.getFile(key);
97
- return true;
98
- } catch (error) {
99
- return false;
100
- }
51
+ /** Upload from a Buffer. */
52
+ async uploadBuffer(buffer: Buffer, key: string, contentType?: string) {
53
+ const result = await this.fileStorage.putFile(buffer, key, { contentType });
54
+ return { key: result.key, url: result.url, size: result.size };
101
55
  }
102
56
 
103
- /**
104
- * Delete multiple files
105
- */
106
- async deleteMultipleFiles(keys: string[]): Promise<void> {
107
- const storage = await FileStorageService.getStorage();
108
- await Promise.all(keys.map(key => storage.deleteFile(key)));
57
+ /** Resolve a specific (non-default) driver by name. */
58
+ async uploadToS3(buffer: Buffer, key: string) {
59
+ const s3 = await this.fileStorage.getDriver('s3');
60
+ return s3.putFile(buffer, key);
109
61
  }
110
62
 
111
- /**
112
- * Get file path (Local storage only)
113
- */
114
- async getFilePath(key: string): Promise<string | undefined> {
115
- const storage = await FileStorageService.getStorage();
116
-
117
- if ('path' in storage) {
118
- return storage.path(key);
119
- }
120
-
121
- return undefined;
63
+ /** Delete several files. */
64
+ deleteMany(keys: string[]): Promise<void[]> {
65
+ return Promise.all(keys.map((key) => this.fileStorage.deleteFile(key)));
122
66
  }
123
67
  }
124
-
@@ -1,139 +1,84 @@
1
1
  /**
2
2
  * Example 7: User Avatar Upload
3
- *
4
- * This example demonstrates a complete user avatar upload feature with
5
- * validation, old avatar cleanup, and database update.
3
+ *
4
+ * A complete avatar feature: declarative validation, old-avatar cleanup, and a DB update.
6
5
  */
7
6
 
8
- import {
9
- Controller,
10
- Post,
11
- UseInterceptors,
12
- Body,
13
- Request,
14
- BadRequestException,
15
- UseGuards,
16
- } from '@nestjs/common';
7
+ import { Controller, Post, UseInterceptors, Body, Request, BadRequestException, UseGuards } from '@nestjs/common';
17
8
  import { FileStorageInterceptor, FileStorageService } from '@ackplus/nest-file-storage';
18
- import { JwtAuthGuard } from './auth/jwt-auth.guard'; // Your auth guard
9
+ import { JwtAuthGuard } from './auth/jwt-auth.guard'; // your auth guard
19
10
 
20
- // User entity interface
21
11
  interface User {
22
12
  id: number;
23
13
  email: string;
24
- avatarKey?: string;
25
- avatarUrl?: string;
14
+ avatarKey?: string | null;
15
+ avatarUrl?: string | null;
26
16
  }
27
17
 
28
- // User service (example)
29
18
  class UserService {
30
- async findById(id: number): Promise<User> {
31
- // Your database query
32
- return {} as User;
19
+ async findById(_id: number): Promise<User> {
20
+ return {} as User; // your DB query
33
21
  }
34
-
35
- async updateAvatar(id: number, avatarKey: string, avatarUrl: string): Promise<User> {
36
- // Your database update
37
- return {} as User;
22
+ async updateAvatar(_id: number, _avatarKey: string | null, _avatarUrl: string | null): Promise<User> {
23
+ return {} as User; // your DB update
38
24
  }
39
25
  }
40
26
 
41
27
  @Controller('users')
42
28
  export class UserAvatarController {
43
- constructor(private readonly userService: UserService) {}
29
+ // v2: inject the service.
30
+ constructor(
31
+ private readonly userService: UserService,
32
+ private readonly fileStorage: FileStorageService,
33
+ ) {}
44
34
 
45
35
  @Post('avatar')
46
- @UseGuards(JwtAuthGuard) // Require authentication
36
+ @UseGuards(JwtAuthGuard)
47
37
  @UseInterceptors(
48
38
  FileStorageInterceptor('avatar', {
49
- fileName: (file, req) => {
50
- // Validate file type
51
- const allowedTypes = ['image/jpeg', 'image/png', 'image/gif', 'image/webp'];
52
- if (!allowedTypes.includes(file.mimetype)) {
53
- throw new BadRequestException(
54
- 'Invalid file type. Only JPEG, PNG, GIF, and WebP are allowed.'
55
- );
56
- }
57
-
58
- // Validate file size (5MB max)
59
- const maxSize = 5 * 1024 * 1024; // 5MB
60
- if (file.size > maxSize) {
61
- throw new BadRequestException('File size must be less than 5MB');
62
- }
63
-
64
- // Generate filename: avatar-{userId}-{timestamp}.{ext}
65
- const userId = req.user.id;
66
- const timestamp = Date.now();
67
- const ext = file.originalname.split('.').pop();
68
- return `avatar-${userId}-${timestamp}.${ext}`;
39
+ // Validation is declarative now — not thrown inside fileName.
40
+ validation: {
41
+ allowedMimeTypes: ['image/jpeg', 'image/png', 'image/gif', 'image/webp'],
42
+ maxSize: 5 * 1024 * 1024, // 5 MB
69
43
  },
70
-
71
- fileDist: () => 'avatars', // Store in avatars directory
72
-
73
- // Return full file object
74
- mapToRequestBody: (file) => file,
44
+ fileDist: () => 'avatars',
45
+ fileName: (file, req) => `avatar-${req!.user.id}-${Date.now()}.${file.originalname.split('.').pop()}`,
46
+ mapToRequestBody: (file) => file, // full UploadedFile
75
47
  })
76
48
  )
77
- async uploadAvatar(@Body() body: any, @Request() req) {
49
+ async uploadAvatar(@Body() body: any, @Request() req: any) {
78
50
  const userId = req.user.id;
79
51
  const newAvatar = body.avatar;
80
52
 
81
- // Get current user
82
53
  const user = await this.userService.findById(userId);
83
54
 
84
- // Delete old avatar if exists
55
+ // Delete the old avatar if present (don't fail the request if cleanup fails).
85
56
  if (user.avatarKey) {
86
57
  try {
87
- const storage = await FileStorageService.getStorage();
88
- await storage.deleteFile(user.avatarKey);
89
- } catch (error) {
90
- // Log error but don't fail the upload
91
- console.error('Failed to delete old avatar:', error);
58
+ await this.fileStorage.deleteFile(user.avatarKey);
59
+ } catch (err) {
60
+ console.error('Failed to delete old avatar:', err);
92
61
  }
93
62
  }
94
63
 
95
- // Update user with new avatar
96
- const updatedUser = await this.userService.updateAvatar(
97
- userId,
98
- newAvatar.key,
99
- newAvatar.url
100
- );
64
+ const updated = await this.userService.updateAvatar(userId, newAvatar.key, newAvatar.url);
101
65
 
102
66
  return {
103
67
  message: 'Avatar updated successfully',
104
- avatar: {
105
- key: newAvatar.key,
106
- url: newAvatar.url,
107
- size: newAvatar.size,
108
- },
109
- user: {
110
- id: updatedUser.id,
111
- email: updatedUser.email,
112
- avatarUrl: updatedUser.avatarUrl,
113
- },
68
+ avatar: { key: newAvatar.key, url: newAvatar.url, size: newAvatar.size },
69
+ user: { id: updated.id, email: updated.email, avatarUrl: updated.avatarUrl },
114
70
  };
115
71
  }
116
72
 
117
73
  @Post('avatar/delete')
118
74
  @UseGuards(JwtAuthGuard)
119
- async deleteAvatar(@Request() req) {
120
- const userId = req.user.id;
121
- const user = await this.userService.findById(userId);
122
-
123
- if (!user.avatarKey) {
124
- throw new BadRequestException('No avatar to delete');
125
- }
126
-
127
- // Delete from storage
128
- const storage = await FileStorageService.getStorage();
129
- await storage.deleteFile(user.avatarKey);
75
+ async deleteAvatar(@Request() req: any) {
76
+ const user = await this.userService.findById(req.user.id);
77
+ if (!user.avatarKey) throw new BadRequestException('No avatar to delete');
130
78
 
131
- // Update database
132
- await this.userService.updateAvatar(userId, null, null);
79
+ await this.fileStorage.deleteFile(user.avatarKey);
80
+ await this.userService.updateAvatar(req.user.id, null, null);
133
81
 
134
- return {
135
- message: 'Avatar deleted successfully',
136
- };
82
+ return { message: 'Avatar deleted successfully' };
137
83
  }
138
84
  }
139
-