@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.
Files changed (86) hide show
  1. package/CHANGELOG.md +80 -0
  2. package/MIGRATION.md +220 -0
  3. package/README.md +49 -664
  4. package/dist/index.d.ts +1 -4
  5. package/dist/index.js +1 -4
  6. package/dist/index.js.map +1 -1
  7. package/dist/lib/constants.d.ts +3 -1
  8. package/dist/lib/constants.js +4 -2
  9. package/dist/lib/constants.js.map +1 -1
  10. package/dist/lib/driver-registry.d.ts +34 -0
  11. package/dist/lib/driver-registry.js +118 -0
  12. package/dist/lib/driver-registry.js.map +1 -0
  13. package/dist/lib/drivers/azure.driver.d.ts +21 -0
  14. package/dist/lib/drivers/azure.driver.js +91 -0
  15. package/dist/lib/drivers/azure.driver.js.map +1 -0
  16. package/dist/lib/drivers/driver.interface.d.ts +40 -0
  17. package/dist/lib/drivers/driver.interface.js +3 -0
  18. package/dist/lib/drivers/driver.interface.js.map +1 -0
  19. package/dist/lib/drivers/driver.util.d.ts +2 -0
  20. package/dist/lib/drivers/driver.util.js +15 -0
  21. package/dist/lib/drivers/driver.util.js.map +1 -0
  22. package/dist/lib/drivers/index.d.ts +7 -0
  23. package/dist/lib/drivers/index.js +39 -0
  24. package/dist/lib/drivers/index.js.map +1 -0
  25. package/dist/lib/drivers/local.driver.d.ts +15 -0
  26. package/dist/lib/drivers/local.driver.js +110 -0
  27. package/dist/lib/drivers/local.driver.js.map +1 -0
  28. package/dist/lib/drivers/s3.driver.d.ts +22 -0
  29. package/dist/lib/drivers/s3.driver.js +103 -0
  30. package/dist/lib/drivers/s3.driver.js.map +1 -0
  31. package/dist/lib/file-storage.service.d.ts +16 -5
  32. package/dist/lib/file-storage.service.js +60 -22
  33. package/dist/lib/file-storage.service.js.map +1 -1
  34. package/dist/lib/index.d.ts +9 -2
  35. package/dist/lib/index.js +15 -2
  36. package/dist/lib/index.js.map +1 -1
  37. package/dist/lib/interceptor/file-storage.interceptor.d.ts +7 -10
  38. package/dist/lib/interceptor/file-storage.interceptor.js +117 -112
  39. package/dist/lib/interceptor/file-storage.interceptor.js.map +1 -1
  40. package/dist/lib/multer/driver-multer-engine.d.ts +18 -0
  41. package/dist/lib/multer/driver-multer-engine.js +91 -0
  42. package/dist/lib/multer/driver-multer-engine.js.map +1 -0
  43. package/dist/lib/nest-file-storage.module.d.ts +3 -3
  44. package/dist/lib/nest-file-storage.module.js +81 -44
  45. package/dist/lib/nest-file-storage.module.js.map +1 -1
  46. package/dist/lib/registry-holder.d.ts +6 -0
  47. package/dist/lib/registry-holder.js +26 -0
  48. package/dist/lib/registry-holder.js.map +1 -0
  49. package/dist/lib/tenant/tenant-from.d.ts +14 -0
  50. package/dist/lib/tenant/tenant-from.js +71 -0
  51. package/dist/lib/tenant/tenant-from.js.map +1 -0
  52. package/dist/lib/tenant/tenant.types.d.ts +20 -0
  53. package/dist/lib/tenant/tenant.types.js +3 -0
  54. package/dist/lib/tenant/tenant.types.js.map +1 -0
  55. package/dist/lib/types.d.ts +45 -35
  56. package/dist/lib/types.js.map +1 -1
  57. package/dist/lib/validation.d.ts +22 -0
  58. package/dist/lib/validation.js +98 -0
  59. package/dist/lib/validation.js.map +1 -0
  60. package/dist/tsconfig.build.tsbuildinfo +1 -1
  61. package/examples/1-basic-local-storage.example.ts +11 -7
  62. package/examples/10-testing.example.ts +60 -196
  63. package/examples/11-custom-driver.example.ts +82 -0
  64. package/examples/12-multi-tenant.example.ts +93 -0
  65. package/examples/2-s3-storage.example.ts +18 -16
  66. package/examples/3-azure-storage.example.ts +14 -12
  67. package/examples/4-upload-controller.example.ts +20 -55
  68. package/examples/5-custom-configuration.example.ts +37 -57
  69. package/examples/6-file-service.example.ts +34 -91
  70. package/examples/7-user-avatar.example.ts +37 -92
  71. package/examples/8-document-management.example.ts +45 -196
  72. package/examples/9-dynamic-storage.example.ts +29 -147
  73. package/examples/README.md +25 -107
  74. package/package.json +17 -4
  75. package/dist/lib/storage/azure.storage.d.ts +0 -18
  76. package/dist/lib/storage/azure.storage.js +0 -210
  77. package/dist/lib/storage/azure.storage.js.map +0 -1
  78. package/dist/lib/storage/local.storage.d.ts +0 -20
  79. package/dist/lib/storage/local.storage.js +0 -212
  80. package/dist/lib/storage/local.storage.js.map +0 -1
  81. package/dist/lib/storage/s3.storage.d.ts +0 -19
  82. package/dist/lib/storage/s3.storage.js +0 -241
  83. package/dist/lib/storage/s3.storage.js.map +0 -1
  84. package/dist/lib/storage.factory.d.ts +0 -8
  85. package/dist/lib/storage.factory.js +0 -46
  86. 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
