@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.
- package/CHANGELOG.md +8 -0
- package/README.md +36 -405
- 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
|
|
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)
|
|
6
8
|
[](./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
|
|
15
|
-
- **Custom storage is first-class
|
|
16
|
-
- **Per-request / multi-tenant
|
|
17
|
-
- **Declarative validation
|
|
18
|
-
- **One result shape
|
|
19
|
-
- **Lean dependencies
|
|
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
|
-
#
|
|
40
|
-
npm i @aws-sdk/client-s3 @aws-sdk/s3-request-presigner
|
|
41
|
-
# Azure Blob
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
|
257
|
-
|
|
|
258
|
-
|
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
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.
|
|
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",
|