@ackplus/nest-file-storage 1.1.11 → 1.1.13
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/LICENSE +22 -0
- package/README.md +658 -6
- package/{src → dist}/index.d.ts +0 -1
- package/dist/index.js +21 -0
- package/dist/index.js.map +1 -0
- package/{src → dist}/lib/constants.d.ts +0 -1
- package/{src → dist}/lib/constants.js +1 -0
- package/dist/lib/constants.js.map +1 -0
- package/{src → dist}/lib/file-storage.service.d.ts +0 -1
- package/{src → dist}/lib/file-storage.service.js +3 -3
- package/dist/lib/file-storage.service.js.map +1 -0
- package/{src → dist}/lib/index.d.ts +0 -1
- package/dist/lib/index.js +22 -0
- package/dist/lib/index.js.map +1 -0
- package/{src → dist}/lib/interceptor/file-storage.interceptor.d.ts +0 -4
- package/{src → dist}/lib/interceptor/file-storage.interceptor.js +5 -17
- package/dist/lib/interceptor/file-storage.interceptor.js.map +1 -0
- package/{src → dist}/lib/nest-file-storage.module.d.ts +0 -1
- package/{src → dist}/lib/nest-file-storage.module.js +9 -3
- package/dist/lib/nest-file-storage.module.js.map +1 -0
- package/{src → dist}/lib/storage/azure.storage.d.ts +0 -1
- package/{src → dist}/lib/storage/azure.storage.js +49 -10
- package/dist/lib/storage/azure.storage.js.map +1 -0
- package/{src → dist}/lib/storage/local.storage.d.ts +0 -15
- package/{src → dist}/lib/storage/local.storage.js +44 -32
- package/dist/lib/storage/local.storage.js.map +1 -0
- package/{src → dist}/lib/storage/s3.storage.d.ts +0 -1
- package/{src → dist}/lib/storage/s3.storage.js +43 -8
- package/dist/lib/storage/s3.storage.js.map +1 -0
- package/{src → dist}/lib/storage.factory.d.ts +0 -1
- package/dist/lib/storage.factory.js +46 -0
- package/dist/lib/storage.factory.js.map +1 -0
- package/{src → dist}/lib/types.d.ts +0 -14
- package/{src → dist}/lib/types.js +1 -0
- package/dist/lib/types.js.map +1 -0
- package/dist/tsconfig.build.tsbuildinfo +1 -0
- package/examples/1-basic-local-storage.example.ts +22 -0
- package/examples/10-testing.example.ts +233 -0
- package/examples/2-s3-storage.example.ts +40 -0
- package/examples/3-azure-storage.example.ts +35 -0
- package/examples/4-upload-controller.example.ts +117 -0
- package/examples/5-custom-configuration.example.ts +75 -0
- package/examples/6-file-service.example.ts +124 -0
- package/examples/7-user-avatar.example.ts +139 -0
- package/examples/8-document-management.example.ts +265 -0
- package/examples/9-dynamic-storage.example.ts +175 -0
- package/examples/README.md +122 -0
- package/package.json +86 -6
- package/src/index.d.ts.map +0 -1
- package/src/index.js +0 -7
- package/src/lib/constants.d.ts.map +0 -1
- package/src/lib/file-storage.service.d.ts.map +0 -1
- package/src/lib/index.d.ts.map +0 -1
- package/src/lib/index.js +0 -8
- package/src/lib/interceptor/file-storage.interceptor.d.ts.map +0 -1
- package/src/lib/nest-file-storage.module.d.ts.map +0 -1
- package/src/lib/storage/azure.storage.d.ts.map +0 -1
- package/src/lib/storage/local.storage.d.ts.map +0 -1
- package/src/lib/storage/s3.storage.d.ts.map +0 -1
- package/src/lib/storage.factory.d.ts.map +0 -1
- package/src/lib/storage.factory.js +0 -81
- package/src/lib/types.d.ts.map +0 -1
|
@@ -0,0 +1,139 @@
|
|
|
1
|
+
/**
|
|
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.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import {
|
|
9
|
+
Controller,
|
|
10
|
+
Post,
|
|
11
|
+
UseInterceptors,
|
|
12
|
+
Body,
|
|
13
|
+
Request,
|
|
14
|
+
BadRequestException,
|
|
15
|
+
UseGuards,
|
|
16
|
+
} from '@nestjs/common';
|
|
17
|
+
import { FileStorageInterceptor, FileStorageService } from '@ackplus/nest-file-storage';
|
|
18
|
+
import { JwtAuthGuard } from './auth/jwt-auth.guard'; // Your auth guard
|
|
19
|
+
|
|
20
|
+
// User entity interface
|
|
21
|
+
interface User {
|
|
22
|
+
id: number;
|
|
23
|
+
email: string;
|
|
24
|
+
avatarKey?: string;
|
|
25
|
+
avatarUrl?: string;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
// User service (example)
|
|
29
|
+
class UserService {
|
|
30
|
+
async findById(id: number): Promise<User> {
|
|
31
|
+
// Your database query
|
|
32
|
+
return {} as User;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
async updateAvatar(id: number, avatarKey: string, avatarUrl: string): Promise<User> {
|
|
36
|
+
// Your database update
|
|
37
|
+
return {} as User;
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
@Controller('users')
|
|
42
|
+
export class UserAvatarController {
|
|
43
|
+
constructor(private readonly userService: UserService) {}
|
|
44
|
+
|
|
45
|
+
@Post('avatar')
|
|
46
|
+
@UseGuards(JwtAuthGuard) // Require authentication
|
|
47
|
+
@UseInterceptors(
|
|
48
|
+
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}`;
|
|
69
|
+
},
|
|
70
|
+
|
|
71
|
+
fileDist: () => 'avatars', // Store in avatars directory
|
|
72
|
+
|
|
73
|
+
// Return full file object
|
|
74
|
+
mapToRequestBody: (file) => file,
|
|
75
|
+
})
|
|
76
|
+
)
|
|
77
|
+
async uploadAvatar(@Body() body: any, @Request() req) {
|
|
78
|
+
const userId = req.user.id;
|
|
79
|
+
const newAvatar = body.avatar;
|
|
80
|
+
|
|
81
|
+
// Get current user
|
|
82
|
+
const user = await this.userService.findById(userId);
|
|
83
|
+
|
|
84
|
+
// Delete old avatar if exists
|
|
85
|
+
if (user.avatarKey) {
|
|
86
|
+
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);
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
// Update user with new avatar
|
|
96
|
+
const updatedUser = await this.userService.updateAvatar(
|
|
97
|
+
userId,
|
|
98
|
+
newAvatar.key,
|
|
99
|
+
newAvatar.url
|
|
100
|
+
);
|
|
101
|
+
|
|
102
|
+
return {
|
|
103
|
+
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
|
+
},
|
|
114
|
+
};
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
@Post('avatar/delete')
|
|
118
|
+
@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);
|
|
130
|
+
|
|
131
|
+
// Update database
|
|
132
|
+
await this.userService.updateAvatar(userId, null, null);
|
|
133
|
+
|
|
134
|
+
return {
|
|
135
|
+
message: 'Avatar deleted successfully',
|
|
136
|
+
};
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
|
|
@@ -0,0 +1,265 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Example 8: Document Management System
|
|
3
|
+
*
|
|
4
|
+
* This example demonstrates a complete document management system with
|
|
5
|
+
* upload, download, listing, and deletion features.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import {
|
|
9
|
+
Controller,
|
|
10
|
+
Get,
|
|
11
|
+
Post,
|
|
12
|
+
Delete,
|
|
13
|
+
Param,
|
|
14
|
+
UseInterceptors,
|
|
15
|
+
Body,
|
|
16
|
+
Res,
|
|
17
|
+
NotFoundException,
|
|
18
|
+
StreamableFile,
|
|
19
|
+
} from '@nestjs/common';
|
|
20
|
+
import { Response } from 'express';
|
|
21
|
+
import { FileStorageInterceptor, FileStorageService } from '@ackplus/nest-file-storage';
|
|
22
|
+
|
|
23
|
+
// Document entity interface
|
|
24
|
+
interface Document {
|
|
25
|
+
id: number;
|
|
26
|
+
name: string;
|
|
27
|
+
key: string;
|
|
28
|
+
url: string;
|
|
29
|
+
size: number;
|
|
30
|
+
mimetype: string;
|
|
31
|
+
uploadedAt: Date;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
// Document service (example)
|
|
35
|
+
class DocumentService {
|
|
36
|
+
async create(data: Partial<Document>): Promise<Document> {
|
|
37
|
+
// Save to database
|
|
38
|
+
return {} as Document;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
async findAll(): Promise<Document[]> {
|
|
42
|
+
// Get all documents from database
|
|
43
|
+
return [] as Document[];
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
async findById(id: number): Promise<Document> {
|
|
47
|
+
// Get document from database
|
|
48
|
+
return {} as Document;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
async delete(id: number): Promise<void> {
|
|
52
|
+
// Delete from database
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
@Controller('documents')
|
|
57
|
+
export class DocumentController {
|
|
58
|
+
constructor(private readonly documentService: DocumentService) {}
|
|
59
|
+
|
|
60
|
+
/**
|
|
61
|
+
* Upload single document
|
|
62
|
+
*/
|
|
63
|
+
@Post('upload')
|
|
64
|
+
@UseInterceptors(
|
|
65
|
+
FileStorageInterceptor('document', {
|
|
66
|
+
fileDist: () => 'documents',
|
|
67
|
+
mapToRequestBody: (file) => file, // Return full file object
|
|
68
|
+
})
|
|
69
|
+
)
|
|
70
|
+
async uploadDocument(@Body() body: any) {
|
|
71
|
+
const uploadedFile = body.document;
|
|
72
|
+
|
|
73
|
+
// Save document info to database
|
|
74
|
+
const document = await this.documentService.create({
|
|
75
|
+
name: uploadedFile.originalName,
|
|
76
|
+
key: uploadedFile.key,
|
|
77
|
+
url: uploadedFile.url,
|
|
78
|
+
size: uploadedFile.size,
|
|
79
|
+
mimetype: uploadedFile.mimetype,
|
|
80
|
+
uploadedAt: new Date(),
|
|
81
|
+
});
|
|
82
|
+
|
|
83
|
+
return {
|
|
84
|
+
message: 'Document uploaded successfully',
|
|
85
|
+
document,
|
|
86
|
+
};
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
/**
|
|
90
|
+
* Upload multiple documents
|
|
91
|
+
*/
|
|
92
|
+
@Post('upload/multiple')
|
|
93
|
+
@UseInterceptors(
|
|
94
|
+
FileStorageInterceptor({
|
|
95
|
+
type: 'array',
|
|
96
|
+
fieldName: 'documents',
|
|
97
|
+
maxCount: 10,
|
|
98
|
+
}, {
|
|
99
|
+
fileDist: () => 'documents',
|
|
100
|
+
mapToRequestBody: (files) => files,
|
|
101
|
+
})
|
|
102
|
+
)
|
|
103
|
+
async uploadMultipleDocuments(@Body() body: any) {
|
|
104
|
+
const uploadedFiles = body.documents;
|
|
105
|
+
|
|
106
|
+
// Save all documents to database
|
|
107
|
+
const documents = await Promise.all(
|
|
108
|
+
uploadedFiles.map(file =>
|
|
109
|
+
this.documentService.create({
|
|
110
|
+
name: file.originalName,
|
|
111
|
+
key: file.key,
|
|
112
|
+
url: file.url,
|
|
113
|
+
size: file.size,
|
|
114
|
+
mimetype: file.mimetype,
|
|
115
|
+
uploadedAt: new Date(),
|
|
116
|
+
})
|
|
117
|
+
)
|
|
118
|
+
);
|
|
119
|
+
|
|
120
|
+
return {
|
|
121
|
+
message: `${documents.length} documents uploaded successfully`,
|
|
122
|
+
documents,
|
|
123
|
+
};
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
/**
|
|
127
|
+
* Get all documents
|
|
128
|
+
*/
|
|
129
|
+
@Get()
|
|
130
|
+
async getAllDocuments() {
|
|
131
|
+
const documents = await this.documentService.findAll();
|
|
132
|
+
return { documents };
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
/**
|
|
136
|
+
* Download document
|
|
137
|
+
*/
|
|
138
|
+
@Get(':id/download')
|
|
139
|
+
async downloadDocument(
|
|
140
|
+
@Param('id') id: number,
|
|
141
|
+
@Res({ passthrough: true }) res: Response,
|
|
142
|
+
): Promise<StreamableFile> {
|
|
143
|
+
// Get document from database
|
|
144
|
+
const document = await this.documentService.findById(id);
|
|
145
|
+
if (!document) {
|
|
146
|
+
throw new NotFoundException('Document not found');
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
// Get file from storage
|
|
150
|
+
const storage = await FileStorageService.getStorage();
|
|
151
|
+
const fileBuffer = await storage.getFile(document.key);
|
|
152
|
+
|
|
153
|
+
// Set response headers
|
|
154
|
+
res.set({
|
|
155
|
+
'Content-Type': document.mimetype || 'application/octet-stream',
|
|
156
|
+
'Content-Disposition': `attachment; filename="${document.name}"`,
|
|
157
|
+
'Content-Length': document.size,
|
|
158
|
+
});
|
|
159
|
+
|
|
160
|
+
return new StreamableFile(fileBuffer);
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
/**
|
|
164
|
+
* Get document URL (for preview or direct access)
|
|
165
|
+
*/
|
|
166
|
+
@Get(':id/url')
|
|
167
|
+
async getDocumentUrl(@Param('id') id: number) {
|
|
168
|
+
const document = await this.documentService.findById(id);
|
|
169
|
+
if (!document) {
|
|
170
|
+
throw new NotFoundException('Document not found');
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
return {
|
|
174
|
+
url: document.url,
|
|
175
|
+
expiresIn: null, // Permanent URL
|
|
176
|
+
};
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
/**
|
|
180
|
+
* Get signed URL (for S3, temporary access)
|
|
181
|
+
*/
|
|
182
|
+
@Get(':id/signed-url')
|
|
183
|
+
async getSignedUrl(@Param('id') id: number) {
|
|
184
|
+
const document = await this.documentService.findById(id);
|
|
185
|
+
if (!document) {
|
|
186
|
+
throw new NotFoundException('Document not found');
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
const storage = await FileStorageService.getStorage();
|
|
190
|
+
|
|
191
|
+
let url: string;
|
|
192
|
+
if ('getSignedUrl' in storage) {
|
|
193
|
+
// Generate signed URL (valid for 1 hour)
|
|
194
|
+
url = await storage.getSignedUrl(document.key, { expiresIn: 3600 });
|
|
195
|
+
} else {
|
|
196
|
+
// Fallback to regular URL
|
|
197
|
+
url = storage.getUrl(document.key);
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
return {
|
|
201
|
+
url,
|
|
202
|
+
expiresIn: 3600, // seconds
|
|
203
|
+
};
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
/**
|
|
207
|
+
* Delete document
|
|
208
|
+
*/
|
|
209
|
+
@Delete(':id')
|
|
210
|
+
async deleteDocument(@Param('id') id: number) {
|
|
211
|
+
const document = await this.documentService.findById(id);
|
|
212
|
+
if (!document) {
|
|
213
|
+
throw new NotFoundException('Document not found');
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
// Delete from storage
|
|
217
|
+
const storage = await FileStorageService.getStorage();
|
|
218
|
+
await storage.deleteFile(document.key);
|
|
219
|
+
|
|
220
|
+
// Delete from database
|
|
221
|
+
await this.documentService.delete(id);
|
|
222
|
+
|
|
223
|
+
return {
|
|
224
|
+
message: 'Document deleted successfully',
|
|
225
|
+
};
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
/**
|
|
229
|
+
* Copy document
|
|
230
|
+
*/
|
|
231
|
+
@Post(':id/copy')
|
|
232
|
+
async copyDocument(@Param('id') id: number) {
|
|
233
|
+
const document = await this.documentService.findById(id);
|
|
234
|
+
if (!document) {
|
|
235
|
+
throw new NotFoundException('Document not found');
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
// Generate new key for the copy
|
|
239
|
+
const timestamp = Date.now();
|
|
240
|
+
const newKey = document.key.replace(
|
|
241
|
+
/(\.[^.]+)$/,
|
|
242
|
+
`-copy-${timestamp}$1`
|
|
243
|
+
);
|
|
244
|
+
|
|
245
|
+
// Copy file in storage
|
|
246
|
+
const storage = await FileStorageService.getStorage();
|
|
247
|
+
const copiedFile = await storage.copyFile(document.key, newKey);
|
|
248
|
+
|
|
249
|
+
// Save copy to database
|
|
250
|
+
const copiedDocument = await this.documentService.create({
|
|
251
|
+
name: `${document.name} (Copy)`,
|
|
252
|
+
key: copiedFile.key,
|
|
253
|
+
url: copiedFile.url,
|
|
254
|
+
size: copiedFile.size,
|
|
255
|
+
mimetype: document.mimetype,
|
|
256
|
+
uploadedAt: new Date(),
|
|
257
|
+
});
|
|
258
|
+
|
|
259
|
+
return {
|
|
260
|
+
message: 'Document copied successfully',
|
|
261
|
+
document: copiedDocument,
|
|
262
|
+
};
|
|
263
|
+
}
|
|
264
|
+
}
|
|
265
|
+
|
|
@@ -0,0 +1,175 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Example 9: Dynamic Storage Selection
|
|
3
|
+
*
|
|
4
|
+
* This example demonstrates how to dynamically choose storage provider
|
|
5
|
+
* based on request, user preferences, or business logic.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import {
|
|
9
|
+
Controller,
|
|
10
|
+
Post,
|
|
11
|
+
UseInterceptors,
|
|
12
|
+
Body,
|
|
13
|
+
Query,
|
|
14
|
+
} from '@nestjs/common';
|
|
15
|
+
import {
|
|
16
|
+
FileStorageInterceptor,
|
|
17
|
+
FileStorageEnum,
|
|
18
|
+
} from '@ackplus/nest-file-storage';
|
|
19
|
+
|
|
20
|
+
@Controller('upload')
|
|
21
|
+
export class DynamicStorageController {
|
|
22
|
+
/**
|
|
23
|
+
* Upload to specific storage based on query parameter
|
|
24
|
+
* Example: POST /upload/dynamic?storage=s3
|
|
25
|
+
*/
|
|
26
|
+
@Post('dynamic')
|
|
27
|
+
@UseInterceptors(
|
|
28
|
+
FileStorageInterceptor('file', {
|
|
29
|
+
// Storage type will be determined at runtime
|
|
30
|
+
})
|
|
31
|
+
)
|
|
32
|
+
uploadDynamic(
|
|
33
|
+
@Body() body: any,
|
|
34
|
+
@Query('storage') storage: string,
|
|
35
|
+
) {
|
|
36
|
+
return {
|
|
37
|
+
message: 'File uploaded successfully',
|
|
38
|
+
storage,
|
|
39
|
+
fileKey: body.file,
|
|
40
|
+
};
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
/**
|
|
44
|
+
* Upload large files to S3, small files to local storage
|
|
45
|
+
*/
|
|
46
|
+
@Post('smart')
|
|
47
|
+
@UseInterceptors(
|
|
48
|
+
FileStorageInterceptor('file', {
|
|
49
|
+
fileName: (file, req) => {
|
|
50
|
+
const timestamp = Date.now();
|
|
51
|
+
const ext = file.originalname.split('.').pop();
|
|
52
|
+
return `${timestamp}-${file.originalname}`;
|
|
53
|
+
},
|
|
54
|
+
// Determine storage based on file size
|
|
55
|
+
storageType: FileStorageEnum.LOCAL, // Can be overridden
|
|
56
|
+
})
|
|
57
|
+
)
|
|
58
|
+
uploadSmart(@Body() body: any) {
|
|
59
|
+
return {
|
|
60
|
+
message: 'File uploaded successfully',
|
|
61
|
+
fileKey: body.file,
|
|
62
|
+
};
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
/**
|
|
66
|
+
* Upload to S3 explicitly (override default storage)
|
|
67
|
+
*/
|
|
68
|
+
@Post('to-s3')
|
|
69
|
+
@UseInterceptors(
|
|
70
|
+
FileStorageInterceptor('file', {
|
|
71
|
+
storageType: FileStorageEnum.S3,
|
|
72
|
+
})
|
|
73
|
+
)
|
|
74
|
+
uploadToS3(@Body() body: any) {
|
|
75
|
+
return {
|
|
76
|
+
message: 'File uploaded to S3',
|
|
77
|
+
fileKey: body.file,
|
|
78
|
+
};
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
/**
|
|
82
|
+
* Upload to Azure explicitly
|
|
83
|
+
*/
|
|
84
|
+
@Post('to-azure')
|
|
85
|
+
@UseInterceptors(
|
|
86
|
+
FileStorageInterceptor('file', {
|
|
87
|
+
storageType: FileStorageEnum.AZURE,
|
|
88
|
+
})
|
|
89
|
+
)
|
|
90
|
+
uploadToAzure(@Body() body: any) {
|
|
91
|
+
return {
|
|
92
|
+
message: 'File uploaded to Azure',
|
|
93
|
+
fileKey: body.file,
|
|
94
|
+
};
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
/**
|
|
98
|
+
* Upload to local storage explicitly
|
|
99
|
+
*/
|
|
100
|
+
@Post('to-local')
|
|
101
|
+
@UseInterceptors(
|
|
102
|
+
FileStorageInterceptor('file', {
|
|
103
|
+
storageType: FileStorageEnum.LOCAL,
|
|
104
|
+
})
|
|
105
|
+
)
|
|
106
|
+
uploadToLocal(@Body() body: any) {
|
|
107
|
+
return {
|
|
108
|
+
message: 'File uploaded to local storage',
|
|
109
|
+
fileKey: body.file,
|
|
110
|
+
};
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
/**
|
|
115
|
+
* Advanced: Custom logic for storage selection
|
|
116
|
+
*/
|
|
117
|
+
import { Injectable } from '@nestjs/common';
|
|
118
|
+
import { FileStorageService } from '@ackplus/nest-file-storage';
|
|
119
|
+
|
|
120
|
+
@Injectable()
|
|
121
|
+
export class SmartStorageService {
|
|
122
|
+
/**
|
|
123
|
+
* Select storage based on file size
|
|
124
|
+
*/
|
|
125
|
+
async uploadFile(buffer: Buffer, fileName: string, fileSize: number) {
|
|
126
|
+
// Use local storage for small files (< 10MB)
|
|
127
|
+
// Use S3 for large files
|
|
128
|
+
const storageType = fileSize < 10 * 1024 * 1024
|
|
129
|
+
? FileStorageEnum.LOCAL
|
|
130
|
+
: FileStorageEnum.S3;
|
|
131
|
+
|
|
132
|
+
const storage = await FileStorageService.getStorage(storageType);
|
|
133
|
+
return await storage.putFile(buffer, fileName);
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
/**
|
|
137
|
+
* Select storage based on file type
|
|
138
|
+
*/
|
|
139
|
+
async uploadByType(buffer: Buffer, fileName: string, mimetype: string) {
|
|
140
|
+
let storageType: FileStorageEnum;
|
|
141
|
+
|
|
142
|
+
if (mimetype.startsWith('image/')) {
|
|
143
|
+
// Store images on S3 with CloudFront CDN
|
|
144
|
+
storageType = FileStorageEnum.S3;
|
|
145
|
+
} else if (mimetype.startsWith('video/')) {
|
|
146
|
+
// Store videos on Azure
|
|
147
|
+
storageType = FileStorageEnum.AZURE;
|
|
148
|
+
} else {
|
|
149
|
+
// Store other files locally
|
|
150
|
+
storageType = FileStorageEnum.LOCAL;
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
const storage = await FileStorageService.getStorage(storageType);
|
|
154
|
+
return await storage.putFile(buffer, fileName);
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
/**
|
|
158
|
+
* Select storage based on user plan
|
|
159
|
+
*/
|
|
160
|
+
async uploadForUser(
|
|
161
|
+
buffer: Buffer,
|
|
162
|
+
fileName: string,
|
|
163
|
+
userPlan: 'free' | 'premium'
|
|
164
|
+
) {
|
|
165
|
+
// Free users: local storage
|
|
166
|
+
// Premium users: S3 with better performance
|
|
167
|
+
const storageType = userPlan === 'premium'
|
|
168
|
+
? FileStorageEnum.S3
|
|
169
|
+
: FileStorageEnum.LOCAL;
|
|
170
|
+
|
|
171
|
+
const storage = await FileStorageService.getStorage(storageType);
|
|
172
|
+
return await storage.putFile(buffer, fileName);
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
|
|
@@ -0,0 +1,122 @@
|
|
|
1
|
+
# Examples
|
|
2
|
+
|
|
3
|
+
This directory contains comprehensive examples demonstrating various features and use cases of `@ackplus/nest-file-storage`.
|
|
4
|
+
|
|
5
|
+
## 📚 Available Examples
|
|
6
|
+
|
|
7
|
+
### Getting Started
|
|
8
|
+
|
|
9
|
+
1. **[Basic Local Storage](./1-basic-local-storage.example.ts)**
|
|
10
|
+
- Setting up local file storage
|
|
11
|
+
- Basic configuration
|
|
12
|
+
- Perfect for development
|
|
13
|
+
|
|
14
|
+
2. **[AWS S3 Storage](./2-s3-storage.example.ts)**
|
|
15
|
+
- Configure AWS S3
|
|
16
|
+
- Async configuration with ConfigService
|
|
17
|
+
- Environment variables setup
|
|
18
|
+
|
|
19
|
+
3. **[Azure Blob Storage](./3-azure-storage.example.ts)**
|
|
20
|
+
- Configure Azure Blob Storage
|
|
21
|
+
- Connection string setup
|
|
22
|
+
- Container management
|
|
23
|
+
|
|
24
|
+
### Core Features
|
|
25
|
+
|
|
26
|
+
4. **[File Upload Controller](./4-upload-controller.example.ts)**
|
|
27
|
+
- Single file upload
|
|
28
|
+
- Multiple files upload
|
|
29
|
+
- Multiple fields upload
|
|
30
|
+
- File validation
|
|
31
|
+
- Custom file mapping
|
|
32
|
+
|
|
33
|
+
5. **[Custom Configuration](./5-custom-configuration.example.ts)**
|
|
34
|
+
- Custom file naming
|
|
35
|
+
- Custom directory structure
|
|
36
|
+
- File transformations
|
|
37
|
+
- Organized file storage
|
|
38
|
+
|
|
39
|
+
6. **[File Service](./6-file-service.example.ts)**
|
|
40
|
+
- File operations (get, delete, copy)
|
|
41
|
+
- Upload from local filesystem
|
|
42
|
+
- Get public URLs
|
|
43
|
+
- Generate signed URLs (S3)
|
|
44
|
+
- Batch operations
|
|
45
|
+
|
|
46
|
+
### Real-World Use Cases
|
|
47
|
+
|
|
48
|
+
7. **[User Avatar Upload](./7-user-avatar.example.ts)**
|
|
49
|
+
- Complete avatar upload feature
|
|
50
|
+
- File validation
|
|
51
|
+
- Old file cleanup
|
|
52
|
+
- Database integration
|
|
53
|
+
|
|
54
|
+
8. **[Document Management System](./8-document-management.example.ts)**
|
|
55
|
+
- Upload/download documents
|
|
56
|
+
- Document listing
|
|
57
|
+
- File copying
|
|
58
|
+
- Signed URLs for secure access
|
|
59
|
+
|
|
60
|
+
9. **[Dynamic Storage Selection](./9-dynamic-storage.example.ts)**
|
|
61
|
+
- Switch storage per request
|
|
62
|
+
- Storage selection by file size
|
|
63
|
+
- Storage selection by file type
|
|
64
|
+
- Storage selection by user plan
|
|
65
|
+
|
|
66
|
+
10. **[Testing](./10-testing.example.ts)**
|
|
67
|
+
- E2E testing examples
|
|
68
|
+
- Unit testing
|
|
69
|
+
- Mock storage implementation
|
|
70
|
+
- Test setup and cleanup
|
|
71
|
+
|
|
72
|
+
## 🚀 How to Use These Examples
|
|
73
|
+
|
|
74
|
+
### 1. Copy Example Code
|
|
75
|
+
|
|
76
|
+
Simply copy the relevant example code to your project:
|
|
77
|
+
|
|
78
|
+
```bash
|
|
79
|
+
# Copy specific example
|
|
80
|
+
cp examples/1-basic-local-storage.example.ts src/config/storage.config.ts
|
|
81
|
+
```
|
|
82
|
+
|
|
83
|
+
### 2. Modify for Your Needs
|
|
84
|
+
|
|
85
|
+
Each example is well-documented with comments explaining:
|
|
86
|
+
- What the code does
|
|
87
|
+
- Configuration options
|
|
88
|
+
- Environment variables needed
|
|
89
|
+
- Expected behavior
|
|
90
|
+
|
|
91
|
+
### 3. Run Your Application
|
|
92
|
+
|
|
93
|
+
```bash
|
|
94
|
+
npm run start:dev
|
|
95
|
+
```
|
|
96
|
+
|
|
97
|
+
## 💡 Tips
|
|
98
|
+
|
|
99
|
+
- **Start Simple**: Begin with Example 1 (Local Storage) for development
|
|
100
|
+
- **Environment Variables**: Use Example 2 or 3 for production with proper secrets management
|
|
101
|
+
- **Validation**: See Example 4 for file type and size validation
|
|
102
|
+
- **Custom Logic**: Examples 5-9 show advanced customization options
|
|
103
|
+
- **Testing**: Example 10 provides complete testing setup
|
|
104
|
+
|
|
105
|
+
## 📖 More Resources
|
|
106
|
+
|
|
107
|
+
- **[Main Documentation](../README.md)** - Complete feature guide
|
|
108
|
+
- **[GitHub Issues](https://github.com/ack-solutions/nest-file-storage/issues)** - Report issues or request features
|
|
109
|
+
- **[NPM Package](https://www.npmjs.com/package/@ackplus/nest-file-storage)** - Published package
|
|
110
|
+
|
|
111
|
+
## 🤝 Contributing
|
|
112
|
+
|
|
113
|
+
Found a bug in an example? Want to add a new example? PRs are welcome!
|
|
114
|
+
|
|
115
|
+
1. Add your example file: `[number]-[name].example.ts`
|
|
116
|
+
2. Add it to this README
|
|
117
|
+
3. Submit a PR
|
|
118
|
+
|
|
119
|
+
---
|
|
120
|
+
|
|
121
|
+
**Need help?** Open an issue on [GitHub](https://github.com/ack-solutions/nest-file-storage/issues)
|
|
122
|
+
|