- `@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.
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
- This README is the canonical developer guide for using the package in an application.
5
+ [![npm](https://img.shields.io/npm/v/@ackplus/nest-file-storage.svg)](https://www.npmjs.com/package/@ackplus/nest-file-storage)
6
+ [![docs](https://img.shields.io/badge/docs-website-3b82f6.svg)](https://ack-solutions.github.io/nest-file-storage/)
7
+ [![downloads](https://img.shields.io/npm/dm/@ackplus/nest-file-storage.svg)](https://www.npmjs.com/package/@ackplus/nest-file-storage)
8
+ [![license](https://img.shields.io/npm/l/@ackplus/nest-file-storage.svg)](./LICENSE)
6
9
 
7
- ## What You Get
10
+ ## 📖 Documentation → **<https://ack-solutions.github.io/nest-file-storage/>**
8
11
 
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.
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
- ## Compatibility And Prerequisites
14
+ ---
16
15
 
17
- This library is designed for NestJS on the Express platform.
16
+ ## Why this library?
18
17
 
19
- Required in the consuming app:
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
- - `@nestjs/common`
22
- - `@nestjs/core`
23
- - `@nestjs/platform-express`
24
- - `multer`
25
- - `reflect-metadata`
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
- 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
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
- pnpm add @azure/storage-blob
61
- ```
62
-
63
- ## Register The Module
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
- ### AWS S3
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
- ```ts
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
- ### Azure Blob Storage
40
+ **1. Register a driver:**
109
41
 
110
42
  ```ts
111
43
  import { Module } from '@nestjs/common';
112
- import { FileStorageEnum, NestFileStorageModule } from '@ackplus/nest-file-storage';
44
+ import { NestFileStorageModule, localDriver } from '@ackplus/nest-file-storage';
113
45
 
114
46
  @Module({
115
47
  imports: [
116
48
  NestFileStorageModule.forRoot({
117
- storage: FileStorageEnum.AZURE,
118
- azureConfig: {
119
- account: process.env.AZURE_STORAGE_ACCOUNT!,
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
- 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
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('single')
67
+ @Post('upload')
278
68
  @UseInterceptors(FileStorageInterceptor('file'))
279
- uploadSingle(@Body() body: { file: string }) {
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
- 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
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
- - `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.
77
+ ## Documentation
697
78
 
698
- ## Current Behavior Notes
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
- - 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.
92
+ Runnable snippets also live in [`examples/`](./examples).
706
93
 
707
- ## Examples And Workspace Docs
94
+ ## Upgrading from v1?
708
95
 
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)
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