@ackplus/nest-file-storage 1.1.22 → 2.0.0
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 +72 -0
- package/MIGRATION.md +220 -0
- package/README.md +353 -547
- 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 +119 -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,105 +1,66 @@
|
|
|
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 store one consistent file shape everywhere.
|
|
4
4
|
|
|
5
|
-
|
|
5
|
+
[](https://www.npmjs.com/package/@ackplus/nest-file-storage)
|
|
6
|
+
[](./LICENSE)
|
|
6
7
|
|
|
7
|
-
|
|
8
|
-
- 🔄 **Easy Switching** - Switch between storage providers with minimal configuration
|
|
9
|
-
- 🎯 **NestJS Integration** - Seamless integration with NestJS decorators and interceptors
|
|
10
|
-
- 📁 **File Operations** - Upload, download, delete, copy files with ease
|
|
11
|
-
- 🔐 **Signed URLs** - Generate presigned URLs for secure file access (S3)
|
|
12
|
-
- 🎨 **Customizable** - Custom file naming, directory structure, and transformations
|
|
13
|
-
- 📝 **TypeScript** - Full TypeScript support with type safety
|
|
14
|
-
- 🧪 **Test-Friendly** - Easy to mock and test
|
|
8
|
+
---
|
|
15
9
|
|
|
16
|
-
##
|
|
10
|
+
## Why this library?
|
|
17
11
|
|
|
18
|
-
|
|
19
|
-
npm install @ackplus/nest-file-storage
|
|
20
|
-
# or
|
|
21
|
-
pnpm add @ackplus/nest-file-storage
|
|
22
|
-
# or
|
|
23
|
-
yarn add @ackplus/nest-file-storage
|
|
24
|
-
```
|
|
12
|
+
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.
|
|
25
13
|
|
|
26
|
-
**
|
|
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.
|
|
27
21
|
|
|
28
|
-
|
|
29
|
-
npm install @aws-sdk/client-s3 @aws-sdk/s3-request-presigner
|
|
30
|
-
```
|
|
22
|
+
> **Upgrading from v1?** See **[MIGRATION.md](./MIGRATION.md)**. Your old config still boots (with a deprecation warning) while you migrate.
|
|
31
23
|
|
|
32
|
-
|
|
24
|
+
## Contents
|
|
33
25
|
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
### Step 1: Configure Module
|
|
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)
|
|
41
32
|
|
|
42
|
-
|
|
33
|
+
---
|
|
43
34
|
|
|
44
|
-
|
|
35
|
+
## Install
|
|
45
36
|
|
|
46
|
-
```
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
@
|
|
52
|
-
imports: [
|
|
53
|
-
NestFileStorageModule.forRoot({
|
|
54
|
-
storage: FileStorageEnum.LOCAL,
|
|
55
|
-
localConfig: {
|
|
56
|
-
rootPath: './uploads',
|
|
57
|
-
baseUrl: 'http://localhost:3000/uploads',
|
|
58
|
-
},
|
|
59
|
-
}),
|
|
60
|
-
],
|
|
61
|
-
})
|
|
62
|
-
export class AppModule {}
|
|
37
|
+
```bash
|
|
38
|
+
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
|
|
63
43
|
```
|
|
64
44
|
|
|
65
|
-
|
|
45
|
+
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`.
|
|
66
46
|
|
|
67
|
-
|
|
68
|
-
// app.module.ts
|
|
69
|
-
import { Module } from '@nestjs/common';
|
|
70
|
-
import { NestFileStorageModule, FileStorageEnum } from '@ackplus/nest-file-storage';
|
|
71
|
-
|
|
72
|
-
@Module({
|
|
73
|
-
imports: [
|
|
74
|
-
NestFileStorageModule.forRoot({
|
|
75
|
-
storage: FileStorageEnum.S3,
|
|
76
|
-
s3Config: {
|
|
77
|
-
accessKeyId: process.env.AWS_ACCESS_KEY_ID,
|
|
78
|
-
secretAccessKey: process.env.AWS_SECRET_ACCESS_KEY,
|
|
79
|
-
region: process.env.AWS_REGION,
|
|
80
|
-
bucket: process.env.AWS_BUCKET,
|
|
81
|
-
},
|
|
82
|
-
}),
|
|
83
|
-
],
|
|
84
|
-
})
|
|
85
|
-
export class AppModule {}
|
|
86
|
-
```
|
|
47
|
+
## Quick start
|
|
87
48
|
|
|
88
|
-
|
|
49
|
+
**1. Register a driver:**
|
|
89
50
|
|
|
90
|
-
```
|
|
91
|
-
// app.module.ts
|
|
51
|
+
```ts
|
|
92
52
|
import { Module } from '@nestjs/common';
|
|
93
|
-
import { NestFileStorageModule,
|
|
53
|
+
import { NestFileStorageModule, localDriver } from '@ackplus/nest-file-storage';
|
|
94
54
|
|
|
95
55
|
@Module({
|
|
96
56
|
imports: [
|
|
97
57
|
NestFileStorageModule.forRoot({
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
58
|
+
default: 'local',
|
|
59
|
+
drivers: {
|
|
60
|
+
local: localDriver({
|
|
61
|
+
rootPath: './uploads',
|
|
62
|
+
baseUrl: 'http://localhost:3000/uploads',
|
|
63
|
+
}),
|
|
103
64
|
},
|
|
104
65
|
}),
|
|
105
66
|
],
|
|
@@ -107,557 +68,402 @@ import { NestFileStorageModule, FileStorageEnum } from '@ackplus/nest-file-stora
|
|
|
107
68
|
export class AppModule {}
|
|
108
69
|
```
|
|
109
70
|
|
|
110
|
-
|
|
71
|
+
**2. Accept uploads in a controller:**
|
|
111
72
|
|
|
112
|
-
```
|
|
113
|
-
|
|
114
|
-
import { Controller, Post, UseInterceptors } from '@nestjs/common';
|
|
73
|
+
```ts
|
|
74
|
+
import { Body, Controller, Post, UseInterceptors } from '@nestjs/common';
|
|
115
75
|
import { FileStorageInterceptor } from '@ackplus/nest-file-storage';
|
|
116
76
|
|
|
117
|
-
@Controller('
|
|
118
|
-
export class
|
|
119
|
-
|
|
120
|
-
@Post('single')
|
|
77
|
+
@Controller('files')
|
|
78
|
+
export class FilesController {
|
|
79
|
+
@Post('upload')
|
|
121
80
|
@UseInterceptors(FileStorageInterceptor('file'))
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
return {
|
|
125
|
-
message: 'File uploaded successfully',
|
|
126
|
-
fileKey: body.file,
|
|
127
|
-
};
|
|
128
|
-
}
|
|
129
|
-
|
|
130
|
-
// Multiple files upload
|
|
131
|
-
@Post('multiple')
|
|
132
|
-
@UseInterceptors(
|
|
133
|
-
FileStorageInterceptor({
|
|
134
|
-
type: 'array',
|
|
135
|
-
fieldName: 'files',
|
|
136
|
-
maxCount: 10,
|
|
137
|
-
})
|
|
138
|
-
)
|
|
139
|
-
uploadMultiple(@Body() body: any) {
|
|
140
|
-
// File keys are automatically added to body.files as array
|
|
141
|
-
return {
|
|
142
|
-
message: 'Files uploaded successfully',
|
|
143
|
-
fileKeys: body.files,
|
|
144
|
-
};
|
|
145
|
-
}
|
|
146
|
-
|
|
147
|
-
// Multiple fields
|
|
148
|
-
@Post('fields')
|
|
149
|
-
@UseInterceptors(
|
|
150
|
-
FileStorageInterceptor({
|
|
151
|
-
type: 'fields',
|
|
152
|
-
fields: [
|
|
153
|
-
{ name: 'avatar', maxCount: 1 },
|
|
154
|
-
{ name: 'photos', maxCount: 5 },
|
|
155
|
-
],
|
|
156
|
-
})
|
|
157
|
-
)
|
|
158
|
-
uploadFields(@Body() body: any) {
|
|
159
|
-
return {
|
|
160
|
-
message: 'Files uploaded successfully',
|
|
161
|
-
avatar: body.avatar,
|
|
162
|
-
photos: body.photos,
|
|
163
|
-
};
|
|
81
|
+
upload(@Body() body: { file: string }) {
|
|
82
|
+
return { key: body.file }; // body.file is the stored key
|
|
164
83
|
}
|
|
165
84
|
}
|
|
166
85
|
```
|
|
167
86
|
|
|
168
|
-
|
|
87
|
+
The interceptor parses `multipart/form-data`, stores the file, and writes the storage key into `request.body.file` before your handler runs.
|
|
169
88
|
|
|
170
|
-
|
|
171
|
-
// file.service.ts
|
|
172
|
-
import { Injectable } from '@nestjs/common';
|
|
173
|
-
import { FileStorageService } from '@ackplus/nest-file-storage';
|
|
89
|
+
## Core concepts
|
|
174
90
|
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
const storage = await FileStorageService.getStorage();
|
|
180
|
-
return await storage.getFile(key);
|
|
181
|
-
}
|
|
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).
|
|
182
95
|
|
|
183
|
-
|
|
184
|
-
async deleteFile(key: string): Promise<void> {
|
|
185
|
-
const storage = await FileStorageService.getStorage();
|
|
186
|
-
await storage.deleteFile(key);
|
|
187
|
-
}
|
|
96
|
+
## Configure providers
|
|
188
97
|
|
|
189
|
-
|
|
190
|
-
async copyFile(oldKey: string, newKey: string) {
|
|
191
|
-
const storage = await FileStorageService.getStorage();
|
|
192
|
-
return await storage.copyFile(oldKey, newKey);
|
|
193
|
-
}
|
|
98
|
+
### Local
|
|
194
99
|
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
const storage = await FileStorageService.getStorage();
|
|
198
|
-
return storage.getUrl(key);
|
|
199
|
-
}
|
|
200
|
-
|
|
201
|
-
// Get signed URL (S3 only)
|
|
202
|
-
async getSignedUrl(key: string): Promise<string> {
|
|
203
|
-
const storage = await FileStorageService.getStorage();
|
|
204
|
-
if ('getSignedUrl' in storage) {
|
|
205
|
-
return await storage.getSignedUrl(key, { expiresIn: 3600 });
|
|
206
|
-
}
|
|
207
|
-
return storage.getUrl(key);
|
|
208
|
-
}
|
|
209
|
-
}
|
|
100
|
+
```ts
|
|
101
|
+
localDriver({ rootPath: './uploads', baseUrl: 'http://localhost:3000/uploads' })
|
|
210
102
|
```
|
|
211
103
|
|
|
212
|
-
|
|
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.
|
|
213
105
|
|
|
214
|
-
###
|
|
106
|
+
### AWS S3 (and S3-compatible: MinIO, R2, Spaces)
|
|
215
107
|
|
|
216
|
-
```
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
}
|
|
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
|
+
})
|
|
225
117
|
```
|
|
226
118
|
|
|
227
|
-
###
|
|
228
|
-
|
|
229
|
-
```typescript
|
|
230
|
-
interface S3StorageOptions {
|
|
231
|
-
accessKeyId: string; // AWS access key
|
|
232
|
-
secretAccessKey: string; // AWS secret key
|
|
233
|
-
region: string; // AWS region
|
|
234
|
-
bucket: string; // S3 bucket name
|
|
235
|
-
endpoint?: string; // Custom S3 endpoint (for S3-compatible services)
|
|
236
|
-
cloudFrontUrl?: string; // CloudFront distribution URL
|
|
237
|
-
prefix?: string; // Optional prefix for file keys
|
|
238
|
-
fileName?: (file: any, req: Request) => string; // Custom file naming
|
|
239
|
-
fileDist?: (file: any, req: Request) => string; // Custom directory structure
|
|
240
|
-
transformUploadedFileObject?: (file: any) => any; // Transform uploaded file object
|
|
241
|
-
}
|
|
242
|
-
```
|
|
119
|
+
### Azure Blob Storage
|
|
243
120
|
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
prefix?: string; // Optional prefix for file keys
|
|
252
|
-
fileName?: (file: any, req: Request) => string; // Custom file naming
|
|
253
|
-
fileDist?: (file: any, req: Request) => string; // Custom directory structure
|
|
254
|
-
transformUploadedFileObject?: (file: any) => any; // Transform uploaded file object
|
|
255
|
-
}
|
|
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
|
+
})
|
|
256
128
|
```
|
|
257
129
|
|
|
258
|
-
|
|
130
|
+
### Async configuration
|
|
259
131
|
|
|
260
|
-
|
|
132
|
+
Use `forRootAsync` when config comes from `ConfigService`, a database, etc.:
|
|
261
133
|
|
|
262
|
-
```
|
|
263
|
-
NestFileStorageModule.
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
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
|
+
}),
|
|
273
147
|
},
|
|
274
|
-
},
|
|
275
|
-
})
|
|
148
|
+
}),
|
|
149
|
+
});
|
|
276
150
|
```
|
|
277
151
|
|
|
278
|
-
###
|
|
152
|
+
### Provider comparison
|
|
279
153
|
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
const date = new Date();
|
|
289
|
-
return `${date.getFullYear()}/${date.getMonth() + 1}/${date.getDate()}`;
|
|
290
|
-
},
|
|
291
|
-
},
|
|
292
|
-
})
|
|
293
|
-
```
|
|
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 |
|
|
294
162
|
|
|
295
|
-
|
|
163
|
+
## Custom storage drivers
|
|
296
164
|
|
|
297
|
-
|
|
298
|
-
NestFileStorageModule.forRoot({
|
|
299
|
-
storage: FileStorageEnum.S3,
|
|
300
|
-
s3Config: {
|
|
301
|
-
accessKeyId: process.env.AWS_ACCESS_KEY_ID,
|
|
302
|
-
secretAccessKey: process.env.AWS_SECRET_ACCESS_KEY,
|
|
303
|
-
region: process.env.AWS_REGION,
|
|
304
|
-
bucket: process.env.AWS_BUCKET,
|
|
305
|
-
transformUploadedFileObject: (file) => {
|
|
306
|
-
// Return only specific fields
|
|
307
|
-
return {
|
|
308
|
-
key: file.key,
|
|
309
|
-
url: file.url,
|
|
310
|
-
size: file.size,
|
|
311
|
-
mimetype: file.mimetype,
|
|
312
|
-
};
|
|
313
|
-
},
|
|
314
|
-
},
|
|
315
|
-
})
|
|
316
|
-
```
|
|
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.
|
|
317
166
|
|
|
318
|
-
|
|
167
|
+
```ts
|
|
168
|
+
import { defineDriver, StorageDriver, UploadedFile } from '@ackplus/nest-file-storage';
|
|
169
|
+
import { Storage } from '@google-cloud/storage';
|
|
319
170
|
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
}
|
|
328
|
-
|
|
329
|
-
)
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
}
|
|
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}`; }
|
|
336
188
|
}
|
|
189
|
+
|
|
190
|
+
NestFileStorageModule.forRoot({
|
|
191
|
+
default: 'gcs',
|
|
192
|
+
drivers: { gcs: defineDriver(GcsDriver, { bucket: 'my-bucket' }) },
|
|
193
|
+
});
|
|
337
194
|
```
|
|
338
195
|
|
|
339
|
-
|
|
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';
|
|
340
204
|
|
|
341
|
-
```typescript
|
|
342
|
-
// app.module.ts
|
|
343
205
|
NestFileStorageModule.forRootAsync({
|
|
344
|
-
|
|
345
|
-
useFactory:
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
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)
|
|
352
230
|
},
|
|
353
231
|
}),
|
|
354
|
-
|
|
355
|
-
})
|
|
232
|
+
});
|
|
356
233
|
```
|
|
357
234
|
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
```
|
|
361
|
-
|
|
362
|
-
@
|
|
363
|
-
@
|
|
364
|
-
FileStorageInterceptor('file', {
|
|
365
|
-
storageType: FileStorageEnum.S3,
|
|
366
|
-
})
|
|
367
|
-
)
|
|
368
|
-
uploadToS3(@Body() body: any) {
|
|
369
|
-
return { fileKey: body.file };
|
|
370
|
-
}
|
|
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 }; }
|
|
371
241
|
```
|
|
372
242
|
|
|
373
|
-
|
|
243
|
+
Outside a request (jobs, URL generation), resolve a tenant's driver programmatically — it uses the same cache:
|
|
374
244
|
|
|
375
|
-
|
|
245
|
+
```ts
|
|
246
|
+
const { driver, prefix } = await this.fileStorage.getTenantDriver('acme');
|
|
247
|
+
const url = await driver.getUrl(key);
|
|
248
|
+
```
|
|
376
249
|
|
|
377
|
-
|
|
378
|
-
import { Controller, Post, UseInterceptors, BadRequestException } from '@nestjs/common';
|
|
379
|
-
import { FileStorageInterceptor } from '@ackplus/nest-file-storage';
|
|
250
|
+
When a tenant changes its storage settings, drop the cached driver: `this.fileStorage.getRegistry().invalidateTenant('acme')`.
|
|
380
251
|
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
imageKey: body.image,
|
|
409
|
-
};
|
|
410
|
-
}
|
|
411
|
-
}
|
|
252
|
+
**`tenant.driver(tenantId)` returns one of:**
|
|
253
|
+
|
|
254
|
+
| Return | Meaning |
|
|
255
|
+
| --- | --- |
|
|
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[] }) {}
|
|
412
279
|
```
|
|
413
280
|
|
|
414
|
-
|
|
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[]` |
|
|
415
286
|
|
|
416
|
-
|
|
417
|
-
import { Controller, Post, UseInterceptors, Body } from '@nestjs/common';
|
|
418
|
-
import { FileStorageInterceptor, FileStorageService } from '@ackplus/nest-file-storage';
|
|
287
|
+
### Control the stored key
|
|
419
288
|
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
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
|
+
```
|
|
423
297
|
|
|
424
|
-
|
|
425
|
-
@UseInterceptors(
|
|
426
|
-
FileStorageInterceptor('avatar', {
|
|
427
|
-
fileName: (file, req) => {
|
|
428
|
-
const userId = req.user.id; // Assuming user from auth guard
|
|
429
|
-
const ext = file.originalname.split('.').pop();
|
|
430
|
-
return `avatar-${userId}.${ext}`;
|
|
431
|
-
},
|
|
432
|
-
fileDist: () => 'avatars',
|
|
433
|
-
})
|
|
434
|
-
)
|
|
435
|
-
async uploadAvatar(@Body() body: any, @Request() req) {
|
|
436
|
-
// Delete old avatar if exists
|
|
437
|
-
const user = await this.userService.findById(req.user.id);
|
|
438
|
-
if (user.avatarKey) {
|
|
439
|
-
const storage = await FileStorageService.getStorage();
|
|
440
|
-
await storage.deleteFile(user.avatarKey);
|
|
441
|
-
}
|
|
442
|
-
|
|
443
|
-
// Update user with new avatar
|
|
444
|
-
await this.userService.updateAvatar(req.user.id, body.avatar);
|
|
298
|
+
The final key is `joinKey(prefix, fileDist, fileName)`. Defaults: `fileDist` = `YYYY/MM/DD`, `fileName` = `uuid-originalname`.
|
|
445
299
|
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
}
|
|
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
|
|
452
306
|
```
|
|
453
307
|
|
|
454
|
-
|
|
308
|
+
## Validation & limits
|
|
455
309
|
|
|
456
|
-
|
|
457
|
-
import { Controller, Get, Post, Delete, Param, UseInterceptors, Body } from '@nestjs/common';
|
|
458
|
-
import { FileStorageInterceptor, FileStorageService } from '@ackplus/nest-file-storage';
|
|
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.
|
|
459
311
|
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
@UseInterceptors(
|
|
464
|
-
FileStorageInterceptor({
|
|
465
|
-
type: 'array',
|
|
466
|
-
fieldName: 'documents',
|
|
467
|
-
maxCount: 10,
|
|
468
|
-
}, {
|
|
469
|
-
fileDist: () => 'documents',
|
|
470
|
-
mapToRequestBody: (files, fieldName) => {
|
|
471
|
-
// Return detailed file info
|
|
472
|
-
return files;
|
|
473
|
-
},
|
|
474
|
-
})
|
|
475
|
-
)
|
|
476
|
-
async uploadDocuments(@Body() body: any) {
|
|
477
|
-
return {
|
|
478
|
-
message: `${body.documents.length} documents uploaded`,
|
|
479
|
-
documents: body.documents,
|
|
480
|
-
};
|
|
481
|
-
}
|
|
312
|
+
```ts
|
|
313
|
+
// module-wide default
|
|
314
|
+
validation: { maxSize: 10 * 1024 * 1024 }
|
|
482
315
|
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
}
|
|
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
|
+
```
|
|
492
327
|
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
return { message: 'Document deleted successfully' };
|
|
499
|
-
}
|
|
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` |
|
|
500
333
|
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
}
|
|
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
|
+
});
|
|
509
343
|
```
|
|
510
344
|
|
|
511
|
-
##
|
|
345
|
+
## Mapping results into the request body
|
|
512
346
|
|
|
513
|
-
|
|
347
|
+
By default the interceptor writes the storage **key** into `request.body[field]`. Customize it with `mapToRequestBody`:
|
|
514
348
|
|
|
515
|
-
```
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
// Get module options
|
|
521
|
-
static getOptions(): FileStorageModuleOptions
|
|
522
|
-
|
|
523
|
-
// Set module options
|
|
524
|
-
static setOptions(options: FileStorageModuleOptions): void
|
|
525
|
-
}
|
|
349
|
+
```ts
|
|
350
|
+
FileStorageInterceptor('document', {
|
|
351
|
+
mapToRequestBody: (file) => ({ key: file.key, url: file.url, size: file.size }),
|
|
352
|
+
});
|
|
353
|
+
// body.document === { key, url, size }
|
|
526
354
|
```
|
|
527
355
|
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
|
|
541
|
-
|
|
542
|
-
|
|
543
|
-
|
|
544
|
-
|
|
545
|
-
|
|
546
|
-
|
|
547
|
-
|
|
548
|
-
|
|
549
|
-
|
|
550
|
-
|
|
551
|
-
path?(filePath: string): Promise<string> | string
|
|
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); }
|
|
552
379
|
}
|
|
553
380
|
```
|
|
554
381
|
|
|
555
|
-
|
|
556
|
-
|
|
557
|
-
```
|
|
558
|
-
|
|
559
|
-
|
|
560
|
-
|
|
561
|
-
options?: FileStorageInterceptorOptions
|
|
562
|
-
)
|
|
563
|
-
|
|
564
|
-
// Multiple files or fields
|
|
565
|
-
FileStorageInterceptor(
|
|
566
|
-
config: {
|
|
567
|
-
type: 'single' | 'array' | 'fields';
|
|
568
|
-
fieldName?: string;
|
|
569
|
-
maxCount?: number;
|
|
570
|
-
fields?: { name: string; maxCount?: number }[];
|
|
571
|
-
},
|
|
572
|
-
options?: FileStorageInterceptorOptions
|
|
573
|
-
)
|
|
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
|
+
}
|
|
574
388
|
```
|
|
575
389
|
|
|
576
|
-
|
|
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.
|
|
577
393
|
|
|
578
|
-
```
|
|
394
|
+
```ts
|
|
579
395
|
interface UploadedFile {
|
|
580
|
-
|
|
581
|
-
|
|
582
|
-
originalName: string;
|
|
583
|
-
|
|
584
|
-
|
|
585
|
-
|
|
586
|
-
|
|
587
|
-
|
|
588
|
-
|
|
589
|
-
|
|
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
|
|
590
406
|
}
|
|
591
407
|
```
|
|
592
408
|
|
|
593
|
-
|
|
594
|
-
|
|
595
|
-
|
|
596
|
-
|
|
597
|
-
|
|
598
|
-
|
|
599
|
-
|
|
600
|
-
|
|
601
|
-
|
|
602
|
-
|
|
603
|
-
beforeEach(async () => {
|
|
604
|
-
const module: TestingModule = await Test.createTestingModule({
|
|
605
|
-
imports: [
|
|
606
|
-
NestFileStorageModule.forRoot({
|
|
607
|
-
storage: FileStorageEnum.LOCAL,
|
|
608
|
-
localConfig: {
|
|
609
|
-
rootPath: './test-uploads',
|
|
610
|
-
baseUrl: 'http://localhost:3000/test-uploads',
|
|
611
|
-
},
|
|
612
|
-
}),
|
|
613
|
-
],
|
|
614
|
-
providers: [FileService],
|
|
615
|
-
}).compile();
|
|
616
|
-
|
|
617
|
-
service = module.get<FileService>(FileService);
|
|
618
|
-
storage = await FileStorageService.getStorage();
|
|
619
|
-
});
|
|
620
|
-
|
|
621
|
-
it('should upload file', async () => {
|
|
622
|
-
const buffer = Buffer.from('test content');
|
|
623
|
-
const result = await storage.putFile(buffer, 'test/file.txt');
|
|
624
|
-
|
|
625
|
-
expect(result.key).toBe('test/file.txt');
|
|
626
|
-
expect(result.size).toBeGreaterThan(0);
|
|
627
|
-
});
|
|
628
|
-
|
|
629
|
-
it('should delete file', async () => {
|
|
630
|
-
const buffer = Buffer.from('test content');
|
|
631
|
-
await storage.putFile(buffer, 'test/file.txt');
|
|
632
|
-
|
|
633
|
-
await storage.deleteFile('test/file.txt');
|
|
634
|
-
|
|
635
|
-
await expect(storage.getFile('test/file.txt')).rejects.toThrow();
|
|
636
|
-
});
|
|
637
|
-
});
|
|
638
|
-
```
|
|
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`
|
|
639
419
|
|
|
640
|
-
|
|
420
|
+
- `forRoot(options)` / `forRootAsync(asyncOptions)` → a global `DynamicModule`.
|
|
421
|
+
- Options: `{ default: string; drivers: Record<string, DriverFactory>; validation?: UploadValidation; tenant?: TenantOptions }`.
|
|
641
422
|
|
|
642
|
-
|
|
423
|
+
### Driver factories
|
|
643
424
|
|
|
644
|
-
|
|
425
|
+
- `localDriver(options)` · `s3Driver(options)` · `azureDriver(options)` · `defineDriver(DriverClass, options?)` → `DriverFactory`.
|
|
645
426
|
|
|
646
|
-
|
|
427
|
+
### `FileStorageService` (injectable)
|
|
647
428
|
|
|
648
|
-
|
|
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.
|
|
649
432
|
|
|
650
|
-
|
|
651
|
-
- Uses [Multer](https://github.com/expressjs/multer) for file handling
|
|
652
|
-
- AWS S3 support via [@aws-sdk/client-s3](https://www.npmjs.com/package/@aws-sdk/client-s3)
|
|
653
|
-
- Azure support via [@azure/storage-blob](https://www.npmjs.com/package/@azure/storage-blob)
|
|
433
|
+
### `FileStorageInterceptor(field, options?)`
|
|
654
434
|
|
|
655
|
-
|
|
435
|
+
`field`: a string (single file) or `{ type, fieldName?, maxCount?, fields? }`.
|
|
436
|
+
`options`: `{ driver?, fileName?, fileDist?, prefix?, validation?, mapToRequestBody?, overwriteBodyField?, afterUpload?, tenant? }`.
|
|
656
437
|
|
|
657
|
-
|
|
658
|
-
|
|
659
|
-
|
|
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`).
|
|
660
460
|
|
|
661
461
|
---
|
|
662
462
|
|
|
663
|
-
|
|
463
|
+
## More
|
|
464
|
+
|
|
465
|
+
- **[Migration from v1](./MIGRATION.md)** · **[Changelog](./CHANGELOG.md)** · **[Examples](./examples/)**
|
|
466
|
+
|
|
467
|
+
## License
|
|
468
|
+
|
|
469
|
+
MIT
|