@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.
- package/CHANGELOG.md +72 -0
- package/MIGRATION.md +220 -0
- package/README.md +353 -547
- package/dist/index.d.ts +1 -4
- package/dist/index.js +1 -4
- package/dist/index.js.map +1 -1
- package/dist/lib/constants.d.ts +3 -1
- package/dist/lib/constants.js +4 -2
- package/dist/lib/constants.js.map +1 -1
- package/dist/lib/driver-registry.d.ts +34 -0
- package/dist/lib/driver-registry.js +118 -0
- package/dist/lib/driver-registry.js.map +1 -0
- package/dist/lib/drivers/azure.driver.d.ts +21 -0
- package/dist/lib/drivers/azure.driver.js +91 -0
- package/dist/lib/drivers/azure.driver.js.map +1 -0
- package/dist/lib/drivers/driver.interface.d.ts +40 -0
- package/dist/lib/drivers/driver.interface.js +3 -0
- package/dist/lib/drivers/driver.interface.js.map +1 -0
- package/dist/lib/drivers/driver.util.d.ts +2 -0
- package/dist/lib/drivers/driver.util.js +15 -0
- package/dist/lib/drivers/driver.util.js.map +1 -0
- package/dist/lib/drivers/index.d.ts +7 -0
- package/dist/lib/drivers/index.js +39 -0
- package/dist/lib/drivers/index.js.map +1 -0
- package/dist/lib/drivers/local.driver.d.ts +15 -0
- package/dist/lib/drivers/local.driver.js +110 -0
- package/dist/lib/drivers/local.driver.js.map +1 -0
- package/dist/lib/drivers/s3.driver.d.ts +22 -0
- package/dist/lib/drivers/s3.driver.js +103 -0
- package/dist/lib/drivers/s3.driver.js.map +1 -0
- package/dist/lib/file-storage.service.d.ts +16 -5
- package/dist/lib/file-storage.service.js +60 -22
- package/dist/lib/file-storage.service.js.map +1 -1
- package/dist/lib/index.d.ts +9 -2
- package/dist/lib/index.js +15 -2
- package/dist/lib/index.js.map +1 -1
- package/dist/lib/interceptor/file-storage.interceptor.d.ts +7 -10
- package/dist/lib/interceptor/file-storage.interceptor.js +119 -112
- package/dist/lib/interceptor/file-storage.interceptor.js.map +1 -1
- package/dist/lib/multer/driver-multer-engine.d.ts +18 -0
- package/dist/lib/multer/driver-multer-engine.js +91 -0
- package/dist/lib/multer/driver-multer-engine.js.map +1 -0
- package/dist/lib/nest-file-storage.module.d.ts +3 -3
- package/dist/lib/nest-file-storage.module.js +81 -44
- package/dist/lib/nest-file-storage.module.js.map +1 -1
- package/dist/lib/registry-holder.d.ts +6 -0
- package/dist/lib/registry-holder.js +26 -0
- package/dist/lib/registry-holder.js.map +1 -0
- package/dist/lib/tenant/tenant-from.d.ts +14 -0
- package/dist/lib/tenant/tenant-from.js +71 -0
- package/dist/lib/tenant/tenant-from.js.map +1 -0
- package/dist/lib/tenant/tenant.types.d.ts +20 -0
- package/dist/lib/tenant/tenant.types.js +3 -0
- package/dist/lib/tenant/tenant.types.js.map +1 -0
- package/dist/lib/types.d.ts +45 -35
- package/dist/lib/types.js.map +1 -1
- package/dist/lib/validation.d.ts +22 -0
- package/dist/lib/validation.js +98 -0
- package/dist/lib/validation.js.map +1 -0
- package/dist/tsconfig.build.tsbuildinfo +1 -1
- package/examples/1-basic-local-storage.example.ts +11 -7
- package/examples/10-testing.example.ts +60 -196
- package/examples/11-custom-driver.example.ts +82 -0
- package/examples/12-multi-tenant.example.ts +93 -0
- package/examples/2-s3-storage.example.ts +18 -16
- package/examples/3-azure-storage.example.ts +14 -12
- package/examples/4-upload-controller.example.ts +20 -55
- package/examples/5-custom-configuration.example.ts +37 -57
- package/examples/6-file-service.example.ts +34 -91
- package/examples/7-user-avatar.example.ts +37 -92
- package/examples/8-document-management.example.ts +45 -196
- package/examples/9-dynamic-storage.example.ts +29 -147
- package/examples/README.md +25 -107
- package/package.json +17 -4
- package/dist/lib/storage/azure.storage.d.ts +0 -18
- package/dist/lib/storage/azure.storage.js +0 -210
- package/dist/lib/storage/azure.storage.js.map +0 -1
- package/dist/lib/storage/local.storage.d.ts +0 -20
- package/dist/lib/storage/local.storage.js +0 -212
- package/dist/lib/storage/local.storage.js.map +0 -1
- package/dist/lib/storage/s3.storage.d.ts +0 -19
- package/dist/lib/storage/s3.storage.js +0 -241
- package/dist/lib/storage/s3.storage.js.map +0 -1
- package/dist/lib/storage.factory.d.ts +0 -8
- package/dist/lib/storage.factory.js +0 -46
- package/dist/lib/storage.factory.js.map +0 -1
|
@@ -1,53 +1,40 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Example 4: File Upload Controller
|
|
3
|
-
*
|
|
4
|
-
*
|
|
3
|
+
*
|
|
4
|
+
* Different upload shapes with the FileStorageInterceptor, plus declarative validation.
|
|
5
5
|
*/
|
|
6
6
|
|
|
7
|
-
import { Controller, Post, UseInterceptors, Body
|
|
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
|
|
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, //
|
|
31
|
+
fileKeys: body.files, // string[]
|
|
43
32
|
count: body.files.length,
|
|
44
33
|
};
|
|
45
34
|
}
|
|
46
35
|
|
|
47
36
|
/**
|
|
48
|
-
* Multiple fields
|
|
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, //
|
|
66
|
-
photos: body.photos, //
|
|
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
|
-
*
|
|
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
|
-
*
|
|
67
|
+
* Declarative validation (v2) — type + size checks become typed 400s.
|
|
92
68
|
*/
|
|
93
69
|
@Post('image')
|
|
94
70
|
@UseInterceptors(
|
|
95
71
|
FileStorageInterceptor('image', {
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
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
|
|
3
|
-
*
|
|
4
|
-
*
|
|
5
|
-
*
|
|
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,
|
|
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
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
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
|
-
*
|
|
62
|
-
*
|
|
63
|
-
*
|
|
64
|
-
*
|
|
65
|
-
*
|
|
66
|
-
*
|
|
67
|
-
*
|
|
68
|
-
*
|
|
69
|
-
*
|
|
70
|
-
*
|
|
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
|
-
*
|
|
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
|
-
|
|
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
|
-
|
|
20
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
47
|
-
|
|
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
|
-
|
|
61
|
-
|
|
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
|
-
|
|
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
|
-
|
|
92
|
-
|
|
93
|
-
|
|
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
|
-
|
|
105
|
-
|
|
106
|
-
|
|
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
|
-
|
|
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
|
-
*
|
|
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'; //
|
|
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(
|
|
31
|
-
//
|
|
32
|
-
return {} as User;
|
|
19
|
+
async findById(_id: number): Promise<User> {
|
|
20
|
+
return {} as User; // your DB query
|
|
33
21
|
}
|
|
34
|
-
|
|
35
|
-
|
|
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
|
-
|
|
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)
|
|
36
|
+
@UseGuards(JwtAuthGuard)
|
|
47
37
|
@UseInterceptors(
|
|
48
38
|
FileStorageInterceptor('avatar', {
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
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
|
-
|
|
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
|
|
55
|
+
// Delete the old avatar if present (don't fail the request if cleanup fails).
|
|
85
56
|
if (user.avatarKey) {
|
|
86
57
|
try {
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
121
|
-
|
|
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
|
-
|
|
132
|
-
await this.userService.updateAvatar(
|
|
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
|
-
|