@ackplus/nest-file-storage 2.0.0 → 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 (3) hide show
  1. package/CHANGELOG.md +8 -0
  2. package/README.md +36 -405
  3. package/package.json +1 -1
package/CHANGELOG.md CHANGED
@@ -3,6 +3,14 @@
3
3
  All notable changes to `@ackplus/nest-file-storage` are documented here. This project adheres to
4
4
  [Semantic Versioning](https://semver.org/) and the [Keep a Changelog](https://keepachangelog.com/) format.
5
5
 
6
+ ## [2.0.1] - 2026-06-13
7
+
8
+ ### Changed
9
+
10
+ - Docs only. Trimmed the npm README to a concise landing page that links into the
11
+ [documentation site](https://ack-solutions.github.io/nest-file-storage/), and added documentation
12
+ links + badges to the package README and the repo root. No code or API changes.
13
+
6
14
  ## [2.0.0] - 2026-06-13
7
15
 
8
16
  A redesign around a **driver registry**. Custom storage providers now work everywhere, storage can be
package/README.md CHANGED
@@ -1,45 +1,36 @@
1
1
  # @ackplus/nest-file-storage
2
2
 
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 store one consistent file shape everywhere.
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
  [![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)
6
8
  [![license](https://img.shields.io/npm/l/@ackplus/nest-file-storage.svg)](./LICENSE)
7
9
 
10
+ ## 📖 Documentation → **<https://ack-solutions.github.io/nest-file-storage/>**
11
+
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/)**.
13
+
8
14
  ---
9
15
 
10
16
  ## Why this library?
11
17
 
12
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.
13
19
 
14
- - **Provider-agnostic.** Write your upload code once. Switch Local → S3 → Azure (or your own backend) by changing config, not controllers.
15
- - **Custom storage is first-class.** Implement a small `StorageDriver` interface and it works **everywhere** interceptor and service alike. No fork, no adapter glue.
16
- - **Per-request / multi-tenant storage.** Route each upload to the right bucket/folder based on the request (tenant, user plan, file type). Built-in caching means one client per tenant, not per request.
17
- - **Declarative validation.** Size, MIME type, extension, and file-count limits as data not hand-rolled checks scattered through your handlers. Rejections are typed `400`s.
18
- - **One result shape.** Every upload local or cloud — returns the same `UploadedFile` (`key`, `url`, `size`, …), so the rest of your app never branches on provider.
19
- - **Lean dependencies.** The AWS and Azure SDKs are optional peers, loaded lazily only if you use them. Install nothing extra for local storage.
20
- - **NestJS-native.** A dynamic module, an injectable service, and a route interceptor — the patterns you already use.
21
-
22
- > **Upgrading from v1?** See **[MIGRATION.md](./MIGRATION.md)**. Your old config still boots (with a deprecation warning) while you migrate.
23
-
24
- ## Contents
25
-
26
- - [Install](#install) · [Quick start](#quick-start) · [Core concepts](#core-concepts)
27
- - [Configure providers](#configure-providers) · [Provider comparison](#provider-comparison)
28
- - [Custom storage drivers](#custom-storage-drivers) · [Multi-tenant storage](#multi-tenant-storage)
29
- - [Uploading in controllers](#uploading-in-controllers) · [Validation & limits](#validation--limits)
30
- - [Mapping results into the body](#mapping-results-into-the-request-body) · [Using the service](#using-the-service-programmatically)
31
- - [The `UploadedFile` model (file state)](#the-uploadedfile-model-file-state) · [API reference](#api-reference)
32
-
33
- ---
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.
34
26
 
35
27
  ## Install
36
28
 
37
29
  ```bash
38
30
  npm i @ackplus/nest-file-storage multer reflect-metadata
39
- # AWS S3 (optional):
40
- npm i @aws-sdk/client-s3 @aws-sdk/s3-request-presigner
41
- # Azure Blob (optional):
42
- npm i @azure/storage-blob
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
43
34
  ```
44
35
 
45
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`.
@@ -57,10 +48,7 @@ import { NestFileStorageModule, localDriver } from '@ackplus/nest-file-storage';
57
48
  NestFileStorageModule.forRoot({
58
49
  default: 'local',
59
50
  drivers: {
60
- local: localDriver({
61
- rootPath: './uploads',
62
- baseUrl: 'http://localhost:3000/uploads',
63
- }),
51
+ local: localDriver({ rootPath: './uploads', baseUrl: 'http://localhost:3000/uploads' }),
64
52
  },
65
53
  }),
66
54
  ],
@@ -84,385 +72,28 @@ export class FilesController {
84
72
  }
85
73
  ```
86
74
 
87
- The interceptor parses `multipart/form-data`, stores the file, and writes the storage key into `request.body.file` before your handler runs.
88
-
89
- ## Core concepts
90
-
91
- - **Driver** — a backend implementing the [`StorageDriver`](#the-storagedriver-interface) contract (put/get/delete/copy/url). Built-ins: `localDriver`, `s3Driver`, `azureDriver`. Bring your own with `defineDriver`.
92
- - **Registry** — the module builds a registry from your `drivers` map. Drivers are instantiated **once** and cached. The interceptor and the service both resolve from it, so everything behaves the same.
93
- - **Default driver** — `default` names the driver used when nothing else is specified.
94
- - **`UploadedFile`** — the one result shape returned by every driver. See [file state](#the-uploadedfile-model-file-state).
95
-
96
- ## Configure providers
97
-
98
- ### Local
99
-
100
- ```ts
101
- localDriver({ rootPath: './uploads', baseUrl: 'http://localhost:3000/uploads' })
102
- ```
103
-
104
- > `baseUrl` only builds the URL string — it does not serve files. Add static serving (e.g. `@nestjs/serve-static`) for browser access, or stream files through your own controller.
105
-
106
- ### AWS S3 (and S3-compatible: MinIO, R2, Spaces)
75
+ That's the basics. Everything else providers, custom drivers, multi-tenant, validation, the service API is in the docs.
107
76
 
108
- ```ts
109
- s3Driver({
110
- accessKeyId: process.env.AWS_ACCESS_KEY_ID!,
111
- secretAccessKey: process.env.AWS_SECRET_ACCESS_KEY!,
112
- region: process.env.AWS_REGION!,
113
- bucket: process.env.AWS_BUCKET!,
114
- endpoint: process.env.S3_ENDPOINT, // optional, for S3-compatible
115
- cloudFrontUrl: process.env.CDN_URL, // optional, used by getUrl()
116
- })
117
- ```
118
-
119
- ### Azure Blob Storage
120
-
121
- ```ts
122
- azureDriver({
123
- account: process.env.AZURE_ACCOUNT!,
124
- accountKey: process.env.AZURE_KEY!,
125
- container: process.env.AZURE_CONTAINER!,
126
- cdnUrl: process.env.AZURE_CDN_URL, // optional, used by getSignedUrl()
127
- })
128
- ```
129
-
130
- ### Async configuration
131
-
132
- Use `forRootAsync` when config comes from `ConfigService`, a database, etc.:
133
-
134
- ```ts
135
- NestFileStorageModule.forRootAsync({
136
- imports: [ConfigModule],
137
- inject: [ConfigService],
138
- useFactory: (config: ConfigService) => ({
139
- default: 's3',
140
- drivers: {
141
- s3: s3Driver({
142
- accessKeyId: config.getOrThrow('AWS_ACCESS_KEY_ID'),
143
- secretAccessKey: config.getOrThrow('AWS_SECRET_ACCESS_KEY'),
144
- region: config.getOrThrow('AWS_REGION'),
145
- bucket: config.getOrThrow('AWS_BUCKET'),
146
- }),
147
- },
148
- }),
149
- });
150
- ```
151
-
152
- ### Provider comparison
153
-
154
- | Capability | Local | S3 | Azure | Custom |
155
- | --- | :---: | :---: | :---: | :---: |
156
- | `putFile` / `getFile` / `deleteFile` / `copyFile` | ✅ | ✅ | ✅ | ✅ |
157
- | `getUrl` | ✅ | ✅ | ✅ | ✅ |
158
- | `getSignedUrl` | — | ✅ | ✅ (SAS) | optional |
159
- | `path` (absolute FS path) | ✅ | — | — | optional |
160
- | Public URLs need extra setup | static serving | bucket/CDN policy | container/CDN policy | your call |
161
- | Extra dependency | none | `@aws-sdk/*` | `@azure/storage-blob` | your SDK |
162
-
163
- ## Custom storage drivers
164
-
165
- Implement the small [`StorageDriver`](#the-storagedriver-interface) interface and register it with `defineDriver`. It then works in the interceptor, the service, and tenant resolution — identically to the built-ins.
166
-
167
- ```ts
168
- import { defineDriver, StorageDriver, UploadedFile } from '@ackplus/nest-file-storage';
169
- import { Storage } from '@google-cloud/storage';
77
+ ## Documentation
170
78
 
171
- class GcsDriver implements StorageDriver {
172
- private storage = new Storage();
173
- constructor(private opts: { bucket: string }) {}
174
-
175
- async putFile(content: Buffer, key: string, meta?): Promise<UploadedFile> {
176
- await this.storage.bucket(this.opts.bucket).file(key).save(content, {
177
- contentType: meta?.contentType,
178
- });
179
- return {
180
- key, url: this.getUrl(key), originalName: key.split('/').pop()!,
181
- fileName: key.split('/').pop()!, size: content.length, fullPath: key,
182
- };
183
- }
184
- async getFile(key: string) { const [buf] = await this.storage.bucket(this.opts.bucket).file(key).download(); return buf; }
185
- async deleteFile(key: string) { await this.storage.bucket(this.opts.bucket).file(key).delete(); }
186
- async copyFile(src: string, dest: string) { /* copy + return UploadedFile */ }
187
- getUrl(key: string) { return `https://storage.googleapis.com/${this.opts.bucket}/${key}`; }
188
- }
189
-
190
- NestFileStorageModule.forRoot({
191
- default: 'gcs',
192
- drivers: { gcs: defineDriver(GcsDriver, { bucket: 'my-bucket' }) },
193
- });
194
- ```
195
-
196
- For async setup, register a plain factory instead: `drivers: { gcs: async () => new GcsDriver(await load()) }`.
197
-
198
- ## Multi-tenant storage
199
-
200
- Route each upload to the right storage based on the request — a different **folder per tenant** in one bucket, a **dedicated bucket per tenant**, or a mix. The library does no authentication: your guard/middleware identifies the tenant; these hooks read it.
201
-
202
- ```ts
203
- import { NestFileStorageModule, localDriver, s3Driver, tenantFrom } from '@ackplus/nest-file-storage';
204
-
205
- NestFileStorageModule.forRootAsync({
206
- inject: [TenantStorageService],
207
- useFactory: (tenants: TenantStorageService) => ({
208
- default: 'local',
209
- drivers: {
210
- local: localDriver({ rootPath: './uploads', baseUrl: 'http://localhost:3000/uploads' }),
211
- },
212
- tenant: {
213
- // Identify the tenant — try several strategies in order.
214
- resolve: tenantFrom.first(
215
- tenantFrom.jwt('tenantId'), // req.user.tenantId (after your auth guard)
216
- tenantFrom.subdomain(), // acme.app.com -> 'acme'
217
- tenantFrom.header('x-tenant-id'),
218
- ),
219
- // Resolve a tenant -> storage. Cached by tenant id (this runs once per tenant).
220
- driver: async (tenantId) => {
221
- const cfg = await tenants.find(tenantId); // your DB lookup
222
- if (cfg?.dedicated) {
223
- return { factory: s3Driver({ bucket: cfg.bucket, region: cfg.region,
224
- accessKeyId: cfg.key, secretAccessKey: cfg.secret }) }; // dedicated bucket
225
- }
226
- return { use: 'local', prefix: `tenants/${tenantId}` }; // shared + folder
227
- },
228
- cache: { ttlMs: 10 * 60_000, max: 500 },
229
- fallback: 'default', // no tenant on the request -> use the default driver ('error' to 400)
230
- },
231
- }),
232
- });
233
- ```
234
-
235
- Controllers need **no tenant-specific code** — a plain `FileStorageInterceptor('file')` is routed automatically:
236
-
237
- ```ts
238
- @Post('upload')
239
- @UseInterceptors(FileStorageInterceptor('file'))
240
- upload(@Body() body: { file: string }) { return { key: body.file }; }
241
- ```
242
-
243
- Outside a request (jobs, URL generation), resolve a tenant's driver programmatically — it uses the same cache:
244
-
245
- ```ts
246
- const { driver, prefix } = await this.fileStorage.getTenantDriver('acme');
247
- const url = await driver.getUrl(key);
248
- ```
249
-
250
- When a tenant changes its storage settings, drop the cached driver: `this.fileStorage.getRegistry().invalidateTenant('acme')`.
251
-
252
- **`tenant.driver(tenantId)` returns one of:**
253
-
254
- | Return | Meaning |
79
+ | Guide | |
255
80
  | --- | --- |
256
- | `'local'` | a registered driver, no prefix |
257
- | `{ use: 'local', prefix: 'tenants/acme' }` | a shared driver + per-tenant key prefix (folder isolation) |
258
- | `{ factory: s3Driver({...}), prefix? }` | a dedicated driver built for this tenant (bucket isolation) |
259
-
260
- ## Uploading in controllers
261
-
262
- `FileStorageInterceptor(field, options?)` accepts `multipart/form-data`, stores the file(s), and writes the result into `request.body`.
263
-
264
- ```ts
265
- // Single file (field name 'file')
266
- @UseInterceptors(FileStorageInterceptor('file'))
267
- upload(@Body() body: { file: string }) {}
268
-
269
- // Multiple files in one field
270
- @UseInterceptors(FileStorageInterceptor({ type: 'array', fieldName: 'photos', maxCount: 5 }))
271
- uploadMany(@Body() body: { photos: string[] }) {}
272
-
273
- // Multiple named fields
274
- @UseInterceptors(FileStorageInterceptor({
275
- type: 'fields',
276
- fields: [{ name: 'avatar', maxCount: 1 }, { name: 'docs', maxCount: 10 }],
277
- }))
278
- uploadFields(@Body() body: { avatar: string[]; docs: string[] }) {}
279
- ```
280
-
281
- | Mode | Accepted field(s) | Default `body` value |
282
- | --- | --- | --- |
283
- | `'file'` (string) | `file` | `body.file: string` |
284
- | `{ type: 'array' }` | repeated `photos` or `photos[0]`, `photos[1]`, … | `body.photos: string[]` |
285
- | `{ type: 'fields' }` | each named field | `body[field]: string[]` |
286
-
287
- ### Control the stored key
288
-
289
- ```ts
290
- FileStorageInterceptor('avatar', {
291
- fileDist: (_file, req) => `users/${req.user.id}/avatars`, // folder (relative)
292
- fileName: (file) => `${Date.now()}${extname(file.originalname)}`, // last segment
293
- prefix: 'public', // optional static prefix
294
- });
295
- // -> public/users/42/avatars/1713876155123.png
296
- ```
297
-
298
- The final key is `joinKey(prefix, fileDist, fileName)`. Defaults: `fileDist` = `YYYY/MM/DD`, `fileName` = `uuid-originalname`.
299
-
300
- ### Override the driver for one route
301
-
302
- ```ts
303
- FileStorageInterceptor('file', { driver: 's3' }); // by name
304
- FileStorageInterceptor('file', { driver: (req) => req.user.plan }); // dynamically
305
- FileStorageInterceptor('file', { tenant: false }); // opt out of tenant routing
306
- ```
307
-
308
- ## Validation & limits
309
-
310
- Declare limits as data — at the module level (applies to every route) and/or per route (merged over the module default). Rejections throw typed `400`s.
311
-
312
- ```ts
313
- // module-wide default
314
- validation: { maxSize: 10 * 1024 * 1024 }
315
-
316
- // per route
317
- FileStorageInterceptor('image', {
318
- validation: {
319
- maxSize: 5 * 1024 * 1024, // bytes
320
- allowedMimeTypes: ['image/jpeg', 'image/png', 'image/*'], // wildcards supported
321
- allowedExtensions: ['.jpg', '.png'], // case-insensitive
322
- maxFiles: 10, // total files per request
323
- fileFilter: (req, file, cb) => cb(null, true), // escape hatch (runs after the above)
324
- },
325
- });
326
- ```
327
-
328
- | Rule | Becomes | Throws on violation |
329
- | --- | --- | --- |
330
- | `maxSize` | Multer `limits.fileSize` | `FileTooLargeException` |
331
- | `maxFiles` | Multer `limits.files` | `TooManyFilesException` |
332
- | `allowedMimeTypes` / `allowedExtensions` | generated `fileFilter` | `InvalidFileTypeException` |
333
-
334
- All three extend `BadRequestException`, so they surface as standard `400` responses. For cross-field rules (e.g. "at least 2 images"), use `afterUpload`:
335
-
336
- ```ts
337
- FileStorageInterceptor({ type: 'fields', fields: [{ name: 'images', maxCount: 10 }] }, {
338
- afterUpload: (req) => {
339
- const files = req.files as Record<string, Express.Multer.File[]>;
340
- if ((files?.images?.length ?? 0) < 2) throw new BadRequestException('At least 2 images required.');
341
- },
342
- });
343
- ```
344
-
345
- ## Mapping results into the request body
346
-
347
- By default the interceptor writes the storage **key** into `request.body[field]`. Customize it with `mapToRequestBody`:
348
-
349
- ```ts
350
- FileStorageInterceptor('document', {
351
- mapToRequestBody: (file) => ({ key: file.key, url: file.url, size: file.size }),
352
- });
353
- // body.document === { key, url, size }
354
- ```
355
-
356
- Set `overwriteBodyField: false` to keep an existing body value (e.g. a JSON field with the same name on a PATCH).
357
-
358
- ## Using the service programmatically
359
-
360
- `FileStorageService` is injectable (the module is global). Use it in services, jobs, or response mappers.
361
-
362
- ```ts
363
- import { Injectable } from '@nestjs/common';
364
- import { FileStorageService } from '@ackplus/nest-file-storage';
365
-
366
- @Injectable()
367
- export class DocsService {
368
- constructor(private readonly fileStorage: FileStorageService) {}
369
-
370
- upload(buf: Buffer, key: string) { return this.fileStorage.putFile(buf, key); }
371
- read(key: string) { return this.fileStorage.getFile(key); }
372
- remove(key: string) { return this.fileStorage.deleteFile(key); }
373
- url(key: string) { return this.fileStorage.getUrl(key); }
374
- signedUrl(key: string) { return this.fileStorage.getSignedUrl(key, { expiresIn: 3600 }); }
375
-
376
- // a specific driver, or a tenant's driver:
377
- async s3() { return this.fileStorage.getDriver('s3'); }
378
- async forTenant(id: string) { return this.fileStorage.getTenantDriver(id); }
379
- }
380
- ```
381
-
382
- A common pattern: store only the `key` in your database and build the URL when serializing a response.
383
-
384
- ```ts
385
- async toResponse(user: { avatarKey?: string }) {
386
- return { ...user, avatarUrl: user.avatarKey ? await this.fileStorage.getUrl(user.avatarKey) : null };
387
- }
388
- ```
389
-
390
- ## The `UploadedFile` model (file state)
391
-
392
- Every driver returns the same shape from `putFile`/`copyFile`, and the interceptor exposes it to `mapToRequestBody`. Persist the **`key`** (stable); derive the **`url`** when you need it.
393
-
394
- ```ts
395
- interface UploadedFile {
396
- key: string; // storage key/path — persist THIS
397
- url: string; // public URL (local needs static serving)
398
- originalName: string; // original client filename
399
- fileName: string; // final filename segment of the key
400
- size: number; // bytes
401
- mimetype?: string; // when known
402
- fieldName?: string; // upload field it came from
403
- fullPath: string; // provider-native path (local absolute; cloud key)
404
- encoding?: string;
405
- buffer?: Buffer; // when the provider returns bytes
406
- }
407
- ```
408
-
409
- | Field | Use it for |
410
- | --- | --- |
411
- | `key` | the database value; the input to `getFile`/`getUrl`/`deleteFile`/`copyFile` |
412
- | `url` | rendering/links (regenerate from `key` via `getUrl` rather than storing) |
413
- | `fullPath` | local file operations; provider-native identifier |
414
- | `size` / `mimetype` / `originalName` | metadata you may want to persist alongside the key |
415
-
416
- ## API reference
417
-
418
- ### `NestFileStorageModule`
419
-
420
- - `forRoot(options)` / `forRootAsync(asyncOptions)` → a global `DynamicModule`.
421
- - Options: `{ default: string; drivers: Record<string, DriverFactory>; validation?: UploadValidation; tenant?: TenantOptions }`.
422
-
423
- ### Driver factories
424
-
425
- - `localDriver(options)` · `s3Driver(options)` · `azureDriver(options)` · `defineDriver(DriverClass, options?)` → `DriverFactory`.
426
-
427
- ### `FileStorageService` (injectable)
428
-
429
- - `getDriver(name?)` · `getTenantDriver(tenantId)` · `getRegistry()`
430
- - `putFile` · `getFile` · `deleteFile` · `copyFile` · `getUrl` · `getSignedUrl` (delegate to the default driver)
431
- - `static getStorage(name?)` — *deprecated* facade for non-DI call sites.
432
-
433
- ### `FileStorageInterceptor(field, options?)`
434
-
435
- `field`: a string (single file) or `{ type, fieldName?, maxCount?, fields? }`.
436
- `options`: `{ driver?, fileName?, fileDist?, prefix?, validation?, mapToRequestBody?, overwriteBodyField?, afterUpload?, tenant? }`.
437
-
438
- ### The `StorageDriver` interface
439
-
440
- ```ts
441
- interface StorageDriver {
442
- putFile(content: Buffer, key: string, meta?: PutFileMeta): Promise<UploadedFile>;
443
- getFile(key: string): Promise<Buffer>;
444
- deleteFile(key: string): Promise<void>;
445
- copyFile(sourceKey: string, destKey: string): Promise<UploadedFile>;
446
- getUrl(key: string): string | Promise<string>;
447
- getSignedUrl?(key: string, options?: SignedUrlOptions): Promise<string>;
448
- path?(key: string): string | Promise<string>;
449
- readonly keyDefaults?: { fileName?; fileDist?; prefix? };
450
- }
451
- ```
452
-
453
- ### Tenant resolvers — `tenantFrom`
454
-
455
- `jwt(path?)` · `header(name)` · `subdomain({ rootDomain?, ignore? })` · `param(name)` · `query(name)` · `first(...resolvers)`.
456
-
457
- ### Exceptions
458
-
459
- `FileTooLargeException` · `InvalidFileTypeException` · `TooManyFilesException` (all extend `BadRequestException`).
460
-
461
- ---
462
-
463
- ## More
464
-
465
- - **[Migration from v1](./MIGRATION.md)** · **[Changelog](./CHANGELOG.md)** · **[Examples](./examples/)**
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 |
91
+
92
+ Runnable snippets also live in [`examples/`](./examples).
93
+
94
+ ## Upgrading from v1?
95
+
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)**.
466
97
 
467
98
  ## License
468
99
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ackplus/nest-file-storage",
3
- "version": "2.0.0",
3
+ "version": "2.0.1",
4
4
  "description": "One file-storage API for NestJS — Local, S3, Azure, and custom drivers, with per-request/multi-tenant storage, declarative validation, and a unified file model.",
5
5
  "author": "AckPlus",
6
6
  "license": "MIT",