@ackplus/nest-file-storage 1.1.10 → 1.1.12
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 +2 -2
- 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
package/LICENSE
ADDED
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2024 AckPlus
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
|
22
|
+
|
package/README.md
CHANGED
|
@@ -1,11 +1,663 @@
|
|
|
1
|
-
# nest-
|
|
1
|
+
# @ackplus/nest-file-storage
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
A flexible and feature-rich file storage solution for NestJS applications with support for Local, AWS S3, and Azure Blob Storage.
|
|
4
4
|
|
|
5
|
-
##
|
|
5
|
+
## ✨ Features
|
|
6
6
|
|
|
7
|
-
|
|
7
|
+
- 📦 **Multiple Storage Providers** - Local, AWS S3, and Azure Blob Storage support
|
|
8
|
+
- 🔄 **Easy Switching** - Switch between storage providers with minimal configuration
|
|
9
|
+
- 🎯 **NestJS Integration** - Seamless integration with NestJS decorators and interceptors
|
|
10
|
+
- 📁 **File Operations** - Upload, download, delete, copy files with ease
|
|
11
|
+
- 🔐 **Signed URLs** - Generate presigned URLs for secure file access (S3)
|
|
12
|
+
- 🎨 **Customizable** - Custom file naming, directory structure, and transformations
|
|
13
|
+
- 📝 **TypeScript** - Full TypeScript support with type safety
|
|
14
|
+
- 🧪 **Test-Friendly** - Easy to mock and test
|
|
8
15
|
|
|
9
|
-
##
|
|
16
|
+
## 📦 Installation
|
|
10
17
|
|
|
11
|
-
|
|
18
|
+
```bash
|
|
19
|
+
npm install @ackplus/nest-file-storage
|
|
20
|
+
# or
|
|
21
|
+
pnpm add @ackplus/nest-file-storage
|
|
22
|
+
# or
|
|
23
|
+
yarn add @ackplus/nest-file-storage
|
|
24
|
+
```
|
|
25
|
+
|
|
26
|
+
**For AWS S3 support:**
|
|
27
|
+
|
|
28
|
+
```bash
|
|
29
|
+
npm install @aws-sdk/client-s3 @aws-sdk/s3-request-presigner
|
|
30
|
+
```
|
|
31
|
+
|
|
32
|
+
**For Azure Blob Storage support:**
|
|
33
|
+
|
|
34
|
+
```bash
|
|
35
|
+
npm install @azure/storage-blob
|
|
36
|
+
```
|
|
37
|
+
|
|
38
|
+
## 🚀 Quick Start
|
|
39
|
+
|
|
40
|
+
### Step 1: Configure Module
|
|
41
|
+
|
|
42
|
+
Choose your storage provider and configure the module:
|
|
43
|
+
|
|
44
|
+
#### Local Storage
|
|
45
|
+
|
|
46
|
+
```typescript
|
|
47
|
+
// app.module.ts
|
|
48
|
+
import { Module } from '@nestjs/common';
|
|
49
|
+
import { NestFileStorageModule, FileStorageEnum } from '@ackplus/nest-file-storage';
|
|
50
|
+
|
|
51
|
+
@Module({
|
|
52
|
+
imports: [
|
|
53
|
+
NestFileStorageModule.forRoot({
|
|
54
|
+
storage: FileStorageEnum.LOCAL,
|
|
55
|
+
localConfig: {
|
|
56
|
+
rootPath: './uploads',
|
|
57
|
+
baseUrl: 'http://localhost:3000/uploads',
|
|
58
|
+
},
|
|
59
|
+
}),
|
|
60
|
+
],
|
|
61
|
+
})
|
|
62
|
+
export class AppModule {}
|
|
63
|
+
```
|
|
64
|
+
|
|
65
|
+
#### AWS S3
|
|
66
|
+
|
|
67
|
+
```typescript
|
|
68
|
+
// app.module.ts
|
|
69
|
+
import { Module } from '@nestjs/common';
|
|
70
|
+
import { NestFileStorageModule, FileStorageEnum } from '@ackplus/nest-file-storage';
|
|
71
|
+
|
|
72
|
+
@Module({
|
|
73
|
+
imports: [
|
|
74
|
+
NestFileStorageModule.forRoot({
|
|
75
|
+
storage: FileStorageEnum.S3,
|
|
76
|
+
s3Config: {
|
|
77
|
+
accessKeyId: process.env.AWS_ACCESS_KEY_ID,
|
|
78
|
+
secretAccessKey: process.env.AWS_SECRET_ACCESS_KEY,
|
|
79
|
+
region: process.env.AWS_REGION,
|
|
80
|
+
bucket: process.env.AWS_BUCKET,
|
|
81
|
+
},
|
|
82
|
+
}),
|
|
83
|
+
],
|
|
84
|
+
})
|
|
85
|
+
export class AppModule {}
|
|
86
|
+
```
|
|
87
|
+
|
|
88
|
+
#### Azure Blob Storage
|
|
89
|
+
|
|
90
|
+
```typescript
|
|
91
|
+
// app.module.ts
|
|
92
|
+
import { Module } from '@nestjs/common';
|
|
93
|
+
import { NestFileStorageModule, FileStorageEnum } from '@ackplus/nest-file-storage';
|
|
94
|
+
|
|
95
|
+
@Module({
|
|
96
|
+
imports: [
|
|
97
|
+
NestFileStorageModule.forRoot({
|
|
98
|
+
storage: FileStorageEnum.AZURE,
|
|
99
|
+
azureConfig: {
|
|
100
|
+
account: process.env.AZURE_STORAGE_ACCOUNT,
|
|
101
|
+
accountKey: process.env.AZURE_STORAGE_KEY,
|
|
102
|
+
container: process.env.AZURE_CONTAINER,
|
|
103
|
+
},
|
|
104
|
+
}),
|
|
105
|
+
],
|
|
106
|
+
})
|
|
107
|
+
export class AppModule {}
|
|
108
|
+
```
|
|
109
|
+
|
|
110
|
+
### Step 2: Upload Files in Controller
|
|
111
|
+
|
|
112
|
+
```typescript
|
|
113
|
+
// upload.controller.ts
|
|
114
|
+
import { Controller, Post, UseInterceptors } from '@nestjs/common';
|
|
115
|
+
import { FileStorageInterceptor } from '@ackplus/nest-file-storage';
|
|
116
|
+
|
|
117
|
+
@Controller('upload')
|
|
118
|
+
export class UploadController {
|
|
119
|
+
// Single file upload
|
|
120
|
+
@Post('single')
|
|
121
|
+
@UseInterceptors(FileStorageInterceptor('file'))
|
|
122
|
+
uploadSingle(@Body() body: any) {
|
|
123
|
+
// File key is automatically added to body.file
|
|
124
|
+
return {
|
|
125
|
+
message: 'File uploaded successfully',
|
|
126
|
+
fileKey: body.file,
|
|
127
|
+
};
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
// Multiple files upload
|
|
131
|
+
@Post('multiple')
|
|
132
|
+
@UseInterceptors(
|
|
133
|
+
FileStorageInterceptor({
|
|
134
|
+
type: 'array',
|
|
135
|
+
fieldName: 'files',
|
|
136
|
+
maxCount: 10,
|
|
137
|
+
})
|
|
138
|
+
)
|
|
139
|
+
uploadMultiple(@Body() body: any) {
|
|
140
|
+
// File keys are automatically added to body.files as array
|
|
141
|
+
return {
|
|
142
|
+
message: 'Files uploaded successfully',
|
|
143
|
+
fileKeys: body.files,
|
|
144
|
+
};
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
// Multiple fields
|
|
148
|
+
@Post('fields')
|
|
149
|
+
@UseInterceptors(
|
|
150
|
+
FileStorageInterceptor({
|
|
151
|
+
type: 'fields',
|
|
152
|
+
fields: [
|
|
153
|
+
{ name: 'avatar', maxCount: 1 },
|
|
154
|
+
{ name: 'photos', maxCount: 5 },
|
|
155
|
+
],
|
|
156
|
+
})
|
|
157
|
+
)
|
|
158
|
+
uploadFields(@Body() body: any) {
|
|
159
|
+
return {
|
|
160
|
+
message: 'Files uploaded successfully',
|
|
161
|
+
avatar: body.avatar,
|
|
162
|
+
photos: body.photos,
|
|
163
|
+
};
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
```
|
|
167
|
+
|
|
168
|
+
### Step 3: Use File Storage Service
|
|
169
|
+
|
|
170
|
+
```typescript
|
|
171
|
+
// file.service.ts
|
|
172
|
+
import { Injectable } from '@nestjs/common';
|
|
173
|
+
import { FileStorageService } from '@ackplus/nest-file-storage';
|
|
174
|
+
|
|
175
|
+
@Injectable()
|
|
176
|
+
export class FileService {
|
|
177
|
+
// Get file
|
|
178
|
+
async getFile(key: string): Promise<Buffer> {
|
|
179
|
+
const storage = await FileStorageService.getStorage();
|
|
180
|
+
return await storage.getFile(key);
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
// Delete file
|
|
184
|
+
async deleteFile(key: string): Promise<void> {
|
|
185
|
+
const storage = await FileStorageService.getStorage();
|
|
186
|
+
await storage.deleteFile(key);
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
// Copy file
|
|
190
|
+
async copyFile(oldKey: string, newKey: string) {
|
|
191
|
+
const storage = await FileStorageService.getStorage();
|
|
192
|
+
return await storage.copyFile(oldKey, newKey);
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
// Get public URL
|
|
196
|
+
async getFileUrl(key: string): Promise<string> {
|
|
197
|
+
const storage = await FileStorageService.getStorage();
|
|
198
|
+
return storage.getUrl(key);
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
// Get signed URL (S3 only)
|
|
202
|
+
async getSignedUrl(key: string): Promise<string> {
|
|
203
|
+
const storage = await FileStorageService.getStorage();
|
|
204
|
+
if ('getSignedUrl' in storage) {
|
|
205
|
+
return await storage.getSignedUrl(key, { expiresIn: 3600 });
|
|
206
|
+
}
|
|
207
|
+
return storage.getUrl(key);
|
|
208
|
+
}
|
|
209
|
+
}
|
|
210
|
+
```
|
|
211
|
+
|
|
212
|
+
## 📚 Configuration Options
|
|
213
|
+
|
|
214
|
+
### Local Storage Options
|
|
215
|
+
|
|
216
|
+
```typescript
|
|
217
|
+
interface LocalStorageOptions {
|
|
218
|
+
rootPath: string; // Directory to store files
|
|
219
|
+
baseUrl: string; // Base URL for file access
|
|
220
|
+
prefix?: string; // Optional prefix for file keys
|
|
221
|
+
fileName?: (file: any, req: Request) => string; // Custom file naming
|
|
222
|
+
fileDist?: (file: any, req: Request) => string; // Custom directory structure
|
|
223
|
+
transformUploadedFileObject?: (file: any) => any; // Transform uploaded file object
|
|
224
|
+
}
|
|
225
|
+
```
|
|
226
|
+
|
|
227
|
+
### S3 Storage Options
|
|
228
|
+
|
|
229
|
+
```typescript
|
|
230
|
+
interface S3StorageOptions {
|
|
231
|
+
accessKeyId: string; // AWS access key
|
|
232
|
+
secretAccessKey: string; // AWS secret key
|
|
233
|
+
region: string; // AWS region
|
|
234
|
+
bucket: string; // S3 bucket name
|
|
235
|
+
endpoint?: string; // Custom S3 endpoint (for S3-compatible services)
|
|
236
|
+
cloudFrontUrl?: string; // CloudFront distribution URL
|
|
237
|
+
prefix?: string; // Optional prefix for file keys
|
|
238
|
+
fileName?: (file: any, req: Request) => string; // Custom file naming
|
|
239
|
+
fileDist?: (file: any, req: Request) => string; // Custom directory structure
|
|
240
|
+
transformUploadedFileObject?: (file: any) => any; // Transform uploaded file object
|
|
241
|
+
}
|
|
242
|
+
```
|
|
243
|
+
|
|
244
|
+
### Azure Storage Options
|
|
245
|
+
|
|
246
|
+
```typescript
|
|
247
|
+
interface AzureStorageOptions {
|
|
248
|
+
account: string; // Azure storage account name
|
|
249
|
+
accountKey: string; // Azure storage account key
|
|
250
|
+
container: string; // Container name
|
|
251
|
+
prefix?: string; // Optional prefix for file keys
|
|
252
|
+
fileName?: (file: any, req: Request) => string; // Custom file naming
|
|
253
|
+
fileDist?: (file: any, req: Request) => string; // Custom directory structure
|
|
254
|
+
transformUploadedFileObject?: (file: any) => any; // Transform uploaded file object
|
|
255
|
+
}
|
|
256
|
+
```
|
|
257
|
+
|
|
258
|
+
## 🎨 Advanced Usage
|
|
259
|
+
|
|
260
|
+
### Custom File Naming
|
|
261
|
+
|
|
262
|
+
```typescript
|
|
263
|
+
NestFileStorageModule.forRoot({
|
|
264
|
+
storage: FileStorageEnum.LOCAL,
|
|
265
|
+
localConfig: {
|
|
266
|
+
rootPath: './uploads',
|
|
267
|
+
baseUrl: 'http://localhost:3000/uploads',
|
|
268
|
+
fileName: (file, req) => {
|
|
269
|
+
// Custom file name with timestamp
|
|
270
|
+
const timestamp = Date.now();
|
|
271
|
+
const ext = file.originalname.split('.').pop();
|
|
272
|
+
return `${timestamp}-${file.originalname}`;
|
|
273
|
+
},
|
|
274
|
+
},
|
|
275
|
+
})
|
|
276
|
+
```
|
|
277
|
+
|
|
278
|
+
### Custom Directory Structure
|
|
279
|
+
|
|
280
|
+
```typescript
|
|
281
|
+
NestFileStorageModule.forRoot({
|
|
282
|
+
storage: FileStorageEnum.LOCAL,
|
|
283
|
+
localConfig: {
|
|
284
|
+
rootPath: './uploads',
|
|
285
|
+
baseUrl: 'http://localhost:3000/uploads',
|
|
286
|
+
fileDist: (file, req) => {
|
|
287
|
+
// Organize by year/month/day
|
|
288
|
+
const date = new Date();
|
|
289
|
+
return `${date.getFullYear()}/${date.getMonth() + 1}/${date.getDate()}`;
|
|
290
|
+
},
|
|
291
|
+
},
|
|
292
|
+
})
|
|
293
|
+
```
|
|
294
|
+
|
|
295
|
+
### Transform Uploaded File Object
|
|
296
|
+
|
|
297
|
+
```typescript
|
|
298
|
+
NestFileStorageModule.forRoot({
|
|
299
|
+
storage: FileStorageEnum.S3,
|
|
300
|
+
s3Config: {
|
|
301
|
+
accessKeyId: process.env.AWS_ACCESS_KEY_ID,
|
|
302
|
+
secretAccessKey: process.env.AWS_SECRET_ACCESS_KEY,
|
|
303
|
+
region: process.env.AWS_REGION,
|
|
304
|
+
bucket: process.env.AWS_BUCKET,
|
|
305
|
+
transformUploadedFileObject: (file) => {
|
|
306
|
+
// Return only specific fields
|
|
307
|
+
return {
|
|
308
|
+
key: file.key,
|
|
309
|
+
url: file.url,
|
|
310
|
+
size: file.size,
|
|
311
|
+
mimetype: file.mimetype,
|
|
312
|
+
};
|
|
313
|
+
},
|
|
314
|
+
},
|
|
315
|
+
})
|
|
316
|
+
```
|
|
317
|
+
|
|
318
|
+
### Custom File Mapping in Interceptor
|
|
319
|
+
|
|
320
|
+
```typescript
|
|
321
|
+
@Post('upload')
|
|
322
|
+
@UseInterceptors(
|
|
323
|
+
FileStorageInterceptor('file', {
|
|
324
|
+
mapToRequestBody: (file, fieldName, req) => {
|
|
325
|
+
// Return full file object instead of just key
|
|
326
|
+
return file;
|
|
327
|
+
},
|
|
328
|
+
})
|
|
329
|
+
)
|
|
330
|
+
uploadFile(@Body() body: any) {
|
|
331
|
+
// body.file now contains the full file object
|
|
332
|
+
return {
|
|
333
|
+
message: 'File uploaded',
|
|
334
|
+
file: body.file,
|
|
335
|
+
};
|
|
336
|
+
}
|
|
337
|
+
```
|
|
338
|
+
|
|
339
|
+
### Async Configuration
|
|
340
|
+
|
|
341
|
+
```typescript
|
|
342
|
+
// app.module.ts
|
|
343
|
+
NestFileStorageModule.forRootAsync({
|
|
344
|
+
imports: [ConfigModule],
|
|
345
|
+
useFactory: async (configService: ConfigService) => ({
|
|
346
|
+
storage: FileStorageEnum.S3,
|
|
347
|
+
s3Config: {
|
|
348
|
+
accessKeyId: configService.get('AWS_ACCESS_KEY_ID'),
|
|
349
|
+
secretAccessKey: configService.get('AWS_SECRET_ACCESS_KEY'),
|
|
350
|
+
region: configService.get('AWS_REGION'),
|
|
351
|
+
bucket: configService.get('AWS_BUCKET'),
|
|
352
|
+
},
|
|
353
|
+
}),
|
|
354
|
+
inject: [ConfigService],
|
|
355
|
+
})
|
|
356
|
+
```
|
|
357
|
+
|
|
358
|
+
### Dynamic Storage Type
|
|
359
|
+
|
|
360
|
+
```typescript
|
|
361
|
+
// Override storage type per route
|
|
362
|
+
@Post('upload-to-s3')
|
|
363
|
+
@UseInterceptors(
|
|
364
|
+
FileStorageInterceptor('file', {
|
|
365
|
+
storageType: FileStorageEnum.S3,
|
|
366
|
+
})
|
|
367
|
+
)
|
|
368
|
+
uploadToS3(@Body() body: any) {
|
|
369
|
+
return { fileKey: body.file };
|
|
370
|
+
}
|
|
371
|
+
```
|
|
372
|
+
|
|
373
|
+
## 🔥 Complete Examples
|
|
374
|
+
|
|
375
|
+
### Image Upload with Validation
|
|
376
|
+
|
|
377
|
+
```typescript
|
|
378
|
+
import { Controller, Post, UseInterceptors, BadRequestException } from '@nestjs/common';
|
|
379
|
+
import { FileStorageInterceptor } from '@ackplus/nest-file-storage';
|
|
380
|
+
|
|
381
|
+
@Controller('images')
|
|
382
|
+
export class ImageController {
|
|
383
|
+
@Post('upload')
|
|
384
|
+
@UseInterceptors(
|
|
385
|
+
FileStorageInterceptor('image', {
|
|
386
|
+
fileName: (file, req) => {
|
|
387
|
+
// Validate image type
|
|
388
|
+
const allowedTypes = ['image/jpeg', 'image/png', 'image/gif'];
|
|
389
|
+
if (!allowedTypes.includes(file.mimetype)) {
|
|
390
|
+
throw new BadRequestException('Only image files are allowed');
|
|
391
|
+
}
|
|
392
|
+
|
|
393
|
+
// Generate unique filename
|
|
394
|
+
const timestamp = Date.now();
|
|
395
|
+
const ext = file.originalname.split('.').pop();
|
|
396
|
+
return `image-${timestamp}.${ext}`;
|
|
397
|
+
},
|
|
398
|
+
fileDist: (file, req) => {
|
|
399
|
+
// Organize by year/month
|
|
400
|
+
const date = new Date();
|
|
401
|
+
return `images/${date.getFullYear()}/${date.getMonth() + 1}`;
|
|
402
|
+
},
|
|
403
|
+
})
|
|
404
|
+
)
|
|
405
|
+
async uploadImage(@Body() body: any) {
|
|
406
|
+
return {
|
|
407
|
+
message: 'Image uploaded successfully',
|
|
408
|
+
imageKey: body.image,
|
|
409
|
+
};
|
|
410
|
+
}
|
|
411
|
+
}
|
|
412
|
+
```
|
|
413
|
+
|
|
414
|
+
### User Avatar Upload
|
|
415
|
+
|
|
416
|
+
```typescript
|
|
417
|
+
import { Controller, Post, UseInterceptors, Body } from '@nestjs/common';
|
|
418
|
+
import { FileStorageInterceptor, FileStorageService } from '@ackplus/nest-file-storage';
|
|
419
|
+
|
|
420
|
+
@Controller('users')
|
|
421
|
+
export class UserController {
|
|
422
|
+
constructor(private readonly userService: UserService) {}
|
|
423
|
+
|
|
424
|
+
@Post('avatar')
|
|
425
|
+
@UseInterceptors(
|
|
426
|
+
FileStorageInterceptor('avatar', {
|
|
427
|
+
fileName: (file, req) => {
|
|
428
|
+
const userId = req.user.id; // Assuming user from auth guard
|
|
429
|
+
const ext = file.originalname.split('.').pop();
|
|
430
|
+
return `avatar-${userId}.${ext}`;
|
|
431
|
+
},
|
|
432
|
+
fileDist: () => 'avatars',
|
|
433
|
+
})
|
|
434
|
+
)
|
|
435
|
+
async uploadAvatar(@Body() body: any, @Request() req) {
|
|
436
|
+
// Delete old avatar if exists
|
|
437
|
+
const user = await this.userService.findById(req.user.id);
|
|
438
|
+
if (user.avatarKey) {
|
|
439
|
+
const storage = await FileStorageService.getStorage();
|
|
440
|
+
await storage.deleteFile(user.avatarKey);
|
|
441
|
+
}
|
|
442
|
+
|
|
443
|
+
// Update user with new avatar
|
|
444
|
+
await this.userService.updateAvatar(req.user.id, body.avatar);
|
|
445
|
+
|
|
446
|
+
return {
|
|
447
|
+
message: 'Avatar updated successfully',
|
|
448
|
+
avatarKey: body.avatar,
|
|
449
|
+
};
|
|
450
|
+
}
|
|
451
|
+
}
|
|
452
|
+
```
|
|
453
|
+
|
|
454
|
+
### Document Management
|
|
455
|
+
|
|
456
|
+
```typescript
|
|
457
|
+
import { Controller, Get, Post, Delete, Param, UseInterceptors, Body } from '@nestjs/common';
|
|
458
|
+
import { FileStorageInterceptor, FileStorageService } from '@ackplus/nest-file-storage';
|
|
459
|
+
|
|
460
|
+
@Controller('documents')
|
|
461
|
+
export class DocumentController {
|
|
462
|
+
@Post('upload')
|
|
463
|
+
@UseInterceptors(
|
|
464
|
+
FileStorageInterceptor({
|
|
465
|
+
type: 'array',
|
|
466
|
+
fieldName: 'documents',
|
|
467
|
+
maxCount: 10,
|
|
468
|
+
}, {
|
|
469
|
+
fileDist: () => 'documents',
|
|
470
|
+
mapToRequestBody: (files, fieldName) => {
|
|
471
|
+
// Return detailed file info
|
|
472
|
+
return files;
|
|
473
|
+
},
|
|
474
|
+
})
|
|
475
|
+
)
|
|
476
|
+
async uploadDocuments(@Body() body: any) {
|
|
477
|
+
return {
|
|
478
|
+
message: `${body.documents.length} documents uploaded`,
|
|
479
|
+
documents: body.documents,
|
|
480
|
+
};
|
|
481
|
+
}
|
|
482
|
+
|
|
483
|
+
@Get(':key/download')
|
|
484
|
+
async downloadDocument(@Param('key') key: string, @Res() res) {
|
|
485
|
+
const storage = await FileStorageService.getStorage();
|
|
486
|
+
const file = await storage.getFile(key);
|
|
487
|
+
|
|
488
|
+
res.setHeader('Content-Type', 'application/octet-stream');
|
|
489
|
+
res.setHeader('Content-Disposition', `attachment; filename="${key}"`);
|
|
490
|
+
res.send(file);
|
|
491
|
+
}
|
|
492
|
+
|
|
493
|
+
@Delete(':key')
|
|
494
|
+
async deleteDocument(@Param('key') key: string) {
|
|
495
|
+
const storage = await FileStorageService.getStorage();
|
|
496
|
+
await storage.deleteFile(key);
|
|
497
|
+
|
|
498
|
+
return { message: 'Document deleted successfully' };
|
|
499
|
+
}
|
|
500
|
+
|
|
501
|
+
@Get(':key/url')
|
|
502
|
+
async getDocumentUrl(@Param('key') key: string) {
|
|
503
|
+
const storage = await FileStorageService.getStorage();
|
|
504
|
+
const url = storage.getUrl(key);
|
|
505
|
+
|
|
506
|
+
return { url };
|
|
507
|
+
}
|
|
508
|
+
}
|
|
509
|
+
```
|
|
510
|
+
|
|
511
|
+
## 📚 API Reference
|
|
512
|
+
|
|
513
|
+
### FileStorageService
|
|
514
|
+
|
|
515
|
+
```typescript
|
|
516
|
+
class FileStorageService {
|
|
517
|
+
// Get storage instance
|
|
518
|
+
static async getStorage(storageType?: FileStorageEnum): Promise<Storage>
|
|
519
|
+
|
|
520
|
+
// Get module options
|
|
521
|
+
static getOptions(): FileStorageModuleOptions
|
|
522
|
+
|
|
523
|
+
// Set module options
|
|
524
|
+
static setOptions(options: FileStorageModuleOptions): void
|
|
525
|
+
}
|
|
526
|
+
```
|
|
527
|
+
|
|
528
|
+
### Storage Interface
|
|
529
|
+
|
|
530
|
+
```typescript
|
|
531
|
+
interface Storage {
|
|
532
|
+
// Get file content as Buffer
|
|
533
|
+
getFile(key: string): Promise<Buffer> | Buffer
|
|
534
|
+
|
|
535
|
+
// Delete file
|
|
536
|
+
deleteFile(key: string): Promise<void> | void
|
|
537
|
+
|
|
538
|
+
// Upload file
|
|
539
|
+
putFile(fileContent: Buffer, key: string): Promise<UploadedFile> | UploadedFile
|
|
540
|
+
|
|
541
|
+
// Copy file
|
|
542
|
+
copyFile(oldKey: string, newKey: string): Promise<UploadedFile>
|
|
543
|
+
|
|
544
|
+
// Get file URL
|
|
545
|
+
getUrl(key: string): Promise<string> | string
|
|
546
|
+
|
|
547
|
+
// Get signed URL (S3 only)
|
|
548
|
+
getSignedUrl?(key: string, options: any): Promise<string> | string
|
|
549
|
+
|
|
550
|
+
// Get file path (Local only)
|
|
551
|
+
path?(filePath: string): Promise<string> | string
|
|
552
|
+
}
|
|
553
|
+
```
|
|
554
|
+
|
|
555
|
+
### FileStorageInterceptor
|
|
556
|
+
|
|
557
|
+
```typescript
|
|
558
|
+
// Single file upload
|
|
559
|
+
FileStorageInterceptor(
|
|
560
|
+
fieldName: string,
|
|
561
|
+
options?: FileStorageInterceptorOptions
|
|
562
|
+
)
|
|
563
|
+
|
|
564
|
+
// Multiple files or fields
|
|
565
|
+
FileStorageInterceptor(
|
|
566
|
+
config: {
|
|
567
|
+
type: 'single' | 'array' | 'fields';
|
|
568
|
+
fieldName?: string;
|
|
569
|
+
maxCount?: number;
|
|
570
|
+
fields?: { name: string; maxCount?: number }[];
|
|
571
|
+
},
|
|
572
|
+
options?: FileStorageInterceptorOptions
|
|
573
|
+
)
|
|
574
|
+
```
|
|
575
|
+
|
|
576
|
+
### UploadedFile Interface
|
|
577
|
+
|
|
578
|
+
```typescript
|
|
579
|
+
interface UploadedFile {
|
|
580
|
+
fieldName?: string; // Form field name
|
|
581
|
+
fileName: string; // Generated file name
|
|
582
|
+
originalName: string; // Original file name
|
|
583
|
+
size: number; // File size in bytes
|
|
584
|
+
mimetype?: string; // MIME type
|
|
585
|
+
buffer?: Buffer; // File buffer (optional)
|
|
586
|
+
key: string; // Storage key/path
|
|
587
|
+
url: string; // Public URL
|
|
588
|
+
fullPath: string; // Full storage path
|
|
589
|
+
encoding?: string; // File encoding
|
|
590
|
+
}
|
|
591
|
+
```
|
|
592
|
+
|
|
593
|
+
## 🧪 Testing
|
|
594
|
+
|
|
595
|
+
```typescript
|
|
596
|
+
import { Test, TestingModule } from '@nestjs/testing';
|
|
597
|
+
import { NestFileStorageModule, FileStorageService, FileStorageEnum } from '@ackplus/nest-file-storage';
|
|
598
|
+
|
|
599
|
+
describe('FileService', () => {
|
|
600
|
+
let service: FileService;
|
|
601
|
+
let storage: Storage;
|
|
602
|
+
|
|
603
|
+
beforeEach(async () => {
|
|
604
|
+
const module: TestingModule = await Test.createTestingModule({
|
|
605
|
+
imports: [
|
|
606
|
+
NestFileStorageModule.forRoot({
|
|
607
|
+
storage: FileStorageEnum.LOCAL,
|
|
608
|
+
localConfig: {
|
|
609
|
+
rootPath: './test-uploads',
|
|
610
|
+
baseUrl: 'http://localhost:3000/test-uploads',
|
|
611
|
+
},
|
|
612
|
+
}),
|
|
613
|
+
],
|
|
614
|
+
providers: [FileService],
|
|
615
|
+
}).compile();
|
|
616
|
+
|
|
617
|
+
service = module.get<FileService>(FileService);
|
|
618
|
+
storage = await FileStorageService.getStorage();
|
|
619
|
+
});
|
|
620
|
+
|
|
621
|
+
it('should upload file', async () => {
|
|
622
|
+
const buffer = Buffer.from('test content');
|
|
623
|
+
const result = await storage.putFile(buffer, 'test/file.txt');
|
|
624
|
+
|
|
625
|
+
expect(result.key).toBe('test/file.txt');
|
|
626
|
+
expect(result.size).toBeGreaterThan(0);
|
|
627
|
+
});
|
|
628
|
+
|
|
629
|
+
it('should delete file', async () => {
|
|
630
|
+
const buffer = Buffer.from('test content');
|
|
631
|
+
await storage.putFile(buffer, 'test/file.txt');
|
|
632
|
+
|
|
633
|
+
await storage.deleteFile('test/file.txt');
|
|
634
|
+
|
|
635
|
+
await expect(storage.getFile('test/file.txt')).rejects.toThrow();
|
|
636
|
+
});
|
|
637
|
+
});
|
|
638
|
+
```
|
|
639
|
+
|
|
640
|
+
## 🤝 Contributing
|
|
641
|
+
|
|
642
|
+
Contributions are welcome! Please feel free to submit a Pull Request.
|
|
643
|
+
|
|
644
|
+
## 📄 License
|
|
645
|
+
|
|
646
|
+
This project is licensed under the MIT License.
|
|
647
|
+
|
|
648
|
+
## 🙏 Acknowledgments
|
|
649
|
+
|
|
650
|
+
- Built with [NestJS](https://nestjs.com/)
|
|
651
|
+
- Uses [Multer](https://github.com/expressjs/multer) for file handling
|
|
652
|
+
- AWS S3 support via [@aws-sdk/client-s3](https://www.npmjs.com/package/@aws-sdk/client-s3)
|
|
653
|
+
- Azure support via [@azure/storage-blob](https://www.npmjs.com/package/@azure/storage-blob)
|
|
654
|
+
|
|
655
|
+
## 📮 Support
|
|
656
|
+
|
|
657
|
+
If you have any questions or need help:
|
|
658
|
+
- Open an issue on [GitHub](https://github.com/ack-solutions/nest-file-storage/issues)
|
|
659
|
+
- Check the [examples](./examples/) directory
|
|
660
|
+
|
|
661
|
+
---
|
|
662
|
+
|
|
663
|
+
Made with ❤️ for the NestJS community
|
package/{src → dist}/index.d.ts
RENAMED
package/dist/index.js
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
|
3
|
+
if (k2 === undefined) k2 = k;
|
|
4
|
+
var desc = Object.getOwnPropertyDescriptor(m, k);
|
|
5
|
+
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
|
6
|
+
desc = { enumerable: true, get: function() { return m[k]; } };
|
|
7
|
+
}
|
|
8
|
+
Object.defineProperty(o, k2, desc);
|
|
9
|
+
}) : (function(o, m, k, k2) {
|
|
10
|
+
if (k2 === undefined) k2 = k;
|
|
11
|
+
o[k2] = m[k];
|
|
12
|
+
}));
|
|
13
|
+
var __exportStar = (this && this.__exportStar) || function(m, exports) {
|
|
14
|
+
for (var p in m) if (p !== "default" && !Object.prototype.hasOwnProperty.call(exports, p)) __createBinding(exports, m, p);
|
|
15
|
+
};
|
|
16
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
17
|
+
__exportStar(require("./lib/nest-file-storage.module"), exports);
|
|
18
|
+
__exportStar(require("./lib/file-storage.service"), exports);
|
|
19
|
+
__exportStar(require("./lib/interceptor/file-storage.interceptor"), exports);
|
|
20
|
+
__exportStar(require("./lib/types"), exports);
|
|
21
|
+
//# sourceMappingURL=index.js.map
|