@ackplus/nest-file-storage 1.1.23 → 2.0.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +80 -0
- package/MIGRATION.md +220 -0
- package/README.md +49 -664
- package/dist/index.d.ts +1 -4
- package/dist/index.js +1 -4
- package/dist/index.js.map +1 -1
- package/dist/lib/constants.d.ts +3 -1
- package/dist/lib/constants.js +4 -2
- package/dist/lib/constants.js.map +1 -1
- package/dist/lib/driver-registry.d.ts +34 -0
- package/dist/lib/driver-registry.js +118 -0
- package/dist/lib/driver-registry.js.map +1 -0
- package/dist/lib/drivers/azure.driver.d.ts +21 -0
- package/dist/lib/drivers/azure.driver.js +91 -0
- package/dist/lib/drivers/azure.driver.js.map +1 -0
- package/dist/lib/drivers/driver.interface.d.ts +40 -0
- package/dist/lib/drivers/driver.interface.js +3 -0
- package/dist/lib/drivers/driver.interface.js.map +1 -0
- package/dist/lib/drivers/driver.util.d.ts +2 -0
- package/dist/lib/drivers/driver.util.js +15 -0
- package/dist/lib/drivers/driver.util.js.map +1 -0
- package/dist/lib/drivers/index.d.ts +7 -0
- package/dist/lib/drivers/index.js +39 -0
- package/dist/lib/drivers/index.js.map +1 -0
- package/dist/lib/drivers/local.driver.d.ts +15 -0
- package/dist/lib/drivers/local.driver.js +110 -0
- package/dist/lib/drivers/local.driver.js.map +1 -0
- package/dist/lib/drivers/s3.driver.d.ts +22 -0
- package/dist/lib/drivers/s3.driver.js +103 -0
- package/dist/lib/drivers/s3.driver.js.map +1 -0
- package/dist/lib/file-storage.service.d.ts +16 -5
- package/dist/lib/file-storage.service.js +60 -22
- package/dist/lib/file-storage.service.js.map +1 -1
- package/dist/lib/index.d.ts +9 -2
- package/dist/lib/index.js +15 -2
- package/dist/lib/index.js.map +1 -1
- package/dist/lib/interceptor/file-storage.interceptor.d.ts +7 -10
- package/dist/lib/interceptor/file-storage.interceptor.js +117 -112
- package/dist/lib/interceptor/file-storage.interceptor.js.map +1 -1
- package/dist/lib/multer/driver-multer-engine.d.ts +18 -0
- package/dist/lib/multer/driver-multer-engine.js +91 -0
- package/dist/lib/multer/driver-multer-engine.js.map +1 -0
- package/dist/lib/nest-file-storage.module.d.ts +3 -3
- package/dist/lib/nest-file-storage.module.js +81 -44
- package/dist/lib/nest-file-storage.module.js.map +1 -1
- package/dist/lib/registry-holder.d.ts +6 -0
- package/dist/lib/registry-holder.js +26 -0
- package/dist/lib/registry-holder.js.map +1 -0
- package/dist/lib/tenant/tenant-from.d.ts +14 -0
- package/dist/lib/tenant/tenant-from.js +71 -0
- package/dist/lib/tenant/tenant-from.js.map +1 -0
- package/dist/lib/tenant/tenant.types.d.ts +20 -0
- package/dist/lib/tenant/tenant.types.js +3 -0
- package/dist/lib/tenant/tenant.types.js.map +1 -0
- package/dist/lib/types.d.ts +45 -35
- package/dist/lib/types.js.map +1 -1
- package/dist/lib/validation.d.ts +22 -0
- package/dist/lib/validation.js +98 -0
- package/dist/lib/validation.js.map +1 -0
- package/dist/tsconfig.build.tsbuildinfo +1 -1
- package/examples/1-basic-local-storage.example.ts +11 -7
- package/examples/10-testing.example.ts +60 -196
- package/examples/11-custom-driver.example.ts +82 -0
- package/examples/12-multi-tenant.example.ts +93 -0
- package/examples/2-s3-storage.example.ts +18 -16
- package/examples/3-azure-storage.example.ts +14 -12
- package/examples/4-upload-controller.example.ts +20 -55
- package/examples/5-custom-configuration.example.ts +37 -57
- package/examples/6-file-service.example.ts +34 -91
- package/examples/7-user-avatar.example.ts +37 -92
- package/examples/8-document-management.example.ts +45 -196
- package/examples/9-dynamic-storage.example.ts +29 -147
- package/examples/README.md +25 -107
- package/package.json +17 -4
- package/dist/lib/storage/azure.storage.d.ts +0 -18
- package/dist/lib/storage/azure.storage.js +0 -210
- package/dist/lib/storage/azure.storage.js.map +0 -1
- package/dist/lib/storage/local.storage.d.ts +0 -20
- package/dist/lib/storage/local.storage.js +0 -212
- package/dist/lib/storage/local.storage.js.map +0 -1
- package/dist/lib/storage/s3.storage.d.ts +0 -19
- package/dist/lib/storage/s3.storage.js +0 -241
- package/dist/lib/storage/s3.storage.js.map +0 -1
- package/dist/lib/storage.factory.d.ts +0 -8
- package/dist/lib/storage.factory.js +0 -46
- package/dist/lib/storage.factory.js.map +0 -1
package/README.md
CHANGED
|
@@ -1,272 +1,62 @@
|
|
|
1
1
|
# @ackplus/nest-file-storage
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
> One file-storage API for NestJS — Local, S3, Azure, or your own driver. Upload through a controller interceptor or call it directly. Pick storage **per request** (great for multi-tenant apps), validate declaratively, and get one consistent file shape everywhere.
|
|
4
4
|
|
|
5
|
-
|
|
5
|
+
[](https://www.npmjs.com/package/@ackplus/nest-file-storage)
|
|
6
|
+
[](https://ack-solutions.github.io/nest-file-storage/)
|
|
7
|
+
[](https://www.npmjs.com/package/@ackplus/nest-file-storage)
|
|
8
|
+
[](./LICENSE)
|
|
6
9
|
|
|
7
|
-
##
|
|
10
|
+
## 📖 Documentation → **<https://ack-solutions.github.io/nest-file-storage/>**
|
|
8
11
|
|
|
9
|
-
-
|
|
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.
|
|
12
|
+
**This README is just a quick start.** The full guides, the custom-driver and multi-tenant cookbooks, the complete API reference, and the v1 → v2 migration guide all live on the **[documentation site](https://ack-solutions.github.io/nest-file-storage/)**.
|
|
14
13
|
|
|
15
|
-
|
|
14
|
+
---
|
|
16
15
|
|
|
17
|
-
|
|
16
|
+
## Why this library?
|
|
18
17
|
|
|
19
|
-
|
|
18
|
+
Wiring Multer to S3/Azure/local by hand means juggling storage engines, SDK clients, key generation, URL building, and validation in every project — and swapping providers later means rewriting it all. This library gives you **one stable API** over all of them.
|
|
20
19
|
|
|
21
|
-
-
|
|
22
|
-
-
|
|
23
|
-
-
|
|
24
|
-
- `
|
|
25
|
-
- `
|
|
20
|
+
- **Provider-agnostic** — write upload code once; switch Local → S3 → Azure (or your own backend) by changing config, not controllers.
|
|
21
|
+
- **Custom storage is first-class** — implement a small [`StorageDriver`](https://ack-solutions.github.io/nest-file-storage/custom-drivers) and it works everywhere (interceptor + service).
|
|
22
|
+
- **Per-request / multi-tenant** — route each upload to the right bucket/folder, with per-tenant driver caching. See [multi-tenant](https://ack-solutions.github.io/nest-file-storage/multi-tenant).
|
|
23
|
+
- **Declarative validation** — size, MIME, extension, and count limits as data → typed `400`s.
|
|
24
|
+
- **One result shape** — every upload returns the same `UploadedFile` (`key`, `url`, `size`, …).
|
|
25
|
+
- **Lean dependencies** — the AWS and Azure SDKs are optional peers, loaded lazily only if you use them.
|
|
26
26
|
|
|
27
|
-
|
|
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
|
|
40
|
-
|
|
41
|
-
```bash
|
|
42
|
-
pnpm add @ackplus/nest-file-storage multer reflect-metadata
|
|
43
|
-
```
|
|
44
|
-
|
|
45
|
-
If your Nest app does not already include the Express platform package:
|
|
46
|
-
|
|
47
|
-
```bash
|
|
48
|
-
pnpm add @nestjs/platform-express
|
|
49
|
-
```
|
|
50
|
-
|
|
51
|
-
For AWS S3 support:
|
|
52
|
-
|
|
53
|
-
```bash
|
|
54
|
-
pnpm add @aws-sdk/client-s3 @aws-sdk/s3-request-presigner
|
|
55
|
-
```
|
|
56
|
-
|
|
57
|
-
For Azure Blob Storage support:
|
|
27
|
+
## Install
|
|
58
28
|
|
|
59
29
|
```bash
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
### Local storage
|
|
66
|
-
|
|
67
|
-
```ts
|
|
68
|
-
import { Module } from '@nestjs/common';
|
|
69
|
-
import { FileStorageEnum, NestFileStorageModule } from '@ackplus/nest-file-storage';
|
|
70
|
-
|
|
71
|
-
@Module({
|
|
72
|
-
imports: [
|
|
73
|
-
NestFileStorageModule.forRoot({
|
|
74
|
-
storage: FileStorageEnum.LOCAL,
|
|
75
|
-
localConfig: {
|
|
76
|
-
rootPath: './uploads',
|
|
77
|
-
baseUrl: 'http://localhost:3000/uploads',
|
|
78
|
-
},
|
|
79
|
-
}),
|
|
80
|
-
],
|
|
81
|
-
})
|
|
82
|
-
export class AppModule {}
|
|
30
|
+
npm i @ackplus/nest-file-storage multer reflect-metadata
|
|
31
|
+
# optional, only if you use them:
|
|
32
|
+
npm i @aws-sdk/client-s3 @aws-sdk/s3-request-presigner # AWS S3
|
|
33
|
+
npm i @azure/storage-blob # Azure Blob
|
|
83
34
|
```
|
|
84
35
|
|
|
85
|
-
|
|
36
|
+
Requires NestJS on **Express** (`@nestjs/platform-express`). Peer ranges: `@nestjs/common`/`@nestjs/core` `^10 || ^11`, `multer` `^1.4.5-lts.1 || ^2`, `reflect-metadata` `^0.2.2`.
|
|
86
37
|
|
|
87
|
-
|
|
88
|
-
import { Module } from '@nestjs/common';
|
|
89
|
-
import { FileStorageEnum, NestFileStorageModule } from '@ackplus/nest-file-storage';
|
|
90
|
-
|
|
91
|
-
@Module({
|
|
92
|
-
imports: [
|
|
93
|
-
NestFileStorageModule.forRoot({
|
|
94
|
-
storage: FileStorageEnum.S3,
|
|
95
|
-
s3Config: {
|
|
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,
|
|
101
|
-
},
|
|
102
|
-
}),
|
|
103
|
-
],
|
|
104
|
-
})
|
|
105
|
-
export class AppModule {}
|
|
106
|
-
```
|
|
38
|
+
## Quick start
|
|
107
39
|
|
|
108
|
-
|
|
40
|
+
**1. Register a driver:**
|
|
109
41
|
|
|
110
42
|
```ts
|
|
111
43
|
import { Module } from '@nestjs/common';
|
|
112
|
-
import {
|
|
44
|
+
import { NestFileStorageModule, localDriver } from '@ackplus/nest-file-storage';
|
|
113
45
|
|
|
114
46
|
@Module({
|
|
115
47
|
imports: [
|
|
116
48
|
NestFileStorageModule.forRoot({
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
accountKey: process.env.AZURE_STORAGE_KEY!,
|
|
121
|
-
container: process.env.AZURE_CONTAINER!,
|
|
122
|
-
},
|
|
123
|
-
}),
|
|
124
|
-
],
|
|
125
|
-
})
|
|
126
|
-
export class AppModule {}
|
|
127
|
-
```
|
|
128
|
-
|
|
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,
|
|
49
|
+
default: 'local',
|
|
50
|
+
drivers: {
|
|
51
|
+
local: localDriver({ rootPath: './uploads', baseUrl: 'http://localhost:3000/uploads' }),
|
|
184
52
|
},
|
|
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
53
|
}),
|
|
236
54
|
],
|
|
237
55
|
})
|
|
238
56
|
export class AppModule {}
|
|
239
57
|
```
|
|
240
58
|
|
|
241
|
-
|
|
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
|
|
59
|
+
**2. Accept uploads in a controller:**
|
|
270
60
|
|
|
271
61
|
```ts
|
|
272
62
|
import { Body, Controller, Post, UseInterceptors } from '@nestjs/common';
|
|
@@ -274,441 +64,36 @@ import { FileStorageInterceptor } from '@ackplus/nest-file-storage';
|
|
|
274
64
|
|
|
275
65
|
@Controller('files')
|
|
276
66
|
export class FilesController {
|
|
277
|
-
@Post('
|
|
67
|
+
@Post('upload')
|
|
278
68
|
@UseInterceptors(FileStorageInterceptor('file'))
|
|
279
|
-
|
|
280
|
-
return {
|
|
281
|
-
key: body.file,
|
|
282
|
-
};
|
|
283
|
-
}
|
|
284
|
-
}
|
|
285
|
-
```
|
|
286
|
-
|
|
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 {
|
|
295
|
-
@Post('multiple')
|
|
296
|
-
@UseInterceptors(
|
|
297
|
-
FileStorageInterceptor({
|
|
298
|
-
type: 'array',
|
|
299
|
-
fieldName: 'files',
|
|
300
|
-
maxCount: 10,
|
|
301
|
-
}),
|
|
302
|
-
)
|
|
303
|
-
uploadMultiple(@Body() body: { files: string[] }) {
|
|
304
|
-
return {
|
|
305
|
-
keys: body.files,
|
|
306
|
-
count: body.files.length,
|
|
307
|
-
};
|
|
308
|
-
}
|
|
309
|
-
}
|
|
310
|
-
```
|
|
311
|
-
|
|
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 {
|
|
320
|
-
@Post('fields')
|
|
321
|
-
@UseInterceptors(
|
|
322
|
-
FileStorageInterceptor({
|
|
323
|
-
type: 'fields',
|
|
324
|
-
fields: [
|
|
325
|
-
{ name: 'avatar', maxCount: 1 },
|
|
326
|
-
{ name: 'attachments', maxCount: 5 },
|
|
327
|
-
],
|
|
328
|
-
}),
|
|
329
|
-
)
|
|
330
|
-
uploadFields(@Body() body: { avatar: string[]; attachments: string[] }) {
|
|
331
|
-
return body;
|
|
332
|
-
}
|
|
333
|
-
}
|
|
334
|
-
```
|
|
335
|
-
|
|
336
|
-
## Validation
|
|
337
|
-
|
|
338
|
-
Use the interceptor options for validation, not the module registration.
|
|
339
|
-
|
|
340
|
-
### File size and mime type validation
|
|
341
|
-
|
|
342
|
-
Use `multerOptions()` when you want Multer to reject invalid uploads before your route handler runs.
|
|
343
|
-
|
|
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';
|
|
353
|
-
|
|
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;
|
|
376
|
-
}
|
|
377
|
-
}
|
|
378
|
-
```
|
|
379
|
-
|
|
380
|
-
### Cross-field or post-upload validation
|
|
381
|
-
|
|
382
|
-
Use `afterUpload()` when validation depends on the final uploaded file list or multiple fields together.
|
|
383
|
-
|
|
384
|
-
```ts
|
|
385
|
-
import { BadRequestException, Body, Controller, Post, UseInterceptors } from '@nestjs/common';
|
|
386
|
-
import { FileStorageInterceptor } from '@ackplus/nest-file-storage';
|
|
387
|
-
|
|
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
|
-
}
|
|
414
|
-
}
|
|
415
|
-
```
|
|
416
|
-
|
|
417
|
-
Practical guidance:
|
|
418
|
-
|
|
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.
|
|
423
|
-
|
|
424
|
-
## Control The Stored Path And Key
|
|
425
|
-
|
|
426
|
-
Two callbacks define where the file is stored and what key is saved:
|
|
427
|
-
|
|
428
|
-
- `fileDist(file, req)`: the directory or path prefix.
|
|
429
|
-
- `fileName(file, req)`: the final filename segment.
|
|
430
|
-
|
|
431
|
-
The final key is effectively:
|
|
432
|
-
|
|
433
|
-
```text
|
|
434
|
-
<fileDist>/<fileName>
|
|
435
|
-
```
|
|
436
|
-
|
|
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
|
-
});
|
|
459
|
-
```
|
|
460
|
-
|
|
461
|
-
This gives you keys such as:
|
|
462
|
-
|
|
463
|
-
```text
|
|
464
|
-
users/42/avatars/1713876155123.png
|
|
465
|
-
```
|
|
466
|
-
|
|
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.
|
|
468
|
-
|
|
469
|
-
## Map Upload Results Into `request.body`
|
|
470
|
-
|
|
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';
|
|
477
|
-
import { FileStorageInterceptor } from '@ackplus/nest-file-storage';
|
|
478
|
-
|
|
479
|
-
@Controller('documents')
|
|
480
|
-
export class DocumentsController {
|
|
481
|
-
@Post()
|
|
482
|
-
@UseInterceptors(
|
|
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
|
-
}),
|
|
493
|
-
)
|
|
494
|
-
upload(@Body() body: any) {
|
|
495
|
-
return body.document;
|
|
496
|
-
}
|
|
497
|
-
}
|
|
498
|
-
```
|
|
499
|
-
|
|
500
|
-
### Preserve an existing body field
|
|
501
|
-
|
|
502
|
-
If the request already contains a JSON/text field with the same name and you only want to populate it when missing:
|
|
503
|
-
|
|
504
|
-
```ts
|
|
505
|
-
FileStorageInterceptor('file', {
|
|
506
|
-
overwriteBodyField: false,
|
|
507
|
-
});
|
|
508
|
-
```
|
|
509
|
-
|
|
510
|
-
## Use `FileStorageService` Programmatically
|
|
511
|
-
|
|
512
|
-
`FileStorageService.getStorage()` gives you the active storage implementation so you can upload or manage files outside controller interceptors.
|
|
513
|
-
|
|
514
|
-
```ts
|
|
515
|
-
import { Injectable } from '@nestjs/common';
|
|
516
|
-
import { FileStorageService } from '@ackplus/nest-file-storage';
|
|
517
|
-
|
|
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
|
-
}
|
|
524
|
-
|
|
525
|
-
async get(key: string) {
|
|
526
|
-
const storage = await FileStorageService.getStorage();
|
|
527
|
-
return storage.getFile(key);
|
|
528
|
-
}
|
|
529
|
-
|
|
530
|
-
async remove(key: string) {
|
|
531
|
-
const storage = await FileStorageService.getStorage();
|
|
532
|
-
await storage.deleteFile(key);
|
|
533
|
-
}
|
|
534
|
-
|
|
535
|
-
async copy(oldKey: string, newKey: string) {
|
|
536
|
-
const storage = await FileStorageService.getStorage();
|
|
537
|
-
return storage.copyFile(oldKey, newKey);
|
|
538
|
-
}
|
|
539
|
-
|
|
540
|
-
async url(key: string) {
|
|
541
|
-
const storage = await FileStorageService.getStorage();
|
|
542
|
-
return storage.getUrl(key);
|
|
543
|
-
}
|
|
544
|
-
|
|
545
|
-
async signedUrl(key: string) {
|
|
546
|
-
const storage = await FileStorageService.getStorage();
|
|
547
|
-
if ('getSignedUrl' in storage && storage.getSignedUrl) {
|
|
548
|
-
return storage.getSignedUrl(key, { expiresIn: 3600 });
|
|
549
|
-
}
|
|
550
|
-
return storage.getUrl(key);
|
|
551
|
-
}
|
|
552
|
-
}
|
|
553
|
-
```
|
|
554
|
-
|
|
555
|
-
### Get full URL from a stored key/path
|
|
556
|
-
|
|
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.
|
|
558
|
-
|
|
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
|
-
}
|
|
573
|
-
}
|
|
574
|
-
```
|
|
575
|
-
|
|
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
|
-
};
|
|
69
|
+
upload(@Body() body: { file: string }) {
|
|
70
|
+
return { key: body.file }; // body.file is the stored key
|
|
600
71
|
}
|
|
601
72
|
}
|
|
602
73
|
```
|
|
603
74
|
|
|
604
|
-
|
|
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
|
|
623
|
-
|
|
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
|
-
}
|
|
652
|
-
}
|
|
653
|
-
```
|
|
654
|
-
|
|
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.
|
|
685
|
-
|
|
686
|
-
### Azure config
|
|
687
|
-
|
|
688
|
-
- `account`
|
|
689
|
-
- `accountKey`
|
|
690
|
-
- `container`
|
|
691
|
-
- Common file options listed above.
|
|
692
|
-
|
|
693
|
-
Azure note:
|
|
75
|
+
That's the basics. Everything else — providers, custom drivers, multi-tenant, validation, the service API — is in the docs.
|
|
694
76
|
|
|
695
|
-
|
|
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.
|
|
77
|
+
## Documentation
|
|
697
78
|
|
|
698
|
-
|
|
79
|
+
| Guide | |
|
|
80
|
+
| --- | --- |
|
|
81
|
+
| 🚀 [Getting started](https://ack-solutions.github.io/nest-file-storage/getting-started) | install, configure, async setup |
|
|
82
|
+
| 🧩 [Core concepts](https://ack-solutions.github.io/nest-file-storage/concepts) | drivers, the registry, the file model |
|
|
83
|
+
| 🗄️ [Providers](https://ack-solutions.github.io/nest-file-storage/providers) | Local / S3 / Azure + comparison |
|
|
84
|
+
| 🔌 [Custom drivers](https://ack-solutions.github.io/nest-file-storage/custom-drivers) | implement your own backend |
|
|
85
|
+
| 🏢 [Multi-tenant storage](https://ack-solutions.github.io/nest-file-storage/multi-tenant) | per-request / per-tenant routing |
|
|
86
|
+
| ✅ [Validation & limits](https://ack-solutions.github.io/nest-file-storage/validation) | size, MIME, extension, count |
|
|
87
|
+
| ⬆️ [Uploading in controllers](https://ack-solutions.github.io/nest-file-storage/uploading) | single / array / fields, key control |
|
|
88
|
+
| 🛠️ [Using the service](https://ack-solutions.github.io/nest-file-storage/service) | programmatic access |
|
|
89
|
+
| 📚 [API reference](https://ack-solutions.github.io/nest-file-storage/api) | every option |
|
|
90
|
+
| 🔄 [Migration v1 → v2](https://ack-solutions.github.io/nest-file-storage/migration) | upgrade path |
|
|
699
91
|
|
|
700
|
-
|
|
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.
|
|
92
|
+
Runnable snippets also live in [`examples/`](./examples).
|
|
706
93
|
|
|
707
|
-
##
|
|
94
|
+
## Upgrading from v1?
|
|
708
95
|
|
|
709
|
-
-
|
|
710
|
-
- Workspace overview: [`../../README.md`](../../README.md)
|
|
711
|
-
- Example app: [`../../apps/example-app/README.md`](../../apps/example-app/README.md)
|
|
96
|
+
v2 is a redesign around a driver registry. Most v1 apps keep booting — the old `forRoot({ storage, *Config })` config is auto-translated and the static `FileStorageService.getStorage()` still works, both with deprecation warnings. See the **[migration guide](https://ack-solutions.github.io/nest-file-storage/migration)**.
|
|
712
97
|
|
|
713
98
|
## License
|
|
714
99
|
|