@ackplus/nest-file-storage 1.1.20 โ 1.1.23
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/README.md +537 -485
- package/dist/lib/interceptor/file-storage.interceptor.js +6 -4
- package/dist/lib/interceptor/file-storage.interceptor.js.map +1 -1
- package/dist/lib/storage/azure.storage.js +8 -2
- package/dist/lib/storage/azure.storage.js.map +1 -1
- package/dist/lib/storage/local.storage.js +8 -2
- package/dist/lib/storage/local.storage.js.map +1 -1
- package/dist/lib/storage/s3.storage.js +8 -5
- package/dist/lib/storage/s3.storage.js.map +1 -1
- package/dist/tsconfig.build.tsbuildinfo +1 -1
- package/package.json +1 -2
package/README.md
CHANGED
|
@@ -1,52 +1,72 @@
|
|
|
1
1
|
# @ackplus/nest-file-storage
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
`@ackplus/nest-file-storage` is a NestJS file upload and storage library for Express-based applications. It connects Multer uploads to Local storage, AWS S3, or Azure Blob Storage and gives you one common runtime API for upload, read, delete, copy, and URL generation.
|
|
4
4
|
|
|
5
|
-
|
|
5
|
+
This README is the canonical developer guide for using the package in an application.
|
|
6
6
|
|
|
7
|
-
|
|
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
|
|
7
|
+
## What You Get
|
|
15
8
|
|
|
16
|
-
|
|
9
|
+
- `NestFileStorageModule` to register the default storage provider.
|
|
10
|
+
- `FileStorageInterceptor()` to accept multipart uploads in controllers.
|
|
11
|
+
- `FileStorageService` to work with files programmatically.
|
|
12
|
+
- Built-in storage adapters for `local`, `s3`, and `azure`.
|
|
13
|
+
- Request-body mapping so uploaded file keys or metadata can flow into your DTO/service layer.
|
|
14
|
+
|
|
15
|
+
## Compatibility And Prerequisites
|
|
16
|
+
|
|
17
|
+
This library is designed for NestJS on the Express platform.
|
|
18
|
+
|
|
19
|
+
Required in the consuming app:
|
|
20
|
+
|
|
21
|
+
- `@nestjs/common`
|
|
22
|
+
- `@nestjs/core`
|
|
23
|
+
- `@nestjs/platform-express`
|
|
24
|
+
- `multer`
|
|
25
|
+
- `reflect-metadata`
|
|
26
|
+
|
|
27
|
+
Peer dependency ranges exported by the package:
|
|
28
|
+
|
|
29
|
+
- `@nestjs/common`: `^10.0.0 || ^11.0.0`
|
|
30
|
+
- `@nestjs/core`: `^10.0.0 || ^11.0.0`
|
|
31
|
+
- `multer`: `^1.4.5-lts.1 || ^2.0.0`
|
|
32
|
+
- `reflect-metadata`: `^0.2.2`
|
|
33
|
+
|
|
34
|
+
Optional provider packages:
|
|
35
|
+
|
|
36
|
+
- AWS S3: `@aws-sdk/client-s3` and `@aws-sdk/s3-request-presigner`
|
|
37
|
+
- Azure Blob Storage: `@azure/storage-blob`
|
|
38
|
+
|
|
39
|
+
## Installation
|
|
17
40
|
|
|
18
41
|
```bash
|
|
19
|
-
|
|
20
|
-
# or
|
|
21
|
-
pnpm add @ackplus/nest-file-storage
|
|
22
|
-
# or
|
|
23
|
-
yarn add @ackplus/nest-file-storage
|
|
42
|
+
pnpm add @ackplus/nest-file-storage multer reflect-metadata
|
|
24
43
|
```
|
|
25
44
|
|
|
26
|
-
|
|
45
|
+
If your Nest app does not already include the Express platform package:
|
|
27
46
|
|
|
28
47
|
```bash
|
|
29
|
-
|
|
48
|
+
pnpm add @nestjs/platform-express
|
|
30
49
|
```
|
|
31
50
|
|
|
32
|
-
|
|
51
|
+
For AWS S3 support:
|
|
33
52
|
|
|
34
53
|
```bash
|
|
35
|
-
|
|
54
|
+
pnpm add @aws-sdk/client-s3 @aws-sdk/s3-request-presigner
|
|
36
55
|
```
|
|
37
56
|
|
|
38
|
-
|
|
57
|
+
For Azure Blob Storage support:
|
|
39
58
|
|
|
40
|
-
|
|
59
|
+
```bash
|
|
60
|
+
pnpm add @azure/storage-blob
|
|
61
|
+
```
|
|
41
62
|
|
|
42
|
-
|
|
63
|
+
## Register The Module
|
|
43
64
|
|
|
44
|
-
|
|
65
|
+
### Local storage
|
|
45
66
|
|
|
46
|
-
```
|
|
47
|
-
// app.module.ts
|
|
67
|
+
```ts
|
|
48
68
|
import { Module } from '@nestjs/common';
|
|
49
|
-
import {
|
|
69
|
+
import { FileStorageEnum, NestFileStorageModule } from '@ackplus/nest-file-storage';
|
|
50
70
|
|
|
51
71
|
@Module({
|
|
52
72
|
imports: [
|
|
@@ -62,22 +82,22 @@ import { NestFileStorageModule, FileStorageEnum } from '@ackplus/nest-file-stora
|
|
|
62
82
|
export class AppModule {}
|
|
63
83
|
```
|
|
64
84
|
|
|
65
|
-
|
|
85
|
+
### AWS S3
|
|
66
86
|
|
|
67
|
-
```
|
|
68
|
-
// app.module.ts
|
|
87
|
+
```ts
|
|
69
88
|
import { Module } from '@nestjs/common';
|
|
70
|
-
import {
|
|
89
|
+
import { FileStorageEnum, NestFileStorageModule } from '@ackplus/nest-file-storage';
|
|
71
90
|
|
|
72
91
|
@Module({
|
|
73
92
|
imports: [
|
|
74
93
|
NestFileStorageModule.forRoot({
|
|
75
94
|
storage: FileStorageEnum.S3,
|
|
76
95
|
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
|
|
96
|
+
accessKeyId: process.env.AWS_ACCESS_KEY_ID!,
|
|
97
|
+
secretAccessKey: process.env.AWS_SECRET_ACCESS_KEY!,
|
|
98
|
+
region: process.env.AWS_REGION!,
|
|
99
|
+
bucket: process.env.AWS_BUCKET!,
|
|
100
|
+
cloudFrontUrl: process.env.AWS_CLOUDFRONT_URL,
|
|
81
101
|
},
|
|
82
102
|
}),
|
|
83
103
|
],
|
|
@@ -85,21 +105,20 @@ import { NestFileStorageModule, FileStorageEnum } from '@ackplus/nest-file-stora
|
|
|
85
105
|
export class AppModule {}
|
|
86
106
|
```
|
|
87
107
|
|
|
88
|
-
|
|
108
|
+
### Azure Blob Storage
|
|
89
109
|
|
|
90
|
-
```
|
|
91
|
-
// app.module.ts
|
|
110
|
+
```ts
|
|
92
111
|
import { Module } from '@nestjs/common';
|
|
93
|
-
import {
|
|
112
|
+
import { FileStorageEnum, NestFileStorageModule } from '@ackplus/nest-file-storage';
|
|
94
113
|
|
|
95
114
|
@Module({
|
|
96
115
|
imports: [
|
|
97
116
|
NestFileStorageModule.forRoot({
|
|
98
117
|
storage: FileStorageEnum.AZURE,
|
|
99
118
|
azureConfig: {
|
|
100
|
-
account: process.env.AZURE_STORAGE_ACCOUNT
|
|
101
|
-
accountKey: process.env.AZURE_STORAGE_KEY
|
|
102
|
-
container: process.env.AZURE_CONTAINER
|
|
119
|
+
account: process.env.AZURE_STORAGE_ACCOUNT!,
|
|
120
|
+
accountKey: process.env.AZURE_STORAGE_KEY!,
|
|
121
|
+
container: process.env.AZURE_CONTAINER!,
|
|
103
122
|
},
|
|
104
123
|
}),
|
|
105
124
|
],
|
|
@@ -107,557 +126,590 @@ import { NestFileStorageModule, FileStorageEnum } from '@ackplus/nest-file-stora
|
|
|
107
126
|
export class AppModule {}
|
|
108
127
|
```
|
|
109
128
|
|
|
110
|
-
###
|
|
129
|
+
### Async configuration
|
|
130
|
+
|
|
131
|
+
```ts
|
|
132
|
+
import { Module } from '@nestjs/common';
|
|
133
|
+
import { ConfigModule, ConfigService } from '@nestjs/config';
|
|
134
|
+
import { FileStorageEnum, NestFileStorageModule } from '@ackplus/nest-file-storage';
|
|
135
|
+
|
|
136
|
+
@Module({
|
|
137
|
+
imports: [
|
|
138
|
+
ConfigModule.forRoot(),
|
|
139
|
+
NestFileStorageModule.forRootAsync({
|
|
140
|
+
imports: [ConfigModule],
|
|
141
|
+
inject: [ConfigService],
|
|
142
|
+
useFactory: async (config: ConfigService) => ({
|
|
143
|
+
storage: FileStorageEnum.S3,
|
|
144
|
+
s3Config: {
|
|
145
|
+
accessKeyId: config.getOrThrow('AWS_ACCESS_KEY_ID'),
|
|
146
|
+
secretAccessKey: config.getOrThrow('AWS_SECRET_ACCESS_KEY'),
|
|
147
|
+
region: config.getOrThrow('AWS_REGION'),
|
|
148
|
+
bucket: config.getOrThrow('AWS_BUCKET'),
|
|
149
|
+
},
|
|
150
|
+
}),
|
|
151
|
+
}),
|
|
152
|
+
],
|
|
153
|
+
})
|
|
154
|
+
export class AppModule {}
|
|
155
|
+
```
|
|
156
|
+
|
|
157
|
+
### Example: build module options from one shared storage config
|
|
158
|
+
|
|
159
|
+
If your app stores provider credentials in one common config record, map that record into the module options once and return the correct provider config.
|
|
160
|
+
|
|
161
|
+
```ts
|
|
162
|
+
import * as path from 'path';
|
|
163
|
+
import { FileStorageEnum } from '@ackplus/nest-file-storage';
|
|
164
|
+
|
|
165
|
+
function buildFileStorageOptions(config: {
|
|
166
|
+
type: FileStorageEnum;
|
|
167
|
+
endpoint?: string;
|
|
168
|
+
accessKeyId?: string;
|
|
169
|
+
secretAccessKey?: string;
|
|
170
|
+
region?: string;
|
|
171
|
+
bucket?: string;
|
|
172
|
+
cloudFrontUrl?: string;
|
|
173
|
+
}, appConfig: { appUrl: string }) {
|
|
174
|
+
if (config.type === FileStorageEnum.S3) {
|
|
175
|
+
return {
|
|
176
|
+
storage: FileStorageEnum.S3,
|
|
177
|
+
s3Config: {
|
|
178
|
+
endpoint: config.endpoint,
|
|
179
|
+
accessKeyId: config.accessKeyId!,
|
|
180
|
+
secretAccessKey: config.secretAccessKey!,
|
|
181
|
+
region: config.region!,
|
|
182
|
+
bucket: config.bucket!,
|
|
183
|
+
cloudFrontUrl: config.cloudFrontUrl,
|
|
184
|
+
},
|
|
185
|
+
};
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
if (config.type === FileStorageEnum.AZURE) {
|
|
189
|
+
return {
|
|
190
|
+
storage: FileStorageEnum.AZURE,
|
|
191
|
+
azureConfig: {
|
|
192
|
+
account: config.accessKeyId!,
|
|
193
|
+
accountKey: config.secretAccessKey!,
|
|
194
|
+
container: config.bucket!,
|
|
195
|
+
},
|
|
196
|
+
};
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
return {
|
|
200
|
+
storage: FileStorageEnum.LOCAL,
|
|
201
|
+
localConfig: {
|
|
202
|
+
rootPath: path.join(process.cwd(), 'public'),
|
|
203
|
+
baseUrl: `${appConfig.appUrl}/public`,
|
|
204
|
+
},
|
|
205
|
+
};
|
|
206
|
+
}
|
|
207
|
+
```
|
|
208
|
+
|
|
209
|
+
This pattern is useful when:
|
|
210
|
+
|
|
211
|
+
- S3 uses `endpoint`, `accessKeyId`, `secretAccessKey`, `region`, `bucket`, and optional `cloudFrontUrl`.
|
|
212
|
+
- Azure reuses the same credential source but maps `accessKeyId -> account`, `secretAccessKey -> accountKey`, and `bucket -> container`.
|
|
213
|
+
- Local storage falls back to a public directory such as `process.cwd()/public`.
|
|
214
|
+
|
|
215
|
+
## Local Storage Note: `baseUrl` Does Not Serve Files
|
|
216
|
+
|
|
217
|
+
For local storage, `baseUrl` only controls the URL string returned by `getUrl()` and upload metadata. It does not expose the directory over HTTP by itself.
|
|
218
|
+
|
|
219
|
+
If you want browser-accessible local files, add static serving in your Nest app:
|
|
220
|
+
|
|
221
|
+
```bash
|
|
222
|
+
pnpm add @nestjs/serve-static
|
|
223
|
+
```
|
|
224
|
+
|
|
225
|
+
```ts
|
|
226
|
+
import { Module } from '@nestjs/common';
|
|
227
|
+
import { ServeStaticModule } from '@nestjs/serve-static';
|
|
228
|
+
import { join } from 'path';
|
|
229
|
+
|
|
230
|
+
@Module({
|
|
231
|
+
imports: [
|
|
232
|
+
ServeStaticModule.forRoot({
|
|
233
|
+
rootPath: join(process.cwd(), 'uploads'),
|
|
234
|
+
serveRoot: '/uploads',
|
|
235
|
+
}),
|
|
236
|
+
],
|
|
237
|
+
})
|
|
238
|
+
export class AppModule {}
|
|
239
|
+
```
|
|
111
240
|
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
241
|
+
Without this, local files can still be downloaded through your own controller endpoints.
|
|
242
|
+
|
|
243
|
+
## Upload Files In Controllers
|
|
244
|
+
|
|
245
|
+
`FileStorageInterceptor()` wraps Multer and uploads files into the configured storage engine before your route handler runs.
|
|
246
|
+
|
|
247
|
+
All upload routes must accept `multipart/form-data`.
|
|
248
|
+
|
|
249
|
+
### Default request-body mapping
|
|
250
|
+
|
|
251
|
+
By default, the interceptor writes storage keys into `request.body`.
|
|
252
|
+
|
|
253
|
+
| Upload mode | Accepted form field(s) | Default `body` value |
|
|
254
|
+
| --- | --- | --- |
|
|
255
|
+
| `FileStorageInterceptor('file')` | `file` | `body.file: string` |
|
|
256
|
+
| `type: 'array'` | repeated `files` or `files[0]`, `files[1]`, ... | `body.files: string[]` |
|
|
257
|
+
| `type: 'fields'` | each configured field name | `body[fieldName]: string[]` |
|
|
258
|
+
|
|
259
|
+
Important behavior for `fields` mode:
|
|
260
|
+
|
|
261
|
+
- Every configured field is mapped to an array.
|
|
262
|
+
- That stays true even when `maxCount: 1`.
|
|
263
|
+
|
|
264
|
+
DTO note:
|
|
265
|
+
|
|
266
|
+
- Because the interceptor writes into `request.body` before your route handler executes, your DTO shape should match the mapped value.
|
|
267
|
+
- Typical DTO fields are `string` for single uploads, `string[]` for array uploads, and custom object shapes when you use `mapToRequestBody`.
|
|
268
|
+
|
|
269
|
+
### Single file upload
|
|
270
|
+
|
|
271
|
+
```ts
|
|
272
|
+
import { Body, Controller, Post, UseInterceptors } from '@nestjs/common';
|
|
115
273
|
import { FileStorageInterceptor } from '@ackplus/nest-file-storage';
|
|
116
274
|
|
|
117
|
-
@Controller('
|
|
118
|
-
export class
|
|
119
|
-
// Single file upload
|
|
275
|
+
@Controller('files')
|
|
276
|
+
export class FilesController {
|
|
120
277
|
@Post('single')
|
|
121
278
|
@UseInterceptors(FileStorageInterceptor('file'))
|
|
122
|
-
uploadSingle(@Body() body:
|
|
123
|
-
// File key is automatically added to body.file
|
|
279
|
+
uploadSingle(@Body() body: { file: string }) {
|
|
124
280
|
return {
|
|
125
|
-
|
|
126
|
-
fileKey: body.file,
|
|
281
|
+
key: body.file,
|
|
127
282
|
};
|
|
128
283
|
}
|
|
284
|
+
}
|
|
285
|
+
```
|
|
129
286
|
|
|
130
|
-
|
|
287
|
+
### Multiple files in one field
|
|
288
|
+
|
|
289
|
+
```ts
|
|
290
|
+
import { Body, Controller, Post, UseInterceptors } from '@nestjs/common';
|
|
291
|
+
import { FileStorageInterceptor } from '@ackplus/nest-file-storage';
|
|
292
|
+
|
|
293
|
+
@Controller('files')
|
|
294
|
+
export class FilesController {
|
|
131
295
|
@Post('multiple')
|
|
132
296
|
@UseInterceptors(
|
|
133
297
|
FileStorageInterceptor({
|
|
134
298
|
type: 'array',
|
|
135
299
|
fieldName: 'files',
|
|
136
300
|
maxCount: 10,
|
|
137
|
-
})
|
|
301
|
+
}),
|
|
138
302
|
)
|
|
139
|
-
uploadMultiple(@Body() body:
|
|
140
|
-
// File keys are automatically added to body.files as array
|
|
303
|
+
uploadMultiple(@Body() body: { files: string[] }) {
|
|
141
304
|
return {
|
|
142
|
-
|
|
143
|
-
|
|
305
|
+
keys: body.files,
|
|
306
|
+
count: body.files.length,
|
|
144
307
|
};
|
|
145
308
|
}
|
|
309
|
+
}
|
|
310
|
+
```
|
|
146
311
|
|
|
147
|
-
|
|
312
|
+
### Multiple named fields
|
|
313
|
+
|
|
314
|
+
```ts
|
|
315
|
+
import { Body, Controller, Post, UseInterceptors } from '@nestjs/common';
|
|
316
|
+
import { FileStorageInterceptor } from '@ackplus/nest-file-storage';
|
|
317
|
+
|
|
318
|
+
@Controller('files')
|
|
319
|
+
export class FilesController {
|
|
148
320
|
@Post('fields')
|
|
149
321
|
@UseInterceptors(
|
|
150
322
|
FileStorageInterceptor({
|
|
151
323
|
type: 'fields',
|
|
152
324
|
fields: [
|
|
153
325
|
{ name: 'avatar', maxCount: 1 },
|
|
154
|
-
{ name: '
|
|
326
|
+
{ name: 'attachments', maxCount: 5 },
|
|
155
327
|
],
|
|
156
|
-
})
|
|
328
|
+
}),
|
|
157
329
|
)
|
|
158
|
-
uploadFields(@Body() body:
|
|
159
|
-
return
|
|
160
|
-
message: 'Files uploaded successfully',
|
|
161
|
-
avatar: body.avatar,
|
|
162
|
-
photos: body.photos,
|
|
163
|
-
};
|
|
330
|
+
uploadFields(@Body() body: { avatar: string[]; attachments: string[] }) {
|
|
331
|
+
return body;
|
|
164
332
|
}
|
|
165
333
|
}
|
|
166
334
|
```
|
|
167
335
|
|
|
168
|
-
|
|
336
|
+
## Validation
|
|
169
337
|
|
|
170
|
-
|
|
171
|
-
// file.service.ts
|
|
172
|
-
import { Injectable } from '@nestjs/common';
|
|
173
|
-
import { FileStorageService } from '@ackplus/nest-file-storage';
|
|
338
|
+
Use the interceptor options for validation, not the module registration.
|
|
174
339
|
|
|
175
|
-
|
|
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
|
-
}
|
|
340
|
+
### File size and mime type validation
|
|
182
341
|
|
|
183
|
-
|
|
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
|
-
}
|
|
342
|
+
Use `multerOptions()` when you want Multer to reject invalid uploads before your route handler runs.
|
|
194
343
|
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
344
|
+
```ts
|
|
345
|
+
import {
|
|
346
|
+
BadRequestException,
|
|
347
|
+
Body,
|
|
348
|
+
Controller,
|
|
349
|
+
Post,
|
|
350
|
+
UseInterceptors,
|
|
351
|
+
} from '@nestjs/common';
|
|
352
|
+
import { FileStorageInterceptor } from '@ackplus/nest-file-storage';
|
|
200
353
|
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
354
|
+
@Controller('images')
|
|
355
|
+
export class ImagesController {
|
|
356
|
+
@Post()
|
|
357
|
+
@UseInterceptors(
|
|
358
|
+
FileStorageInterceptor('image', {
|
|
359
|
+
multerOptions: () => ({
|
|
360
|
+
limits: {
|
|
361
|
+
fileSize: 5 * 1024 * 1024,
|
|
362
|
+
},
|
|
363
|
+
fileFilter: (_req, file, cb) => {
|
|
364
|
+
const allowed = ['image/jpeg', 'image/png', 'image/webp'];
|
|
365
|
+
if (!allowed.includes(file.mimetype)) {
|
|
366
|
+
return cb(new BadRequestException('Only JPEG, PNG, and WebP files are allowed'), false);
|
|
367
|
+
}
|
|
368
|
+
cb(null, true);
|
|
369
|
+
},
|
|
370
|
+
}),
|
|
371
|
+
fileDist: () => 'images',
|
|
372
|
+
}),
|
|
373
|
+
)
|
|
374
|
+
upload(@Body() body: { image: string }) {
|
|
375
|
+
return body;
|
|
208
376
|
}
|
|
209
377
|
}
|
|
210
378
|
```
|
|
211
379
|
|
|
212
|
-
|
|
380
|
+
### Cross-field or post-upload validation
|
|
213
381
|
|
|
214
|
-
|
|
382
|
+
Use `afterUpload()` when validation depends on the final uploaded file list or multiple fields together.
|
|
215
383
|
|
|
216
|
-
```
|
|
217
|
-
|
|
218
|
-
|
|
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
|
-
```
|
|
384
|
+
```ts
|
|
385
|
+
import { BadRequestException, Body, Controller, Post, UseInterceptors } from '@nestjs/common';
|
|
386
|
+
import { FileStorageInterceptor } from '@ackplus/nest-file-storage';
|
|
226
387
|
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
388
|
+
@Controller('gallery')
|
|
389
|
+
export class GalleryController {
|
|
390
|
+
@Post()
|
|
391
|
+
@UseInterceptors(
|
|
392
|
+
FileStorageInterceptor(
|
|
393
|
+
{
|
|
394
|
+
type: 'fields',
|
|
395
|
+
fields: [
|
|
396
|
+
{ name: 'cover', maxCount: 1 },
|
|
397
|
+
{ name: 'images', maxCount: 10 },
|
|
398
|
+
],
|
|
399
|
+
},
|
|
400
|
+
{
|
|
401
|
+
afterUpload: (req) => {
|
|
402
|
+
const files = req.files as Record<string, Express.Multer.File[]>;
|
|
403
|
+
const imageCount = files?.images?.length ?? 0;
|
|
404
|
+
if (imageCount < 2) {
|
|
405
|
+
throw new BadRequestException('At least 2 gallery images are required.');
|
|
406
|
+
}
|
|
407
|
+
},
|
|
408
|
+
},
|
|
409
|
+
),
|
|
410
|
+
)
|
|
411
|
+
create(@Body() body: { cover: string[]; images: string[] }) {
|
|
412
|
+
return body;
|
|
413
|
+
}
|
|
241
414
|
}
|
|
242
415
|
```
|
|
243
416
|
|
|
244
|
-
|
|
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
|
-
```
|
|
417
|
+
Practical guidance:
|
|
257
418
|
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
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
|
-
```
|
|
419
|
+
- Use `limits.fileSize` for upload size limits.
|
|
420
|
+
- Use `fileFilter` for mime-type or extension checks.
|
|
421
|
+
- Use `afterUpload` for rules involving multiple files or fields.
|
|
422
|
+
- Do not rely on `file.size` inside `fileName()`; Multer size limits are the reliable way to cap upload size.
|
|
277
423
|
|
|
278
|
-
|
|
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
|
-
```
|
|
424
|
+
## Control The Stored Path And Key
|
|
294
425
|
|
|
295
|
-
|
|
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
|
-
```
|
|
426
|
+
Two callbacks define where the file is stored and what key is saved:
|
|
317
427
|
|
|
318
|
-
|
|
428
|
+
- `fileDist(file, req)`: the directory or path prefix.
|
|
429
|
+
- `fileName(file, req)`: the final filename segment.
|
|
319
430
|
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
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
|
-
}
|
|
431
|
+
The final key is effectively:
|
|
432
|
+
|
|
433
|
+
```text
|
|
434
|
+
<fileDist>/<fileName>
|
|
337
435
|
```
|
|
338
436
|
|
|
339
|
-
###
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
}
|
|
437
|
+
### Default key generation
|
|
438
|
+
|
|
439
|
+
If you do not override anything:
|
|
440
|
+
|
|
441
|
+
- Local storage stores files under `rootPath/YYYY/MM/DD`.
|
|
442
|
+
- S3 and Azure store files under `uploads/YYYY/MM/DD`.
|
|
443
|
+
- The default filename is `uuid-originalname`.
|
|
444
|
+
|
|
445
|
+
Examples:
|
|
446
|
+
|
|
447
|
+
- Local key: `2026/04/23/2d0b8f6e-report.pdf`
|
|
448
|
+
- S3 key: `uploads/2026/04/23/2d0b8f6e-report.pdf`
|
|
449
|
+
|
|
450
|
+
### Custom path and filename
|
|
451
|
+
|
|
452
|
+
```ts
|
|
453
|
+
import { extname } from 'path';
|
|
454
|
+
|
|
455
|
+
FileStorageInterceptor('avatar', {
|
|
456
|
+
fileDist: (_file, req) => `users/${req.user.id}/avatars`,
|
|
457
|
+
fileName: (file) => `${Date.now()}${extname(file.originalname)}`,
|
|
458
|
+
});
|
|
356
459
|
```
|
|
357
460
|
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
```
|
|
361
|
-
|
|
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
|
-
}
|
|
461
|
+
This gives you keys such as:
|
|
462
|
+
|
|
463
|
+
```text
|
|
464
|
+
users/42/avatars/1713876155123.png
|
|
371
465
|
```
|
|
372
466
|
|
|
373
|
-
|
|
467
|
+
Use `fileDist` for folder structure and `fileName` for the last path segment. That is the most reliable way to control saved keys in the current implementation.
|
|
374
468
|
|
|
375
|
-
|
|
469
|
+
## Map Upload Results Into `request.body`
|
|
376
470
|
|
|
377
|
-
|
|
378
|
-
|
|
471
|
+
The default mapping stores only keys. If you want richer metadata in the controller body, use `mapToRequestBody`.
|
|
472
|
+
|
|
473
|
+
### Return metadata instead of just the key
|
|
474
|
+
|
|
475
|
+
```ts
|
|
476
|
+
import { Body, Controller, Post, UseInterceptors } from '@nestjs/common';
|
|
379
477
|
import { FileStorageInterceptor } from '@ackplus/nest-file-storage';
|
|
380
478
|
|
|
381
|
-
@Controller('
|
|
382
|
-
export class
|
|
383
|
-
@Post(
|
|
479
|
+
@Controller('documents')
|
|
480
|
+
export class DocumentsController {
|
|
481
|
+
@Post()
|
|
384
482
|
@UseInterceptors(
|
|
385
|
-
FileStorageInterceptor('
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
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
|
-
})
|
|
483
|
+
FileStorageInterceptor('document', {
|
|
484
|
+
mapToRequestBody: (file) => ({
|
|
485
|
+
key: file.key,
|
|
486
|
+
url: file.url,
|
|
487
|
+
size: file.size,
|
|
488
|
+
mimetype: file.mimetype,
|
|
489
|
+
originalName: file.originalName,
|
|
490
|
+
fullPath: file.fullPath,
|
|
491
|
+
}),
|
|
492
|
+
}),
|
|
404
493
|
)
|
|
405
|
-
|
|
406
|
-
return
|
|
407
|
-
message: 'Image uploaded successfully',
|
|
408
|
-
imageKey: body.image,
|
|
409
|
-
};
|
|
494
|
+
upload(@Body() body: any) {
|
|
495
|
+
return body.document;
|
|
410
496
|
}
|
|
411
497
|
}
|
|
412
498
|
```
|
|
413
499
|
|
|
414
|
-
###
|
|
500
|
+
### Preserve an existing body field
|
|
415
501
|
|
|
416
|
-
|
|
417
|
-
import { Controller, Post, UseInterceptors, Body } from '@nestjs/common';
|
|
418
|
-
import { FileStorageInterceptor, FileStorageService } from '@ackplus/nest-file-storage';
|
|
502
|
+
If the request already contains a JSON/text field with the same name and you only want to populate it when missing:
|
|
419
503
|
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
504
|
+
```ts
|
|
505
|
+
FileStorageInterceptor('file', {
|
|
506
|
+
overwriteBodyField: false,
|
|
507
|
+
});
|
|
508
|
+
```
|
|
423
509
|
|
|
424
|
-
|
|
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
|
-
}
|
|
510
|
+
## Use `FileStorageService` Programmatically
|
|
442
511
|
|
|
443
|
-
|
|
444
|
-
await this.userService.updateAvatar(req.user.id, body.avatar);
|
|
512
|
+
`FileStorageService.getStorage()` gives you the active storage implementation so you can upload or manage files outside controller interceptors.
|
|
445
513
|
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
};
|
|
450
|
-
}
|
|
451
|
-
}
|
|
452
|
-
```
|
|
514
|
+
```ts
|
|
515
|
+
import { Injectable } from '@nestjs/common';
|
|
516
|
+
import { FileStorageService } from '@ackplus/nest-file-storage';
|
|
453
517
|
|
|
454
|
-
|
|
518
|
+
@Injectable()
|
|
519
|
+
export class DocumentStorageService {
|
|
520
|
+
async upload(buffer: Buffer, key: string) {
|
|
521
|
+
const storage = await FileStorageService.getStorage();
|
|
522
|
+
return storage.putFile(buffer, key);
|
|
523
|
+
}
|
|
455
524
|
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
525
|
+
async get(key: string) {
|
|
526
|
+
const storage = await FileStorageService.getStorage();
|
|
527
|
+
return storage.getFile(key);
|
|
528
|
+
}
|
|
459
529
|
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
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
|
-
};
|
|
530
|
+
async remove(key: string) {
|
|
531
|
+
const storage = await FileStorageService.getStorage();
|
|
532
|
+
await storage.deleteFile(key);
|
|
481
533
|
}
|
|
482
534
|
|
|
483
|
-
|
|
484
|
-
async downloadDocument(@Param('key') key: string, @Res() res) {
|
|
535
|
+
async copy(oldKey: string, newKey: string) {
|
|
485
536
|
const storage = await FileStorageService.getStorage();
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
res.setHeader('Content-Type', 'application/octet-stream');
|
|
489
|
-
res.setHeader('Content-Disposition', `attachment; filename="${key}"`);
|
|
490
|
-
res.send(file);
|
|
537
|
+
return storage.copyFile(oldKey, newKey);
|
|
491
538
|
}
|
|
492
539
|
|
|
493
|
-
|
|
494
|
-
async deleteDocument(@Param('key') key: string) {
|
|
540
|
+
async url(key: string) {
|
|
495
541
|
const storage = await FileStorageService.getStorage();
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
return { message: 'Document deleted successfully' };
|
|
542
|
+
return storage.getUrl(key);
|
|
499
543
|
}
|
|
500
544
|
|
|
501
|
-
|
|
502
|
-
async getDocumentUrl(@Param('key') key: string) {
|
|
545
|
+
async signedUrl(key: string) {
|
|
503
546
|
const storage = await FileStorageService.getStorage();
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
|
|
547
|
+
if ('getSignedUrl' in storage && storage.getSignedUrl) {
|
|
548
|
+
return storage.getSignedUrl(key, { expiresIn: 3600 });
|
|
549
|
+
}
|
|
550
|
+
return storage.getUrl(key);
|
|
507
551
|
}
|
|
508
552
|
}
|
|
509
553
|
```
|
|
510
554
|
|
|
511
|
-
|
|
555
|
+
### Get full URL from a stored key/path
|
|
512
556
|
|
|
513
|
-
|
|
557
|
+
If you save only the storage key in your database, generate the public URL later with `getUrl(key)`. This works across Local, S3, and Azure.
|
|
514
558
|
|
|
515
|
-
```
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
|
|
559
|
+
```ts
|
|
560
|
+
import { Injectable } from '@nestjs/common';
|
|
561
|
+
import { FileStorageService } from '@ackplus/nest-file-storage';
|
|
562
|
+
|
|
563
|
+
@Injectable()
|
|
564
|
+
export class FileUrlService {
|
|
565
|
+
async getUrlFromKey(key?: string | null): Promise<string | null> {
|
|
566
|
+
if (!key) {
|
|
567
|
+
return null;
|
|
568
|
+
}
|
|
569
|
+
|
|
570
|
+
const storage = await FileStorageService.getStorage();
|
|
571
|
+
return await Promise.resolve(storage.getUrl(key));
|
|
572
|
+
}
|
|
525
573
|
}
|
|
526
574
|
```
|
|
527
575
|
|
|
528
|
-
###
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
|
|
541
|
-
|
|
542
|
-
|
|
543
|
-
|
|
544
|
-
|
|
545
|
-
|
|
546
|
-
|
|
547
|
-
|
|
548
|
-
|
|
549
|
-
|
|
550
|
-
|
|
551
|
-
|
|
576
|
+
### Add full URLs after loading entities
|
|
577
|
+
|
|
578
|
+
The usual pattern is to store only `fileKey` in the entity and attach the final URL after fetching data.
|
|
579
|
+
|
|
580
|
+
```ts
|
|
581
|
+
import { Injectable } from '@nestjs/common';
|
|
582
|
+
import { FileStorageService } from '@ackplus/nest-file-storage';
|
|
583
|
+
|
|
584
|
+
type UserRecord = {
|
|
585
|
+
id: number;
|
|
586
|
+
avatarKey?: string | null;
|
|
587
|
+
};
|
|
588
|
+
|
|
589
|
+
@Injectable()
|
|
590
|
+
export class UsersResponseMapper {
|
|
591
|
+
async mapUser(user: UserRecord) {
|
|
592
|
+
const storage = await FileStorageService.getStorage();
|
|
593
|
+
|
|
594
|
+
return {
|
|
595
|
+
...user,
|
|
596
|
+
avatarUrl: user.avatarKey
|
|
597
|
+
? await Promise.resolve(storage.getUrl(user.avatarKey))
|
|
598
|
+
: null,
|
|
599
|
+
};
|
|
600
|
+
}
|
|
552
601
|
}
|
|
553
602
|
```
|
|
554
603
|
|
|
555
|
-
|
|
556
|
-
|
|
557
|
-
|
|
558
|
-
|
|
559
|
-
|
|
560
|
-
|
|
561
|
-
|
|
562
|
-
|
|
563
|
-
|
|
564
|
-
|
|
565
|
-
|
|
566
|
-
|
|
567
|
-
|
|
568
|
-
|
|
569
|
-
|
|
570
|
-
|
|
571
|
-
|
|
572
|
-
|
|
573
|
-
|
|
574
|
-
```
|
|
604
|
+
For entity-based applications:
|
|
605
|
+
|
|
606
|
+
- Store the key/path in the entity, for example `avatarKey` or `documentKey`.
|
|
607
|
+
- Build the full URL in a service, mapper, serializer, or response DTO step after loading the entity.
|
|
608
|
+
- This is usually cleaner than doing URL generation inside ORM entity hooks, because `FileStorageService.getStorage()` is async.
|
|
609
|
+
|
|
610
|
+
### Provider capability summary
|
|
611
|
+
|
|
612
|
+
| Method | Local | S3 | Azure |
|
|
613
|
+
| --- | --- | --- | --- |
|
|
614
|
+
| `putFile()` | yes | yes | yes |
|
|
615
|
+
| `getFile()` | yes | yes | yes |
|
|
616
|
+
| `deleteFile()` | yes | yes | yes |
|
|
617
|
+
| `copyFile()` | yes | yes | yes |
|
|
618
|
+
| `getUrl()` | yes | yes | yes |
|
|
619
|
+
| `getSignedUrl()` | no | yes | yes |
|
|
620
|
+
| `path()` | yes | no | no |
|
|
621
|
+
|
|
622
|
+
## Route-Level Storage Override
|
|
575
623
|
|
|
576
|
-
|
|
577
|
-
|
|
578
|
-
```
|
|
579
|
-
|
|
580
|
-
|
|
581
|
-
|
|
582
|
-
|
|
583
|
-
|
|
584
|
-
|
|
585
|
-
|
|
586
|
-
|
|
587
|
-
|
|
588
|
-
|
|
589
|
-
|
|
624
|
+
You can override the storage provider per route.
|
|
625
|
+
|
|
626
|
+
```ts
|
|
627
|
+
import { Body, Controller, Post, UseInterceptors } from '@nestjs/common';
|
|
628
|
+
import { FileStorageEnum, FileStorageInterceptor } from '@ackplus/nest-file-storage';
|
|
629
|
+
|
|
630
|
+
@Controller('avatars')
|
|
631
|
+
export class AvatarController {
|
|
632
|
+
@Post()
|
|
633
|
+
@UseInterceptors(
|
|
634
|
+
FileStorageInterceptor('avatar', {
|
|
635
|
+
storageType: FileStorageEnum.S3,
|
|
636
|
+
storageOptions: {
|
|
637
|
+
accessKeyId: process.env.AWS_ACCESS_KEY_ID!,
|
|
638
|
+
secretAccessKey: process.env.AWS_SECRET_ACCESS_KEY!,
|
|
639
|
+
region: process.env.AWS_REGION!,
|
|
640
|
+
bucket: process.env.AWS_BUCKET!,
|
|
641
|
+
fileDist: (_file, req) => `users/${req.user.id}/avatars`,
|
|
642
|
+
},
|
|
643
|
+
mapToRequestBody: (file) => ({
|
|
644
|
+
key: file.key,
|
|
645
|
+
url: file.url,
|
|
646
|
+
}),
|
|
647
|
+
}),
|
|
648
|
+
)
|
|
649
|
+
upload(@Body() body: any) {
|
|
650
|
+
return body.avatar;
|
|
651
|
+
}
|
|
590
652
|
}
|
|
591
653
|
```
|
|
592
654
|
|
|
593
|
-
|
|
594
|
-
|
|
595
|
-
|
|
596
|
-
|
|
597
|
-
|
|
598
|
-
|
|
599
|
-
|
|
600
|
-
|
|
601
|
-
|
|
602
|
-
|
|
603
|
-
|
|
604
|
-
|
|
605
|
-
|
|
606
|
-
|
|
607
|
-
|
|
608
|
-
|
|
609
|
-
|
|
610
|
-
|
|
611
|
-
|
|
612
|
-
|
|
613
|
-
|
|
614
|
-
|
|
615
|
-
|
|
616
|
-
|
|
617
|
-
|
|
618
|
-
|
|
619
|
-
|
|
620
|
-
|
|
621
|
-
|
|
622
|
-
|
|
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
|
-
```
|
|
655
|
+
Important behavior:
|
|
656
|
+
|
|
657
|
+
- If the route uses the same provider as the module default, `storageOptions` can override pieces of that provider config.
|
|
658
|
+
- If the route uses a different provider than the module default, pass the full config for that provider inside `storageOptions`.
|
|
659
|
+
|
|
660
|
+
## Configuration Reference
|
|
661
|
+
|
|
662
|
+
### Common file options
|
|
663
|
+
|
|
664
|
+
These are shared by Local, S3, and Azure configs:
|
|
665
|
+
|
|
666
|
+
- `fileName(file, req)`: return the filename segment.
|
|
667
|
+
- `fileDist(file, req)`: return the folder/path prefix.
|
|
668
|
+
- `transformUploadedFileObject(file)`: transform the raw uploaded file object returned by the storage engine before Multer stores it.
|
|
669
|
+
|
|
670
|
+
### Local config
|
|
671
|
+
|
|
672
|
+
- `rootPath`: local directory where files are written.
|
|
673
|
+
- `baseUrl`: URL prefix used by `getUrl()`.
|
|
674
|
+
- Common file options listed above.
|
|
675
|
+
|
|
676
|
+
### S3 config
|
|
677
|
+
|
|
678
|
+
- `accessKeyId`
|
|
679
|
+
- `secretAccessKey`
|
|
680
|
+
- `region`
|
|
681
|
+
- `bucket`
|
|
682
|
+
- `endpoint`: optional custom S3-compatible endpoint.
|
|
683
|
+
- `cloudFrontUrl`: optional CDN/public URL prefix used by `getUrl()`.
|
|
684
|
+
- Common file options listed above.
|
|
639
685
|
|
|
640
|
-
|
|
686
|
+
### Azure config
|
|
641
687
|
|
|
642
|
-
|
|
688
|
+
- `account`
|
|
689
|
+
- `accountKey`
|
|
690
|
+
- `container`
|
|
691
|
+
- Common file options listed above.
|
|
643
692
|
|
|
644
|
-
|
|
693
|
+
Azure note:
|
|
645
694
|
|
|
646
|
-
|
|
695
|
+
- `getSignedUrl()` generates a SAS URL by default.
|
|
696
|
+
- If `AZURE_CDN_DOMAIN_NAME` is set in the runtime environment, the Azure storage adapter returns `AZURE_CDN_DOMAIN_NAME/<key>` instead of a SAS URL.
|
|
647
697
|
|
|
648
|
-
##
|
|
698
|
+
## Current Behavior Notes
|
|
649
699
|
|
|
650
|
-
-
|
|
651
|
-
-
|
|
652
|
-
-
|
|
653
|
-
-
|
|
700
|
+
- This package is implemented for NestJS with Express and Multer. It is not a Fastify-targeted integration.
|
|
701
|
+
- In `fields` mode, each field is mapped to an array even when `maxCount` is `1`.
|
|
702
|
+
- `baseUrl` is only a URL prefix. Add static serving yourself for direct local-file access.
|
|
703
|
+
- The exported types include a `prefix` option, but `fileDist` and `fileName` are the reliable hooks for controlling saved paths in the current implementation.
|
|
704
|
+
- The module stores one provider configuration at a time. For per-route provider switching, use `storageType` with `storageOptions`.
|
|
705
|
+
- The custom `storageFactory` module option is best treated as an advanced service-level hook. The controller upload flow documented here is the built-in path for Local, S3, and Azure storage.
|
|
654
706
|
|
|
655
|
-
##
|
|
707
|
+
## Examples And Workspace Docs
|
|
656
708
|
|
|
657
|
-
|
|
658
|
-
-
|
|
659
|
-
-
|
|
709
|
+
- Package examples: [`examples/`](./examples/)
|
|
710
|
+
- Workspace overview: [`../../README.md`](../../README.md)
|
|
711
|
+
- Example app: [`../../apps/example-app/README.md`](../../apps/example-app/README.md)
|
|
660
712
|
|
|
661
|
-
|
|
713
|
+
## License
|
|
662
714
|
|
|
663
|
-
|
|
715
|
+
MIT
|