@flusys/nestjs-storage 4.0.1 → 4.1.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/README.md
CHANGED
|
@@ -1,176 +1,106 @@
|
|
|
1
|
-
#
|
|
1
|
+
# @flusys/nestjs-storage
|
|
2
2
|
|
|
3
|
-
>
|
|
4
|
-
> **Version:** 4.0.1
|
|
5
|
-
> **Type:** File storage system with pluggable providers and multi-tenant support
|
|
3
|
+
> Multi-provider file storage for NestJS — Local disk, AWS S3, Azure Blob, and SFTP with folder management, image compression, presigned URL generation, and multi-tenant support.
|
|
6
4
|
|
|
7
|
-
|
|
5
|
+
[](https://www.npmjs.com/package/@flusys/nestjs-storage)
|
|
6
|
+
[](https://opensource.org/licenses/MIT)
|
|
7
|
+
[](https://nestjs.com/)
|
|
8
|
+
[](https://www.typescriptlang.org/)
|
|
9
|
+
[](https://nodejs.org/)
|
|
10
|
+
|
|
11
|
+
---
|
|
8
12
|
|
|
9
13
|
## Table of Contents
|
|
10
14
|
|
|
11
15
|
- [Overview](#overview)
|
|
16
|
+
- [Features](#features)
|
|
17
|
+
- [Compatibility](#compatibility)
|
|
12
18
|
- [Installation](#installation)
|
|
13
|
-
- [
|
|
14
|
-
- [
|
|
15
|
-
- [
|
|
16
|
-
- [
|
|
19
|
+
- [Quick Start](#quick-start)
|
|
20
|
+
- [Module Registration](#module-registration)
|
|
21
|
+
- [forRoot (Sync)](#forroot-sync)
|
|
22
|
+
- [forRootAsync (Factory)](#forrootasync-factory)
|
|
23
|
+
- [Configuration Reference](#configuration-reference)
|
|
24
|
+
- [Feature Toggles](#feature-toggles)
|
|
17
25
|
- [Storage Providers](#storage-providers)
|
|
18
|
-
- [
|
|
19
|
-
- [
|
|
20
|
-
- [
|
|
21
|
-
- [
|
|
22
|
-
- [
|
|
26
|
+
- [Local](#local-provider)
|
|
27
|
+
- [AWS S3](#aws-s3)
|
|
28
|
+
- [Azure Blob Storage](#azure-blob-storage)
|
|
29
|
+
- [SFTP](#sftp)
|
|
30
|
+
- [Custom Provider](#custom-provider)
|
|
31
|
+
- [API Endpoints](#api-endpoints)
|
|
32
|
+
- [Entities](#entities)
|
|
33
|
+
- [File Serving (Local)](#file-serving-local)
|
|
23
34
|
- [Image Compression](#image-compression)
|
|
24
|
-
- [
|
|
25
|
-
- [
|
|
26
|
-
- [
|
|
27
|
-
- [
|
|
28
|
-
- [
|
|
29
|
-
- [Best Practices](#best-practices)
|
|
30
|
-
- [API Reference](#api-reference)
|
|
35
|
+
- [File Validation](#file-validation)
|
|
36
|
+
- [Exported Services](#exported-services)
|
|
37
|
+
- [Programmatic Usage](#programmatic-usage)
|
|
38
|
+
- [Troubleshooting](#troubleshooting)
|
|
39
|
+
- [License](#license)
|
|
31
40
|
|
|
32
41
|
---
|
|
33
42
|
|
|
34
43
|
## Overview
|
|
35
44
|
|
|
36
|
-
`@flusys/nestjs-storage` provides a
|
|
45
|
+
`@flusys/nestjs-storage` provides a unified API for managing files across multiple storage backends. Provider configurations are stored in the database — switch providers without code changes. Cloud providers (S3, Azure, SFTP) are dynamically imported so you only pay the install cost for what you use.
|
|
37
46
|
|
|
38
|
-
|
|
39
|
-
- **Provider Connection Reuse** - Efficient connection management with caching
|
|
40
|
-
- **File Validation** - Size, type, and magic bytes validation
|
|
41
|
-
- **Image Compression** - Automatic optimization with format conversion
|
|
42
|
-
- **Folder Organization** - Simple folder-based file organization
|
|
43
|
-
- **Presigned URLs** - Secure time-limited access for cloud providers
|
|
44
|
-
- **Multi-Tenant Support** - Company/branch file isolation
|
|
45
|
-
- **Storage Configuration** - Per-company storage settings
|
|
47
|
+
---
|
|
46
48
|
|
|
47
|
-
|
|
49
|
+
## Features
|
|
48
50
|
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
51
|
+
- **4 storage backends** — Local disk, AWS S3, Azure Blob Storage, SFTP
|
|
52
|
+
- **Pluggable providers** — Register custom providers via `StorageProviderRegistry`
|
|
53
|
+
- **Folder hierarchy** — Organize files in folders with parent-child relationships
|
|
54
|
+
- **Image compression** — Automatic optimization via `sharp` (JPEG, PNG, WebP, AVIF, TIFF, GIF)
|
|
55
|
+
- **Presigned URLs** — Time-limited signed URLs for S3 and Azure with TTL caching
|
|
56
|
+
- **File validation** — Magic-byte validation prevents file-type spoofing
|
|
57
|
+
- **Path traversal protection** — All file paths sanitized before disk access
|
|
58
|
+
- **Company scoping** — Optional `companyId` filtering on all queries
|
|
59
|
+
- **Multi-tenant** — Per-tenant DataSource isolation
|
|
58
60
|
|
|
59
61
|
---
|
|
60
62
|
|
|
61
|
-
##
|
|
62
|
-
|
|
63
|
-
```bash
|
|
64
|
-
npm install @flusys/nestjs-storage @flusys/nestjs-shared @flusys/nestjs-core
|
|
65
|
-
|
|
66
|
-
# Required dependencies
|
|
67
|
-
npm install sharp mime-types uuid
|
|
63
|
+
## Compatibility
|
|
68
64
|
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
65
|
+
| Package | Version |
|
|
66
|
+
|---------|---------|
|
|
67
|
+
| `@flusys/nestjs-core` | `^4.0.0` |
|
|
68
|
+
| `@flusys/nestjs-shared` | `^4.0.0` |
|
|
69
|
+
| `sharp` | `^0.33.0` |
|
|
70
|
+
| `uuid` | `^9.0.0` |
|
|
71
|
+
| `mime-types` | `^2.0.0` |
|
|
72
|
+
| `@aws-sdk/client-s3` | `^3.0.0` *(optional)* |
|
|
73
|
+
| `@azure/storage-blob` | `^12.0.0` *(optional)* |
|
|
74
|
+
| `ssh2-sftp-client` | `^9.0.0` *(optional)* |
|
|
75
|
+
| Node.js | `>= 18.x` |
|
|
74
76
|
|
|
75
77
|
---
|
|
76
78
|
|
|
77
|
-
##
|
|
78
|
-
|
|
79
|
-
```typescript
|
|
80
|
-
// Injection Token
|
|
81
|
-
export const STORAGE_MODULE_OPTIONS = 'STORAGE_MODULE_OPTIONS';
|
|
79
|
+
## Installation
|
|
82
80
|
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
export const DEFAULT_ALLOWED_FILE_TYPES = ['*/*']; // All file types
|
|
81
|
+
```bash
|
|
82
|
+
npm install @flusys/nestjs-storage @flusys/nestjs-shared @flusys/nestjs-core sharp uuid mime-types
|
|
86
83
|
```
|
|
87
84
|
|
|
88
|
-
|
|
85
|
+
Optional cloud provider SDKs (install only what you need):
|
|
89
86
|
|
|
90
|
-
|
|
87
|
+
```bash
|
|
88
|
+
# AWS S3
|
|
89
|
+
npm install @aws-sdk/client-s3 @aws-sdk/lib-storage @aws-sdk/s3-request-presigner
|
|
91
90
|
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
│ ├── config/
|
|
99
|
-
│ │ ├── storage.constants.ts # Module constants
|
|
100
|
-
│ │ └── index.ts
|
|
101
|
-
│ │
|
|
102
|
-
│ ├── providers/
|
|
103
|
-
│ │ ├── local-provider.ts # Local filesystem (built-in)
|
|
104
|
-
│ │ ├── s3-provider.optional.ts # AWS S3 (requires @aws-sdk/client-s3)
|
|
105
|
-
│ │ ├── azure-provider.optional.ts # Azure Blob (requires @azure/storage-blob)
|
|
106
|
-
│ │ ├── sftp-provider.optional.ts # SFTP (requires ssh2-sftp-client)
|
|
107
|
-
│ │ ├── storage-factory.service.ts # Provider factory with caching
|
|
108
|
-
│ │ ├── storage-provider.registry.ts # Provider class registry
|
|
109
|
-
│ │ └── index.ts
|
|
110
|
-
│ │
|
|
111
|
-
│ ├── services/
|
|
112
|
-
│ │ ├── upload.service.ts # Upload/delete operations
|
|
113
|
-
│ │ ├── file-manager.service.ts # File metadata CRUD
|
|
114
|
-
│ │ ├── folder.service.ts # Folder CRUD
|
|
115
|
-
│ │ ├── storage-config.service.ts # Module configuration
|
|
116
|
-
│ │ ├── storage-provider-config.service.ts # Storage config CRUD
|
|
117
|
-
│ │ ├── storage-datasource.provider.ts # Dynamic entity loading
|
|
118
|
-
│ │ └── index.ts
|
|
119
|
-
│ │
|
|
120
|
-
│ ├── controllers/
|
|
121
|
-
│ │ ├── upload.controller.ts # /storage/upload/*
|
|
122
|
-
│ │ ├── file-manager.controller.ts # /storage/file-manager/*
|
|
123
|
-
│ │ ├── folder.controller.ts # /storage/folder/*
|
|
124
|
-
│ │ ├── storage-config.controller.ts # /storage/storage-config/*
|
|
125
|
-
│ │ └── index.ts
|
|
126
|
-
│ │
|
|
127
|
-
│ ├── entities/
|
|
128
|
-
│ │ ├── file-manager.entity.ts # FileManager base
|
|
129
|
-
│ │ ├── file-manager-with-company.entity.ts
|
|
130
|
-
│ │ ├── folder.entity.ts # Folder base
|
|
131
|
-
│ │ ├── folder-with-company.entity.ts
|
|
132
|
-
│ │ ├── storage-config.entity.ts # StorageConfig base
|
|
133
|
-
│ │ ├── storage-config-with-company.entity.ts
|
|
134
|
-
│ │ └── index.ts
|
|
135
|
-
│ │
|
|
136
|
-
│ ├── dtos/
|
|
137
|
-
│ │ ├── upload.dto.ts # Upload options, delete DTOs
|
|
138
|
-
│ │ ├── file-manager.dto.ts # File manager DTOs
|
|
139
|
-
│ │ ├── folder.dto.ts # Folder DTOs
|
|
140
|
-
│ │ ├── storage-config.dto.ts # Storage config DTOs
|
|
141
|
-
│ │ └── index.ts
|
|
142
|
-
│ │
|
|
143
|
-
│ ├── interfaces/
|
|
144
|
-
│ │ ├── file-manager.interface.ts
|
|
145
|
-
│ │ ├── folder.interface.ts
|
|
146
|
-
│ │ ├── storage-config.interface.ts
|
|
147
|
-
│ │ ├── storage-module-options.interface.ts
|
|
148
|
-
│ │ ├── storage-provider.interface.ts
|
|
149
|
-
│ │ └── index.ts
|
|
150
|
-
│ │
|
|
151
|
-
│ ├── enums/
|
|
152
|
-
│ │ ├── file-location.enum.ts # Provider type enum
|
|
153
|
-
│ │ └── index.ts
|
|
154
|
-
│ │
|
|
155
|
-
│ ├── middlewares/
|
|
156
|
-
│ │ ├── file-serve.middleware.ts # File serving with fallback strategies
|
|
157
|
-
│ │ └── index.ts
|
|
158
|
-
│ │
|
|
159
|
-
│ ├── docs/
|
|
160
|
-
│ │ ├── storage-swagger.config.ts # Swagger configuration
|
|
161
|
-
│ │ └── index.ts
|
|
162
|
-
│ │
|
|
163
|
-
│ └── utils/
|
|
164
|
-
│ ├── file-validator.util.ts # Magic bytes validation
|
|
165
|
-
│ ├── image-compressor.util.ts # Sharp-based compression
|
|
166
|
-
│ └── index.ts
|
|
91
|
+
# Azure Blob Storage
|
|
92
|
+
npm install @azure/storage-blob
|
|
93
|
+
|
|
94
|
+
# SFTP
|
|
95
|
+
npm install ssh2-sftp-client
|
|
96
|
+
npm install -D @types/ssh2-sftp-client
|
|
167
97
|
```
|
|
168
98
|
|
|
169
99
|
---
|
|
170
100
|
|
|
171
|
-
##
|
|
101
|
+
## Quick Start
|
|
172
102
|
|
|
173
|
-
###
|
|
103
|
+
### Local Storage (Minimal Setup)
|
|
174
104
|
|
|
175
105
|
```typescript
|
|
176
106
|
import { Module } from '@nestjs/common';
|
|
@@ -184,21 +114,20 @@ import { StorageModule } from '@flusys/nestjs-storage';
|
|
|
184
114
|
bootstrapAppConfig: {
|
|
185
115
|
databaseMode: 'single',
|
|
186
116
|
enableCompanyFeature: false,
|
|
187
|
-
permissionMode: 'RBAC',
|
|
188
117
|
},
|
|
189
118
|
config: {
|
|
190
119
|
defaultDatabaseConfig: {
|
|
191
120
|
type: 'postgres',
|
|
192
|
-
host:
|
|
193
|
-
port: 5432,
|
|
194
|
-
username:
|
|
195
|
-
password:
|
|
196
|
-
database:
|
|
121
|
+
host: process.env.DB_HOST,
|
|
122
|
+
port: Number(process.env.DB_PORT ?? 5432),
|
|
123
|
+
username: process.env.DB_USER,
|
|
124
|
+
password: process.env.DB_PASSWORD,
|
|
125
|
+
database: process.env.DB_NAME,
|
|
197
126
|
},
|
|
198
|
-
maxFileSize: 10 * 1024 * 1024,
|
|
199
|
-
allowedFileTypes: ['image/*', 'application/pdf'
|
|
127
|
+
maxFileSize: 10 * 1024 * 1024, // 10 MB
|
|
128
|
+
allowedFileTypes: ['image/*', 'application/pdf'],
|
|
200
129
|
localStoragePath: './uploads',
|
|
201
|
-
appUrl: process.env.APP_URL
|
|
130
|
+
appUrl: process.env.APP_URL ?? 'http://localhost:2002',
|
|
202
131
|
},
|
|
203
132
|
}),
|
|
204
133
|
],
|
|
@@ -206,1312 +135,443 @@ import { StorageModule } from '@flusys/nestjs-storage';
|
|
|
206
135
|
export class AppModule {}
|
|
207
136
|
```
|
|
208
137
|
|
|
209
|
-
###
|
|
138
|
+
### Register Local File Serving Middleware
|
|
139
|
+
|
|
140
|
+
```typescript
|
|
141
|
+
import { FileServeMiddleware } from '@flusys/nestjs-storage';
|
|
142
|
+
|
|
143
|
+
@Module({})
|
|
144
|
+
export class AppModule implements NestModule {
|
|
145
|
+
configure(consumer: MiddlewareConsumer) {
|
|
146
|
+
// Serves GET /storage/upload/file/* from local disk
|
|
147
|
+
consumer.apply(FileServeMiddleware).forRoutes('storage/upload/file');
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
```
|
|
151
|
+
|
|
152
|
+
---
|
|
153
|
+
|
|
154
|
+
## Module Registration
|
|
155
|
+
|
|
156
|
+
### forRoot (Sync)
|
|
210
157
|
|
|
211
158
|
```typescript
|
|
212
159
|
StorageModule.forRoot({
|
|
213
|
-
global
|
|
214
|
-
includeController
|
|
160
|
+
global?: boolean;
|
|
161
|
+
includeController?: boolean;
|
|
215
162
|
bootstrapAppConfig: {
|
|
216
|
-
databaseMode: 'single'
|
|
217
|
-
enableCompanyFeature:
|
|
218
|
-
|
|
219
|
-
},
|
|
163
|
+
databaseMode: 'single' | 'multi-tenant';
|
|
164
|
+
enableCompanyFeature: boolean;
|
|
165
|
+
};
|
|
220
166
|
config: {
|
|
221
|
-
defaultDatabaseConfig:
|
|
222
|
-
maxFileSize
|
|
223
|
-
allowedFileTypes
|
|
224
|
-
localStoragePath: './uploads'
|
|
225
|
-
appUrl
|
|
226
|
-
}
|
|
227
|
-
})
|
|
167
|
+
defaultDatabaseConfig: IDatabaseConfig;
|
|
168
|
+
maxFileSize?: number; // bytes, default: 100MB
|
|
169
|
+
allowedFileTypes?: string[]; // MIME types/patterns, default: ['*/*']
|
|
170
|
+
localStoragePath?: string; // default: './uploads'
|
|
171
|
+
appUrl?: string; // Base URL for local file links
|
|
172
|
+
};
|
|
173
|
+
})
|
|
228
174
|
```
|
|
229
175
|
|
|
230
|
-
###
|
|
176
|
+
### forRootAsync (Factory)
|
|
231
177
|
|
|
232
178
|
```typescript
|
|
179
|
+
import { ConfigService } from '@nestjs/config';
|
|
180
|
+
|
|
233
181
|
StorageModule.forRootAsync({
|
|
234
182
|
global: true,
|
|
235
183
|
includeController: true,
|
|
236
184
|
bootstrapAppConfig: {
|
|
237
185
|
databaseMode: 'single',
|
|
238
186
|
enableCompanyFeature: true,
|
|
239
|
-
permissionMode: 'FULL',
|
|
240
187
|
},
|
|
241
188
|
imports: [ConfigModule],
|
|
242
|
-
useFactory:
|
|
243
|
-
defaultDatabaseConfig:
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
189
|
+
useFactory: (configService: ConfigService) => ({
|
|
190
|
+
defaultDatabaseConfig: {
|
|
191
|
+
type: 'postgres',
|
|
192
|
+
host: configService.get('DB_HOST'),
|
|
193
|
+
port: configService.get<number>('DB_PORT'),
|
|
194
|
+
username: configService.get('DB_USER'),
|
|
195
|
+
password: configService.get('DB_PASSWORD'),
|
|
196
|
+
database: configService.get('DB_NAME'),
|
|
197
|
+
},
|
|
198
|
+
maxFileSize: configService.get<number>('MAX_FILE_SIZE', 10 * 1024 * 1024),
|
|
247
199
|
appUrl: configService.get('APP_URL'),
|
|
200
|
+
localStoragePath: configService.get('UPLOAD_PATH', './uploads'),
|
|
248
201
|
}),
|
|
249
202
|
inject: [ConfigService],
|
|
250
|
-
})
|
|
203
|
+
})
|
|
251
204
|
```
|
|
252
205
|
|
|
253
|
-
|
|
206
|
+
---
|
|
207
|
+
|
|
208
|
+
## Configuration Reference
|
|
254
209
|
|
|
255
210
|
```typescript
|
|
256
211
|
interface IStorageModuleConfig extends IDataSourceServiceOptions {
|
|
257
212
|
/** Maximum file size in bytes (default: 100MB) */
|
|
258
213
|
maxFileSize?: number;
|
|
259
|
-
|
|
214
|
+
|
|
215
|
+
/** Allowed MIME types. Use '*/*' for all. (default: ['*/*']) */
|
|
260
216
|
allowedFileTypes?: string[];
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
/** Local storage path (default: './uploads') */
|
|
217
|
+
|
|
218
|
+
/** Local storage base directory (default: './uploads') */
|
|
264
219
|
localStoragePath?: string;
|
|
265
|
-
/** Application base URL for file URLs */
|
|
266
|
-
appUrl?: string;
|
|
267
|
-
}
|
|
268
220
|
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
config?: IStorageModuleConfig;
|
|
221
|
+
/** Application base URL for building file URLs (local provider) */
|
|
222
|
+
appUrl?: string;
|
|
272
223
|
}
|
|
273
224
|
```
|
|
274
225
|
|
|
275
|
-
|
|
226
|
+
---
|
|
276
227
|
|
|
277
|
-
|
|
228
|
+
## Feature Toggles
|
|
278
229
|
|
|
279
|
-
|
|
|
280
|
-
|
|
281
|
-
| `
|
|
282
|
-
|
|
|
283
|
-
|
|
|
230
|
+
| Feature | Config | Default | Effect |
|
|
231
|
+
|---------|--------|---------|--------|
|
|
232
|
+
| Company scoping | `enableCompanyFeature: true` | `false` | Uses `*WithCompany` entity variants; filters all queries by `companyId` |
|
|
233
|
+
| Local file serving | Built-in | Always on | Register `FileServeMiddleware` to serve uploaded files from disk |
|
|
234
|
+
| Cloud providers | Install SDK + register | Disabled | S3/Azure/SFTP only activate after provider registration |
|
|
284
235
|
|
|
285
236
|
---
|
|
286
237
|
|
|
287
|
-
##
|
|
238
|
+
## Storage Providers
|
|
288
239
|
|
|
289
|
-
###
|
|
240
|
+
### Local Provider
|
|
290
241
|
|
|
291
|
-
|
|
292
|
-
export enum FileLocationEnum {
|
|
293
|
-
AWS = 'aws',
|
|
294
|
-
AZURE = 'azure',
|
|
295
|
-
SFTP = 'sftp',
|
|
296
|
-
LOCAL = 'local',
|
|
297
|
-
}
|
|
298
|
-
```
|
|
242
|
+
Built-in, no extra SDK required. Files stored on the server filesystem.
|
|
299
243
|
|
|
300
|
-
|
|
244
|
+
Create a `StorageConfig` record:
|
|
301
245
|
|
|
302
|
-
```
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
StorageConfigWithCompany,
|
|
311
|
-
];
|
|
312
|
-
|
|
313
|
-
// Helper function
|
|
314
|
-
export function getStorageEntitiesByConfig(enableCompanyFeature: boolean): any[] {
|
|
315
|
-
return enableCompanyFeature ? StorageCompanyEntities : StorageCoreEntities;
|
|
246
|
+
```json
|
|
247
|
+
POST /storage/storage-config/insert
|
|
248
|
+
{
|
|
249
|
+
"name": "Local Storage",
|
|
250
|
+
"storage": "local",
|
|
251
|
+
"config": {},
|
|
252
|
+
"isActive": true,
|
|
253
|
+
"isDefault": true
|
|
316
254
|
}
|
|
317
|
-
|
|
318
|
-
// Base type aliases
|
|
319
|
-
export { FileManager as FileManagerBase } from './file-manager.entity';
|
|
320
|
-
export { Folder as FolderBase } from './folder.entity';
|
|
321
|
-
export { StorageConfig as StorageConfigBase } from './storage-config.entity';
|
|
322
255
|
```
|
|
323
256
|
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
```typescript
|
|
327
|
-
@Entity({ name: 'file_manager' })
|
|
328
|
-
export class FileManager extends Identity {
|
|
329
|
-
@Column({ type: 'varchar', length: 255 })
|
|
330
|
-
name!: string; // Original filename
|
|
257
|
+
File URLs format: `{appUrl}/storage/upload/file/{path}`
|
|
331
258
|
|
|
332
|
-
|
|
333
|
-
contentType!: string; // MIME type
|
|
259
|
+
### AWS S3
|
|
334
260
|
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
@Column({ type: 'text' })
|
|
339
|
-
key!: string; // Storage key/path
|
|
340
|
-
|
|
341
|
-
@Column({ type: 'text', nullable: true })
|
|
342
|
-
url!: string | null; // Public/presigned URL
|
|
343
|
-
|
|
344
|
-
@Column({ type: 'varchar', length: 50 })
|
|
345
|
-
location!: string; // Provider type (local, aws, azure, sftp)
|
|
346
|
-
|
|
347
|
-
@Column({ type: 'uuid', nullable: true })
|
|
348
|
-
storageConfigId!: string | null;
|
|
349
|
-
|
|
350
|
-
@Column({ type: 'bigint', nullable: true })
|
|
351
|
-
expiresAt: number | null = null; // URL expiration timestamp
|
|
352
|
-
|
|
353
|
-
@Column({ type: 'boolean', default: false })
|
|
354
|
-
isPrivate!: boolean;
|
|
355
|
-
|
|
356
|
-
@ManyToOne('Folder', (folder: any) => folder.fileManager, { nullable: true, onDelete: 'SET NULL' })
|
|
357
|
-
@JoinColumn({ name: 'folder_id' })
|
|
358
|
-
folder!: Folder | null;
|
|
359
|
-
}
|
|
360
|
-
|
|
361
|
-
// With company feature
|
|
362
|
-
@Entity({ name: 'file_manager' })
|
|
363
|
-
export class FileManagerWithCompany extends FileManager {
|
|
364
|
-
@Column({ type: 'uuid', nullable: true })
|
|
365
|
-
companyId!: string | null;
|
|
366
|
-
}
|
|
261
|
+
**1. Install the SDK:**
|
|
262
|
+
```bash
|
|
263
|
+
npm install @aws-sdk/client-s3 @aws-sdk/lib-storage @aws-sdk/s3-request-presigner
|
|
367
264
|
```
|
|
368
265
|
|
|
369
|
-
|
|
370
|
-
|
|
266
|
+
**2. Register the provider at startup (before module init):**
|
|
371
267
|
```typescript
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
@Column({ type: 'varchar', length: 255 })
|
|
375
|
-
name!: string;
|
|
376
|
-
|
|
377
|
-
@Column({ type: 'varchar', length: 255 })
|
|
378
|
-
slug!: string;
|
|
379
|
-
|
|
380
|
-
@OneToMany('FileManager', (fileManager: any) => fileManager.folder)
|
|
381
|
-
fileManager!: any[];
|
|
382
|
-
}
|
|
268
|
+
import { StorageProviderRegistry, FileLocationEnum } from '@flusys/nestjs-storage';
|
|
269
|
+
import { S3Provider } from '@flusys/nestjs-storage/providers/s3-provider.optional';
|
|
383
270
|
|
|
384
|
-
|
|
385
|
-
@Entity({ name: 'folder' })
|
|
386
|
-
export class FolderWithCompany extends Folder {
|
|
387
|
-
@Column({ type: 'uuid', nullable: true })
|
|
388
|
-
companyId!: string | null;
|
|
389
|
-
}
|
|
271
|
+
StorageProviderRegistry.register(FileLocationEnum.AWS, S3Provider);
|
|
390
272
|
```
|
|
391
273
|
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
storage!: string; // Provider type (local, aws, azure, sftp)
|
|
406
|
-
|
|
407
|
-
@Column({ type: 'json' })
|
|
408
|
-
config!: Record<string, any>; // Provider-specific config
|
|
409
|
-
|
|
410
|
-
@Column({ type: 'boolean', default: true, name: 'is_active' })
|
|
411
|
-
isActive!: boolean;
|
|
412
|
-
|
|
413
|
-
@Column({ type: 'boolean', default: false, name: 'is_default' })
|
|
414
|
-
isDefault!: boolean;
|
|
415
|
-
}
|
|
416
|
-
|
|
417
|
-
// With company feature
|
|
418
|
-
@Entity({ name: 'storage_config' })
|
|
419
|
-
export class StorageConfigWithCompany extends StorageConfig {
|
|
420
|
-
@Column({ type: 'uuid', nullable: true })
|
|
421
|
-
companyId!: string | null;
|
|
274
|
+
**3. Create a StorageConfig:**
|
|
275
|
+
```json
|
|
276
|
+
POST /storage/storage-config/insert
|
|
277
|
+
{
|
|
278
|
+
"name": "Production S3",
|
|
279
|
+
"storage": "aws",
|
|
280
|
+
"config": {
|
|
281
|
+
"region": "us-east-1",
|
|
282
|
+
"bucket": "my-app-files",
|
|
283
|
+
"accessKeyId": "AKIAIOSFODNN7EXAMPLE",
|
|
284
|
+
"secretAccessKey": "wJalrXUtnFEMI/K7MDENG/bPxRfiCYEXAMPLEKEY"
|
|
285
|
+
},
|
|
286
|
+
"isDefault": true
|
|
422
287
|
}
|
|
423
288
|
```
|
|
424
289
|
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
## Storage Providers
|
|
428
|
-
|
|
429
|
-
### Provider Interface
|
|
430
|
-
|
|
431
|
-
```typescript
|
|
432
|
-
interface IStorageProvider {
|
|
433
|
-
uploadFile(file: Express.Multer.File, options: UploadOptionsDto): Promise<IUploadedFileInfo>;
|
|
434
|
-
uploadMultipleFiles(files: Express.Multer.File[], options: UploadOptionsDto): Promise<IUploadedFileInfo[]>;
|
|
435
|
-
deleteFile(key: string): Promise<void>;
|
|
436
|
-
deleteMultipleFiles(keys: string[]): Promise<void>;
|
|
437
|
-
generatePresignedUrl(key: string, expiresInSeconds?: number): Promise<string>;
|
|
438
|
-
healthCheck(): Promise<boolean>;
|
|
439
|
-
initialize?(config: any): Promise<void>;
|
|
440
|
-
}
|
|
290
|
+
### Azure Blob Storage
|
|
441
291
|
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
size: number;
|
|
446
|
-
name: string;
|
|
447
|
-
location?: string;
|
|
448
|
-
storageConfigId?: string;
|
|
449
|
-
}
|
|
292
|
+
**1. Install the SDK:**
|
|
293
|
+
```bash
|
|
294
|
+
npm install @azure/storage-blob
|
|
450
295
|
```
|
|
451
296
|
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
Providers are automatically registered when the module loads:
|
|
455
|
-
|
|
297
|
+
**2. Register:**
|
|
456
298
|
```typescript
|
|
457
|
-
|
|
458
|
-
StorageProviderRegistry.register(FileLocationEnum.
|
|
459
|
-
|
|
460
|
-
// Optional providers loaded dynamically
|
|
461
|
-
const OPTIONAL_PROVIDERS = [
|
|
462
|
-
{ location: FileLocationEnum.AWS, path: '../providers/s3-provider.optional', name: 'S3Provider', dep: '@aws-sdk/client-s3' },
|
|
463
|
-
{ location: FileLocationEnum.AZURE, path: '../providers/azure-provider.optional', name: 'AzureProvider', dep: '@azure/storage-blob' },
|
|
464
|
-
{ location: FileLocationEnum.SFTP, path: '../providers/sftp-provider.optional', name: 'SftpProvider', dep: 'ssh2-sftp-client' },
|
|
465
|
-
];
|
|
466
|
-
|
|
467
|
-
for (const { location, path, name, dep } of OPTIONAL_PROVIDERS) {
|
|
468
|
-
try {
|
|
469
|
-
StorageProviderRegistry.register(location, require(path)[name]);
|
|
470
|
-
logger.log(`Registered ${name}`);
|
|
471
|
-
} catch {
|
|
472
|
-
logger.debug(`${name} not available (install ${dep} to enable)`);
|
|
473
|
-
}
|
|
474
|
-
}
|
|
299
|
+
import { AzureProvider } from '@flusys/nestjs-storage/providers/azure-provider.optional';
|
|
300
|
+
StorageProviderRegistry.register(FileLocationEnum.AZURE, AzureProvider);
|
|
475
301
|
```
|
|
476
302
|
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
// Built-in, no external dependencies required
|
|
481
|
-
// Uses Node.js fs module
|
|
482
|
-
|
|
483
|
-
// Configuration:
|
|
303
|
+
**3. Create a StorageConfig:**
|
|
304
|
+
```json
|
|
305
|
+
POST /storage/storage-config/insert
|
|
484
306
|
{
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
307
|
+
"name": "Azure Blob",
|
|
308
|
+
"storage": "azure",
|
|
309
|
+
"config": {
|
|
310
|
+
"connectionString": "DefaultEndpointsProtocol=https;AccountName=...;AccountKey=...;EndpointSuffix=core.windows.net",
|
|
311
|
+
"containerName": "my-files"
|
|
312
|
+
},
|
|
313
|
+
"isDefault": true
|
|
490
314
|
}
|
|
491
315
|
```
|
|
492
316
|
|
|
493
|
-
|
|
494
|
-
- Path traversal attack prevention
|
|
495
|
-
- Automatic directory creation
|
|
496
|
-
- UUID-prefixed filenames
|
|
497
|
-
- Image compression support
|
|
498
|
-
|
|
499
|
-
### AWS S3 Provider
|
|
500
|
-
|
|
501
|
-
```typescript
|
|
502
|
-
// Requires: npm install @aws-sdk/client-s3 @aws-sdk/lib-storage @aws-sdk/s3-request-presigner
|
|
317
|
+
### SFTP
|
|
503
318
|
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
config: {
|
|
508
|
-
region: 'us-east-1',
|
|
509
|
-
bucket: 'my-bucket',
|
|
510
|
-
accessKeyId: 'AKIA...',
|
|
511
|
-
secretAccessKey: 'secret...',
|
|
512
|
-
endpoint: 'https://s3.us-east-1.amazonaws.com' // Optional
|
|
513
|
-
}
|
|
514
|
-
}
|
|
319
|
+
**1. Install the SDK:**
|
|
320
|
+
```bash
|
|
321
|
+
npm install ssh2-sftp-client
|
|
515
322
|
```
|
|
516
323
|
|
|
517
|
-
|
|
518
|
-
|
|
324
|
+
**2. Register:**
|
|
519
325
|
```typescript
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
// Configuration:
|
|
523
|
-
{
|
|
524
|
-
storage: 'azure',
|
|
525
|
-
config: {
|
|
526
|
-
accountName: 'myaccount',
|
|
527
|
-
accountKey: 'key...',
|
|
528
|
-
containerName: 'my-container',
|
|
529
|
-
// Or use connection string
|
|
530
|
-
connectionString: 'DefaultEndpointsProtocol=https;...'
|
|
531
|
-
}
|
|
532
|
-
}
|
|
326
|
+
import { SFTPProvider } from '@flusys/nestjs-storage/providers/sftp-provider.optional';
|
|
327
|
+
StorageProviderRegistry.register(FileLocationEnum.SFTP, SFTPProvider);
|
|
533
328
|
```
|
|
534
329
|
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
// Requires: npm install ssh2-sftp-client
|
|
539
|
-
|
|
540
|
-
// Configuration:
|
|
330
|
+
**3. Create a StorageConfig:**
|
|
331
|
+
```json
|
|
332
|
+
POST /storage/storage-config/insert
|
|
541
333
|
{
|
|
542
|
-
|
|
543
|
-
|
|
544
|
-
|
|
545
|
-
|
|
546
|
-
|
|
547
|
-
|
|
548
|
-
|
|
549
|
-
|
|
550
|
-
basePath: '/uploads'
|
|
334
|
+
"name": "Legacy SFTP",
|
|
335
|
+
"storage": "sftp",
|
|
336
|
+
"config": {
|
|
337
|
+
"host": "sftp.example.com",
|
|
338
|
+
"port": 22,
|
|
339
|
+
"username": "sftpuser",
|
|
340
|
+
"password": "secret",
|
|
341
|
+
"remotePath": "/uploads"
|
|
551
342
|
}
|
|
552
343
|
}
|
|
553
344
|
```
|
|
554
345
|
|
|
555
|
-
### Provider
|
|
346
|
+
### Custom Provider
|
|
556
347
|
|
|
557
348
|
```typescript
|
|
558
|
-
@
|
|
559
|
-
export class StorageFactoryService implements OnModuleDestroy {
|
|
560
|
-
private readonly cache = new Map<string, IStorageProvider>();
|
|
561
|
-
|
|
562
|
-
async createProvider(config: IStorageProviderConfig): Promise<IStorageProvider> {
|
|
563
|
-
const cacheKey = this.generateCacheKey(config);
|
|
564
|
-
|
|
565
|
-
const cached = this.cache.get(cacheKey);
|
|
566
|
-
if (cached) return cached;
|
|
567
|
-
|
|
568
|
-
const ProviderClass = StorageProviderRegistry.get(config.provider);
|
|
569
|
-
if (!ProviderClass) {
|
|
570
|
-
throw new NotFoundException(`Storage provider '${config.provider}' not registered`);
|
|
571
|
-
}
|
|
572
|
-
|
|
573
|
-
const instance = new ProviderClass();
|
|
574
|
-
await this.initializeProvider(instance, config);
|
|
575
|
-
this.cache.set(cacheKey, instance);
|
|
576
|
-
return instance;
|
|
577
|
-
}
|
|
578
|
-
|
|
579
|
-
// Generate cache key using SHA256 hash of config
|
|
580
|
-
private generateCacheKey(config: IStorageProviderConfig): string {
|
|
581
|
-
const hash = crypto.createHash('sha256')
|
|
582
|
-
.update(JSON.stringify(config.config, Object.keys(config.config || {}).sort()))
|
|
583
|
-
.digest('hex').substring(0, 16);
|
|
584
|
-
return `${config.provider}-${hash}`;
|
|
585
|
-
}
|
|
349
|
+
import { IStorageProvider, StorageProviderRegistry } from '@flusys/nestjs-storage';
|
|
586
350
|
|
|
587
|
-
|
|
588
|
-
async
|
|
589
|
-
|
|
590
|
-
|
|
591
|
-
await (provider as any).close();
|
|
592
|
-
}
|
|
593
|
-
}
|
|
594
|
-
this.cache.clear();
|
|
595
|
-
}
|
|
351
|
+
class MyCloudProvider implements IStorageProvider {
|
|
352
|
+
async upload(file: IFileUploadOptions): Promise<IUploadedFile> { /* ... */ }
|
|
353
|
+
async delete(path: string): Promise<void> { /* ... */ }
|
|
354
|
+
async getUrl(path: string, ttl?: number): Promise<string> { /* ... */ }
|
|
596
355
|
}
|
|
597
|
-
```
|
|
598
|
-
|
|
599
|
-
---
|
|
600
|
-
|
|
601
|
-
## Storage Configuration
|
|
602
|
-
|
|
603
|
-
### StorageProviderConfigService
|
|
604
|
-
|
|
605
|
-
```typescript
|
|
606
|
-
@Injectable({ scope: Scope.REQUEST })
|
|
607
|
-
export class StorageProviderConfigService extends RequestScopedApiService<...> {
|
|
608
|
-
|
|
609
|
-
/** Direct lookup by ID (bypasses company filtering) */
|
|
610
|
-
async findByIdDirect(id: string): Promise<StorageConfigBase | null>;
|
|
611
|
-
|
|
612
|
-
/** Get default config for user's company */
|
|
613
|
-
async getDefaultConfig(user?: ILoggedUserInfo): Promise<StorageConfigBase | null>;
|
|
614
356
|
|
|
615
|
-
|
|
616
|
-
async getConfigByType(storage: string, user?: ILoggedUserInfo): Promise<StorageConfigBase[]>;
|
|
617
|
-
}
|
|
618
|
-
```
|
|
619
|
-
|
|
620
|
-
### Default Configuration Resolution
|
|
621
|
-
|
|
622
|
-
When `storageConfigId` is not provided for uploads:
|
|
623
|
-
|
|
624
|
-
1. **Priority 1:** Find config with `isDefault: true` and `isActive: true`
|
|
625
|
-
2. **Priority 2:** Fall back to oldest active config
|
|
626
|
-
|
|
627
|
-
```typescript
|
|
628
|
-
async getDefaultConfig(user?: ILoggedUserInfo): Promise<StorageConfigBase | null> {
|
|
629
|
-
await this.ensureRepositoryInitialized();
|
|
630
|
-
|
|
631
|
-
const baseWhere = buildCompanyWhereCondition(
|
|
632
|
-
{ isActive: true },
|
|
633
|
-
this.storageConfig.isCompanyFeatureEnabled(),
|
|
634
|
-
user,
|
|
635
|
-
);
|
|
636
|
-
|
|
637
|
-
// First try to find config marked as default
|
|
638
|
-
const defaultConfig = await this.repository.findOne({
|
|
639
|
-
where: { ...baseWhere, isDefault: true },
|
|
640
|
-
order: { createdAt: 'ASC' },
|
|
641
|
-
});
|
|
642
|
-
|
|
643
|
-
if (defaultConfig) return defaultConfig;
|
|
644
|
-
|
|
645
|
-
// Fall back to oldest active config
|
|
646
|
-
return await this.repository.findOne({
|
|
647
|
-
where: baseWhere,
|
|
648
|
-
order: { createdAt: 'ASC' },
|
|
649
|
-
});
|
|
650
|
-
}
|
|
651
|
-
```
|
|
652
|
-
|
|
653
|
-
### Creating Storage Configurations
|
|
654
|
-
|
|
655
|
-
```typescript
|
|
656
|
-
import { StorageProviderConfigService } from '@flusys/nestjs-storage';
|
|
657
|
-
|
|
658
|
-
@Injectable()
|
|
659
|
-
export class SetupService {
|
|
660
|
-
constructor(private readonly storageConfigService: StorageProviderConfigService) {}
|
|
661
|
-
|
|
662
|
-
async setupStorageConfigs(user: ILoggedUserInfo) {
|
|
663
|
-
// Create default S3 config
|
|
664
|
-
await this.storageConfigService.insert({
|
|
665
|
-
name: 'default',
|
|
666
|
-
storage: 'aws',
|
|
667
|
-
config: {
|
|
668
|
-
region: 'us-east-1',
|
|
669
|
-
bucket: 'company-files',
|
|
670
|
-
accessKeyId: process.env.AWS_ACCESS_KEY,
|
|
671
|
-
secretAccessKey: process.env.AWS_SECRET_KEY,
|
|
672
|
-
},
|
|
673
|
-
isActive: true,
|
|
674
|
-
isDefault: true,
|
|
675
|
-
}, user);
|
|
676
|
-
|
|
677
|
-
// Create local backup config
|
|
678
|
-
await this.storageConfigService.insert({
|
|
679
|
-
name: 'local-backup',
|
|
680
|
-
storage: 'local',
|
|
681
|
-
config: {
|
|
682
|
-
basePath: './backups',
|
|
683
|
-
},
|
|
684
|
-
isActive: true,
|
|
685
|
-
isDefault: false,
|
|
686
|
-
}, user);
|
|
687
|
-
}
|
|
688
|
-
}
|
|
357
|
+
StorageProviderRegistry.register('my-cloud', MyCloudProvider);
|
|
689
358
|
```
|
|
690
359
|
|
|
691
360
|
---
|
|
692
361
|
|
|
693
|
-
##
|
|
694
|
-
|
|
695
|
-
### FileManagerService
|
|
696
|
-
|
|
697
|
-
```typescript
|
|
698
|
-
@Injectable({ scope: Scope.REQUEST })
|
|
699
|
-
export class FileManagerService extends RequestScopedApiService<...> {
|
|
700
|
-
|
|
701
|
-
/** Enrich files with provider names */
|
|
702
|
-
async enrichWithProviderNames<T extends { storageConfigId?: string | null }>(
|
|
703
|
-
items: T[]
|
|
704
|
-
): Promise<(T & { providerName?: string })[]>;
|
|
705
|
-
|
|
706
|
-
/** Get multiple files with refreshed URLs */
|
|
707
|
-
async getFiles(
|
|
708
|
-
dtos: GetFilesRequestDto[],
|
|
709
|
-
protocol: string,
|
|
710
|
-
host: string,
|
|
711
|
-
user?: ILoggedUserInfo
|
|
712
|
-
): Promise<FilesResponseDto[]>;
|
|
713
|
-
}
|
|
714
|
-
```
|
|
362
|
+
## API Endpoints
|
|
715
363
|
|
|
716
|
-
|
|
364
|
+
All endpoints use **POST** and require JWT authentication.
|
|
717
365
|
|
|
718
|
-
|
|
366
|
+
### Storage Config — `POST /storage/storage-config/*`
|
|
719
367
|
|
|
720
|
-
|
|
721
|
-
|
|
722
|
-
|
|
723
|
-
|
|
724
|
-
|
|
725
|
-
|
|
726
|
-
|
|
727
|
-
|
|
728
|
-
const updated = await this.refreshFileUrl(file, protocol, host, now, user);
|
|
729
|
-
if (updated) updatedFiles.push(file);
|
|
730
|
-
return this.toFileResponse(file);
|
|
731
|
-
}),
|
|
732
|
-
);
|
|
368
|
+
| Endpoint | Permission | Description |
|
|
369
|
+
|----------|-----------|-------------|
|
|
370
|
+
| `POST /storage/storage-config/insert` | `storage-config.create` | Add provider config |
|
|
371
|
+
| `POST /storage/storage-config/get-all` | `storage-config.read` | List configs |
|
|
372
|
+
| `POST /storage/storage-config/get/:id` | `storage-config.read` | Get config by ID |
|
|
373
|
+
| `POST /storage/storage-config/update` | `storage-config.update` | Update config |
|
|
374
|
+
| `POST /storage/storage-config/delete` | `storage-config.delete` | Delete config |
|
|
375
|
+
| `POST /storage/storage-config/set-default` | `storage-config.update` | Set as default provider |
|
|
733
376
|
|
|
734
|
-
|
|
735
|
-
await this.repository.save(updatedFiles);
|
|
736
|
-
}
|
|
377
|
+
### Folders — `POST /storage/folder/*`
|
|
737
378
|
|
|
738
|
-
|
|
739
|
-
|
|
379
|
+
| Endpoint | Permission | Description |
|
|
380
|
+
|----------|-----------|-------------|
|
|
381
|
+
| `POST /storage/folder/insert` | `folder.create` | Create folder |
|
|
382
|
+
| `POST /storage/folder/get-all` | `folder.read` | List folders |
|
|
383
|
+
| `POST /storage/folder/get/:id` | `folder.read` | Get folder by ID |
|
|
384
|
+
| `POST /storage/folder/get-tree` | `folder.read` | Get hierarchical folder tree |
|
|
385
|
+
| `POST /storage/folder/update` | `folder.update` | Update folder |
|
|
386
|
+
| `POST /storage/folder/delete` | `folder.delete` | Delete folder (must be empty) |
|
|
740
387
|
|
|
741
|
-
|
|
742
|
-
const isCloudProvider = file.location === 'aws' || file.location === 'azure';
|
|
743
|
-
const needsNewUrl = !file.url || (isCloudProvider && now >= file.expiresAt);
|
|
388
|
+
### Files — `POST /storage/file/*`
|
|
744
389
|
|
|
745
|
-
|
|
746
|
-
|
|
747
|
-
|
|
748
|
-
|
|
749
|
-
|
|
390
|
+
| Endpoint | Permission | Description |
|
|
391
|
+
|----------|-----------|-------------|
|
|
392
|
+
| `POST /storage/file/upload` | `file.create` | Upload a file (multipart/form-data) |
|
|
393
|
+
| `POST /storage/file/get-all` | `file.read` | List files with pagination |
|
|
394
|
+
| `POST /storage/file/get/:id` | `file.read` | Get file metadata by ID |
|
|
395
|
+
| `POST /storage/file/get-url/:id` | `file.read` | Get presigned or direct file URL |
|
|
396
|
+
| `POST /storage/file/update` | `file.update` | Update file metadata |
|
|
397
|
+
| `POST /storage/file/delete` | `file.delete` | Delete file (from storage + DB) |
|
|
398
|
+
| `POST /storage/file/move` | `file.update` | Move file to a different folder |
|
|
750
399
|
|
|
751
|
-
|
|
752
|
-
if (file.location === 'sftp' || file.location === 'local') {
|
|
753
|
-
const baseUrl = this.getFileBaseUrl(protocol, host);
|
|
754
|
-
file.url = `${baseUrl}/storage/upload/file/${file.key}`;
|
|
755
|
-
return true;
|
|
756
|
-
}
|
|
400
|
+
### File Serving (Local)
|
|
757
401
|
|
|
758
|
-
return false;
|
|
759
|
-
}
|
|
760
402
|
```
|
|
761
|
-
|
|
762
|
-
---
|
|
763
|
-
|
|
764
|
-
## Folder Management
|
|
765
|
-
|
|
766
|
-
### FolderService
|
|
767
|
-
|
|
768
|
-
```typescript
|
|
769
|
-
@Injectable({ scope: Scope.REQUEST })
|
|
770
|
-
export class FolderService extends RequestScopedApiService<
|
|
771
|
-
CreateFolderDto,
|
|
772
|
-
UpdateFolderDto,
|
|
773
|
-
IFolder,
|
|
774
|
-
FolderBase,
|
|
775
|
-
Repository<FolderBase>
|
|
776
|
-
> {
|
|
777
|
-
// Standard CRUD operations inherited from RequestScopedApiService
|
|
778
|
-
// Company filtering applied automatically when enabled
|
|
779
|
-
}
|
|
403
|
+
GET /storage/upload/file/{path}
|
|
780
404
|
```
|
|
781
405
|
|
|
782
|
-
|
|
783
|
-
|
|
784
|
-
```typescript
|
|
785
|
-
export class CreateFolderDto {
|
|
786
|
-
@IsNotEmpty()
|
|
787
|
-
@IsString()
|
|
788
|
-
name!: string;
|
|
789
|
-
}
|
|
790
|
-
|
|
791
|
-
export class UpdateFolderDto extends PartialType(CreateFolderDto) {
|
|
792
|
-
@IsUUID()
|
|
793
|
-
@IsNotEmpty()
|
|
794
|
-
id!: string;
|
|
795
|
-
}
|
|
796
|
-
|
|
797
|
-
export class FolderResponseDto {
|
|
798
|
-
id!: string;
|
|
799
|
-
name!: string;
|
|
800
|
-
slug!: string;
|
|
801
|
-
}
|
|
802
|
-
```
|
|
406
|
+
Served directly by `FileServeMiddleware`. No authentication (public file access). Register the middleware in your `AppModule`.
|
|
803
407
|
|
|
804
408
|
---
|
|
805
409
|
|
|
806
|
-
##
|
|
807
|
-
|
|
808
|
-
### UploadService
|
|
809
|
-
|
|
810
|
-
```typescript
|
|
811
|
-
@Injectable({ scope: Scope.REQUEST })
|
|
812
|
-
export class UploadService {
|
|
813
|
-
|
|
814
|
-
/** Upload single file with validation */
|
|
815
|
-
async uploadSingleFile(
|
|
816
|
-
file: Express.Multer.File,
|
|
817
|
-
options: UploadOptionsDto,
|
|
818
|
-
user?: ILoggedUserInfo
|
|
819
|
-
): Promise<IUploadedFileInfo>;
|
|
820
|
-
|
|
821
|
-
/** Upload multiple files */
|
|
822
|
-
async uploadMultipleFiles(
|
|
823
|
-
files: Express.Multer.File[],
|
|
824
|
-
options: UploadOptionsDto,
|
|
825
|
-
user?: ILoggedUserInfo
|
|
826
|
-
): Promise<IUploadedFileInfo[]>;
|
|
827
|
-
|
|
828
|
-
/** Delete single file */
|
|
829
|
-
async deleteSingleFile(
|
|
830
|
-
key: string,
|
|
831
|
-
storageConfigId?: string,
|
|
832
|
-
user?: ILoggedUserInfo,
|
|
833
|
-
locationHint?: string
|
|
834
|
-
): Promise<boolean>;
|
|
835
|
-
|
|
836
|
-
/** Delete multiple files */
|
|
837
|
-
async deleteMultipleFile(
|
|
838
|
-
keys: string[],
|
|
839
|
-
storageConfigId?: string,
|
|
840
|
-
user?: ILoggedUserInfo,
|
|
841
|
-
locationHint?: string
|
|
842
|
-
): Promise<boolean>;
|
|
843
|
-
|
|
844
|
-
/** Generate presigned URL */
|
|
845
|
-
async makeFileUrl(
|
|
846
|
-
key: string,
|
|
847
|
-
storageConfigId: string,
|
|
848
|
-
expiresIn?: number,
|
|
849
|
-
user?: ILoggedUserInfo
|
|
850
|
-
): Promise<string>;
|
|
851
|
-
|
|
852
|
-
/** Get local storage base path from DB config */
|
|
853
|
-
async getLocalStorageBasePath(): Promise<string | null>;
|
|
854
|
-
}
|
|
855
|
-
```
|
|
856
|
-
|
|
857
|
-
### Upload Options DTO
|
|
858
|
-
|
|
859
|
-
```typescript
|
|
860
|
-
export enum ImageFormat {
|
|
861
|
-
ORIGINAL = 'original',
|
|
862
|
-
JPEG = 'jpeg',
|
|
863
|
-
PNG = 'png',
|
|
864
|
-
WEBP = 'webp',
|
|
865
|
-
}
|
|
866
|
-
|
|
867
|
-
export class UploadOptionsDto {
|
|
868
|
-
@IsOptional()
|
|
869
|
-
@IsUUID()
|
|
870
|
-
storageConfigId?: string;
|
|
871
|
-
|
|
872
|
-
@IsOptional()
|
|
873
|
-
@Matches(/^[a-zA-Z0-9-_/]*$/)
|
|
874
|
-
folderPath?: string;
|
|
875
|
-
|
|
876
|
-
@IsOptional()
|
|
877
|
-
@IsInt()
|
|
878
|
-
@Min(100)
|
|
879
|
-
@Max(10000)
|
|
880
|
-
maxWidth?: number = 1280;
|
|
881
|
-
|
|
882
|
-
@IsOptional()
|
|
883
|
-
@IsInt()
|
|
884
|
-
@Min(100)
|
|
885
|
-
@Max(10000)
|
|
886
|
-
maxHeight?: number = 1280;
|
|
887
|
-
|
|
888
|
-
@IsOptional()
|
|
889
|
-
@IsInt()
|
|
890
|
-
@Min(1)
|
|
891
|
-
@Max(100)
|
|
892
|
-
quality?: number = 85;
|
|
893
|
-
|
|
894
|
-
@IsOptional()
|
|
895
|
-
@IsEnum(ImageFormat)
|
|
896
|
-
format?: ImageFormat = ImageFormat.ORIGINAL;
|
|
897
|
-
|
|
898
|
-
@IsOptional()
|
|
899
|
-
@IsBoolean()
|
|
900
|
-
compress?: boolean = true;
|
|
901
|
-
}
|
|
902
|
-
```
|
|
410
|
+
## Entities
|
|
903
411
|
|
|
904
|
-
|
|
412
|
+
### Core Entities (always registered)
|
|
905
413
|
|
|
906
|
-
|
|
414
|
+
| Entity | Table | Description |
|
|
415
|
+
|--------|-------|-------------|
|
|
416
|
+
| `StorageConfig` | `storage_config` | Provider configuration (credentials, bucket, etc.) |
|
|
417
|
+
| `Folder` | `storage_folder` | Folder hierarchy with `parentId` |
|
|
418
|
+
| `FileManager` | `storage_file` | File metadata (name, path, size, MIME type, provider) |
|
|
907
419
|
|
|
908
|
-
###
|
|
420
|
+
### Company Feature Entities (`enableCompanyFeature: true`)
|
|
909
421
|
|
|
910
|
-
|
|
422
|
+
| Entity | Table | Description |
|
|
423
|
+
|--------|-------|-------------|
|
|
424
|
+
| `StorageConfigWithCompany` | `storage_config` | Same + `companyId` |
|
|
425
|
+
| `FolderWithCompany` | `storage_folder` | Same + `companyId` |
|
|
426
|
+
| `FileManagerWithCompany` | `storage_file` | Same + `companyId` |
|
|
911
427
|
|
|
912
428
|
```typescript
|
|
913
|
-
|
|
914
|
-
/** Detect file type from buffer using magic bytes */
|
|
915
|
-
static detectFileType(buffer: Buffer): string | null;
|
|
916
|
-
|
|
917
|
-
/** Check if MIME type is text-based (no magic bytes) */
|
|
918
|
-
static isTextBasedType(mimeType: string): boolean;
|
|
919
|
-
|
|
920
|
-
/** Check if MIME type is dangerous (HTML, JS, SVG) */
|
|
921
|
-
static isDangerousTextType(mimeType: string): boolean;
|
|
922
|
-
|
|
923
|
-
/** Check if two MIME types are compatible */
|
|
924
|
-
static mimeTypesMatch(detected: string, declared: string): boolean;
|
|
925
|
-
|
|
926
|
-
/** Check if MIME type is in allowed list */
|
|
927
|
-
static isTypeAllowed(mimeType: string, allowedTypes: string[]): boolean;
|
|
928
|
-
|
|
929
|
-
/** Validate file content matches declared MIME type */
|
|
930
|
-
static validateFileContent(
|
|
931
|
-
buffer: Buffer,
|
|
932
|
-
declaredMimeType: string,
|
|
933
|
-
allowedTypes?: string[]
|
|
934
|
-
): FileValidationResult;
|
|
935
|
-
|
|
936
|
-
/** Sanitize filename to prevent path traversal */
|
|
937
|
-
static sanitizeFilename(filename: string): string;
|
|
938
|
-
}
|
|
939
|
-
|
|
940
|
-
interface FileValidationResult {
|
|
941
|
-
valid: boolean;
|
|
942
|
-
detectedType?: string;
|
|
943
|
-
declaredType?: string;
|
|
944
|
-
message?: string;
|
|
945
|
-
}
|
|
946
|
-
```
|
|
947
|
-
|
|
948
|
-
### Supported Magic Bytes
|
|
949
|
-
|
|
950
|
-
| Category | Formats |
|
|
951
|
-
|----------|---------|
|
|
952
|
-
| Images | JPEG, PNG, GIF, BMP, WebP, ICO |
|
|
953
|
-
| Documents | PDF, ZIP (includes DOCX, XLSX, PPTX) |
|
|
954
|
-
| Audio | MP3, OGG, FLAC |
|
|
955
|
-
| Video | MP4, WebM, AVI |
|
|
956
|
-
| Archives | GZIP, 7Z, RAR |
|
|
957
|
-
|
|
958
|
-
### Dangerous File Types
|
|
959
|
-
|
|
960
|
-
These types bypass magic-bytes validation but require **explicit allowlisting**:
|
|
961
|
-
|
|
962
|
-
- `text/html`
|
|
963
|
-
- `application/javascript`, `text/javascript`
|
|
964
|
-
- `image/svg+xml`
|
|
965
|
-
- `application/xhtml+xml`
|
|
966
|
-
|
|
967
|
-
### Safe Text-Based Types
|
|
968
|
-
|
|
969
|
-
These are allowed without magic bytes:
|
|
970
|
-
|
|
971
|
-
- `text/plain`, `text/csv`, `text/markdown`
|
|
972
|
-
- `application/json`, `application/xml`
|
|
973
|
-
- `application/typescript`, `text/css`
|
|
974
|
-
|
|
975
|
-
### Filename Sanitization
|
|
429
|
+
import { StorageModule } from '@flusys/nestjs-storage';
|
|
976
430
|
|
|
977
|
-
|
|
978
|
-
|
|
979
|
-
|
|
980
|
-
|
|
981
|
-
|
|
982
|
-
.replace(/\.{2,}/g, '.') // Replace multiple dots
|
|
983
|
-
.replace(/[^a-zA-Z0-9._-]/g, '_') // Remove special characters
|
|
984
|
-
.substring(0, 255); // Limit length
|
|
985
|
-
}
|
|
431
|
+
TypeOrmModule.forRoot({
|
|
432
|
+
entities: [
|
|
433
|
+
...StorageModule.getEntities({ enableCompanyFeature: true }),
|
|
434
|
+
],
|
|
435
|
+
})
|
|
986
436
|
```
|
|
987
437
|
|
|
988
438
|
---
|
|
989
439
|
|
|
990
|
-
##
|
|
991
|
-
|
|
992
|
-
### ImageCompressor Utility
|
|
440
|
+
## File Serving (Local)
|
|
993
441
|
|
|
994
|
-
|
|
442
|
+
Register `FileServeMiddleware` to serve uploaded local files over HTTP:
|
|
995
443
|
|
|
996
444
|
```typescript
|
|
997
|
-
|
|
998
|
-
static async compress(
|
|
999
|
-
buffer: Buffer,
|
|
1000
|
-
mimetype: string,
|
|
1001
|
-
options?: CompressionOptions
|
|
1002
|
-
): Promise<{ buffer: Buffer; format: string }>;
|
|
1003
|
-
}
|
|
445
|
+
import { FileServeMiddleware } from '@flusys/nestjs-storage';
|
|
1004
446
|
|
|
1005
|
-
|
|
1006
|
-
|
|
1007
|
-
|
|
1008
|
-
quality?: number; // Default: 85
|
|
1009
|
-
format?: ImageFormat; // Default: 'original'
|
|
447
|
+
// In AppModule
|
|
448
|
+
configure(consumer: MiddlewareConsumer) {
|
|
449
|
+
consumer.apply(FileServeMiddleware).forRoutes('storage/upload/file');
|
|
1010
450
|
}
|
|
1011
451
|
```
|
|
1012
452
|
|
|
1013
|
-
|
|
1014
|
-
|
|
1015
|
-
| Format | Quality Options |
|
|
1016
|
-
|--------|-----------------|
|
|
1017
|
-
| JPEG | MozJPEG optimization, 4:4:4 chroma |
|
|
1018
|
-
| PNG | Compression level 9, adaptive filtering, palette |
|
|
1019
|
-
| WebP | Smart subsample, effort 6, lossless at 100% |
|
|
1020
|
-
| AVIF | Effort 6, 4:4:4 chroma |
|
|
1021
|
-
| TIFF | LZW compression, pyramid |
|
|
1022
|
-
| GIF | 256 colors, effort 10, dithering |
|
|
1023
|
-
| JP2 | JPEG 2000, lossless at 100% |
|
|
1024
|
-
|
|
1025
|
-
### Usage
|
|
1026
|
-
|
|
1027
|
-
```typescript
|
|
1028
|
-
// Automatic compression during upload
|
|
1029
|
-
await uploadService.uploadSingleFile(file, {
|
|
1030
|
-
compress: true,
|
|
1031
|
-
maxWidth: 1920,
|
|
1032
|
-
maxHeight: 1080,
|
|
1033
|
-
quality: 80,
|
|
1034
|
-
format: 'webp',
|
|
1035
|
-
}, user);
|
|
1036
|
-
```
|
|
453
|
+
Files are served from the `localStoragePath` directory. The middleware validates paths to prevent directory traversal attacks.
|
|
1037
454
|
|
|
1038
455
|
---
|
|
1039
456
|
|
|
1040
|
-
##
|
|
1041
|
-
|
|
1042
|
-
All storage endpoints are prefixed with `/storage`.
|
|
1043
|
-
|
|
1044
|
-
### Upload Endpoints
|
|
1045
|
-
|
|
1046
|
-
| Endpoint | Method | Description | Permission |
|
|
1047
|
-
|----------|--------|-------------|------------|
|
|
1048
|
-
| `/storage/upload/single-file` | POST | Upload single file | `file.upload` |
|
|
1049
|
-
| `/storage/upload/multiple-file` | POST | Upload multiple files (max 50) | `file.upload` |
|
|
1050
|
-
| `/storage/upload/delete-single-file` | POST | Delete single file | `file.delete` |
|
|
1051
|
-
| `/storage/upload/delete-multiple-file` | POST | Delete multiple files | `file.delete` |
|
|
1052
|
-
| `/storage/upload/file/*filePath` | GET | Serve file (local storage) | Public |
|
|
1053
|
-
|
|
1054
|
-
### File Manager Endpoints
|
|
1055
|
-
|
|
1056
|
-
| Endpoint | Method | Permission |
|
|
1057
|
-
|----------|--------|------------|
|
|
1058
|
-
| `/storage/file-manager/insert` | POST | `file.create` |
|
|
1059
|
-
| `/storage/file-manager/get/:id` | POST | `file.read` |
|
|
1060
|
-
| `/storage/file-manager/get-all` | POST | `file.read` |
|
|
1061
|
-
| `/storage/file-manager/update` | POST | `file.update` |
|
|
1062
|
-
| `/storage/file-manager/delete` | POST | `file.delete` |
|
|
1063
|
-
| `/storage/file-manager/get-files` | POST | JWT Auth |
|
|
1064
|
-
|
|
1065
|
-
### Folder Endpoints
|
|
457
|
+
## Image Compression
|
|
1066
458
|
|
|
1067
|
-
|
|
1068
|
-
|----------|--------|------------|
|
|
1069
|
-
| `/storage/folder/insert` | POST | `folder.create` |
|
|
1070
|
-
| `/storage/folder/get/:id` | POST | `folder.read` |
|
|
1071
|
-
| `/storage/folder/get-all` | POST | `folder.read` |
|
|
1072
|
-
| `/storage/folder/update` | POST | `folder.update` |
|
|
1073
|
-
| `/storage/folder/delete` | POST | `folder.delete` |
|
|
459
|
+
Automatic compression is applied on upload for supported image types:
|
|
1074
460
|
|
|
1075
|
-
|
|
461
|
+
| Format | Compression applied |
|
|
462
|
+
|--------|---------------------|
|
|
463
|
+
| JPEG | Quality 80%, progressive |
|
|
464
|
+
| PNG | Compression level 6 |
|
|
465
|
+
| WebP | Quality 80% |
|
|
466
|
+
| AVIF | Quality 75% |
|
|
467
|
+
| TIFF | Deflate compression |
|
|
468
|
+
| GIF | Passthrough |
|
|
1076
469
|
|
|
1077
|
-
|
|
1078
|
-
|----------|--------|------------|
|
|
1079
|
-
| `/storage/storage-config/insert` | POST | `storageConfig.create` |
|
|
1080
|
-
| `/storage/storage-config/get/:id` | POST | `storageConfig.read` |
|
|
1081
|
-
| `/storage/storage-config/get-all` | POST | `storageConfig.read` |
|
|
1082
|
-
| `/storage/storage-config/update` | POST | `storageConfig.update` |
|
|
1083
|
-
| `/storage/storage-config/delete` | POST | `storageConfig.delete` |
|
|
470
|
+
Custom compression options:
|
|
1084
471
|
|
|
1085
|
-
|
|
472
|
+
```json
|
|
473
|
+
POST /storage/file/upload
|
|
474
|
+
Content-Type: multipart/form-data
|
|
1086
475
|
|
|
1087
|
-
```bash
|
|
1088
|
-
# Upload single file
|
|
1089
|
-
curl -X POST http://localhost:3000/storage/upload/single-file \
|
|
1090
|
-
-H "Authorization: Bearer <token>" \
|
|
1091
|
-
-H "Content-Type: multipart/form-data" \
|
|
1092
|
-
-F "file=@document.pdf" \
|
|
1093
|
-
-F "folderPath=documents" \
|
|
1094
|
-
-F "compress=true"
|
|
1095
|
-
|
|
1096
|
-
# Response:
|
|
1097
476
|
{
|
|
1098
|
-
"
|
|
1099
|
-
"
|
|
1100
|
-
"
|
|
1101
|
-
|
|
1102
|
-
|
|
1103
|
-
|
|
1104
|
-
"contentType": "application/pdf",
|
|
1105
|
-
"location": "local",
|
|
1106
|
-
"storageConfigId": "123e4567-e89b-12d3-a456-426614174000"
|
|
1107
|
-
}
|
|
477
|
+
"file": <binary>,
|
|
478
|
+
"folderId": "uuid",
|
|
479
|
+
"compress": true,
|
|
480
|
+
"maxWidth": 1920,
|
|
481
|
+
"maxHeight": 1080,
|
|
482
|
+
"quality": 75
|
|
1108
483
|
}
|
|
1109
484
|
```
|
|
1110
485
|
|
|
1111
486
|
---
|
|
1112
487
|
|
|
1113
|
-
## File
|
|
1114
|
-
|
|
1115
|
-
The `FileServeMiddleware` handles file serving for local storage with multiple fallback strategies:
|
|
1116
|
-
|
|
1117
|
-
```typescript
|
|
1118
|
-
@Injectable()
|
|
1119
|
-
export class FileServeMiddleware implements NestMiddleware {
|
|
1120
|
-
async use(req: Request, res: Response, next: NextFunction) {
|
|
1121
|
-
const normalizedPath = this.extractFilePath(req);
|
|
1122
|
-
const fullPath = await this.resolveFilePath(normalizedPath);
|
|
1123
|
-
// Stream file with proper headers
|
|
1124
|
-
}
|
|
1125
|
-
|
|
1126
|
-
private async resolveFilePath(normalizedPath: string): Promise<string> {
|
|
1127
|
-
// Strategy 1: Path relative to CWD (new format)
|
|
1128
|
-
// Strategy 2: basePath + remaining path
|
|
1129
|
-
// Strategy 3: Old format - prepend basePath
|
|
1130
|
-
}
|
|
1131
|
-
}
|
|
1132
|
-
```
|
|
488
|
+
## File Validation
|
|
1133
489
|
|
|
1134
|
-
|
|
490
|
+
Files are validated using **magic bytes** (file header inspection), not just the file extension or MIME type header. This prevents users from uploading malicious files with forged extensions.
|
|
1135
491
|
|
|
492
|
+
Allowed types are configured via `allowedFileTypes`:
|
|
1136
493
|
```typescript
|
|
1137
|
-
|
|
1138
|
-
'image/',
|
|
1139
|
-
'video/',
|
|
1140
|
-
'audio/',
|
|
1141
|
-
'text/',
|
|
1142
|
-
'application/pdf',
|
|
1143
|
-
'application/json',
|
|
1144
|
-
'application/xml',
|
|
1145
|
-
];
|
|
494
|
+
allowedFileTypes: ['image/*', 'application/pdf', 'text/plain']
|
|
1146
495
|
```
|
|
1147
496
|
|
|
1148
|
-
|
|
1149
|
-
|
|
1150
|
-
```typescript
|
|
1151
|
-
res.set({
|
|
1152
|
-
'Content-Type': mimeType,
|
|
1153
|
-
'Content-Length': size,
|
|
1154
|
-
'Content-Disposition': isViewable ? 'inline' : 'attachment',
|
|
1155
|
-
'Cache-Control': 'public, max-age=3600',
|
|
1156
|
-
'Accept-Ranges': 'bytes',
|
|
1157
|
-
'Cross-Origin-Resource-Policy': 'cross-origin',
|
|
1158
|
-
'Access-Control-Allow-Origin': '*',
|
|
1159
|
-
'X-Content-Type-Options': 'nosniff',
|
|
1160
|
-
});
|
|
1161
|
-
```
|
|
497
|
+
Wildcard patterns like `image/*` match any subtype (e.g., `image/jpeg`, `image/png`, `image/webp`).
|
|
1162
498
|
|
|
1163
499
|
---
|
|
1164
500
|
|
|
1165
|
-
##
|
|
1166
|
-
|
|
1167
|
-
### StorageDataSourceProvider
|
|
501
|
+
## Exported Services
|
|
1168
502
|
|
|
1169
|
-
|
|
1170
|
-
|
|
1171
|
-
|
|
1172
|
-
|
|
1173
|
-
|
|
1174
|
-
|
|
1175
|
-
|
|
1176
|
-
/** Get storage entities based on company feature flag */
|
|
1177
|
-
async getStorageEntities(enableCompanyFeature?: boolean): Promise<any[]>;
|
|
1178
|
-
|
|
1179
|
-
/** Get company feature for current tenant */
|
|
1180
|
-
getEnableCompanyFeatureForCurrentTenant(): boolean;
|
|
1181
|
-
}
|
|
1182
|
-
```
|
|
1183
|
-
|
|
1184
|
-
### Dynamic Entity Selection
|
|
1185
|
-
|
|
1186
|
-
Services select entities at runtime:
|
|
1187
|
-
|
|
1188
|
-
```typescript
|
|
1189
|
-
@Injectable({ scope: Scope.REQUEST })
|
|
1190
|
-
export class FileManagerService extends RequestScopedApiService<...> {
|
|
1191
|
-
protected resolveEntity(): EntityTarget<FileManagerBase> {
|
|
1192
|
-
return this.storageConfig.isCompanyFeatureEnabled()
|
|
1193
|
-
? FileManagerWithCompany
|
|
1194
|
-
: FileManager;
|
|
1195
|
-
}
|
|
1196
|
-
|
|
1197
|
-
protected getDataSourceProvider() {
|
|
1198
|
-
return this.dataSourceProvider;
|
|
1199
|
-
}
|
|
1200
|
-
}
|
|
1201
|
-
```
|
|
503
|
+
| Service | Description |
|
|
504
|
+
|---------|-------------|
|
|
505
|
+
| `FileManagerService` | File upload, retrieval, deletion, move |
|
|
506
|
+
| `FolderService` | Folder CRUD and tree queries |
|
|
507
|
+
| `StorageConfigService` | Provider config CRUD |
|
|
508
|
+
| `StorageDataSourceProvider` | Dynamic DataSource per request |
|
|
1202
509
|
|
|
1203
510
|
---
|
|
1204
511
|
|
|
1205
|
-
##
|
|
1206
|
-
|
|
1207
|
-
### Company Filtering
|
|
1208
|
-
|
|
1209
|
-
When company feature is enabled:
|
|
1210
|
-
|
|
1211
|
-
```typescript
|
|
1212
|
-
protected override async getExtraManipulateQuery(query, filterDto, user) {
|
|
1213
|
-
const result = await super.getExtraManipulateQuery(query, filterDto, user);
|
|
1214
|
-
|
|
1215
|
-
applyCompanyFilter(query, {
|
|
1216
|
-
isCompanyFeatureEnabled: this.storageConfig.isCompanyFeatureEnabled(),
|
|
1217
|
-
entityAlias: 'file_manager',
|
|
1218
|
-
}, user);
|
|
1219
|
-
|
|
1220
|
-
await this.applyPrivateFileFilter(query, user);
|
|
1221
|
-
|
|
1222
|
-
return result;
|
|
1223
|
-
}
|
|
1224
|
-
```
|
|
1225
|
-
|
|
1226
|
-
### Private File Access
|
|
512
|
+
## Programmatic Usage
|
|
1227
513
|
|
|
1228
514
|
```typescript
|
|
1229
|
-
|
|
1230
|
-
if (!user) {
|
|
1231
|
-
query.andWhere('file_manager.isPrivate = :isPrivate', { isPrivate: false });
|
|
1232
|
-
return;
|
|
1233
|
-
}
|
|
515
|
+
import { FileManagerService } from '@flusys/nestjs-storage';
|
|
1234
516
|
|
|
1235
|
-
|
|
1236
|
-
|
|
1237
|
-
|
|
1238
|
-
|
|
1239
|
-
|
|
1240
|
-
|
|
517
|
+
@Injectable()
|
|
518
|
+
export class ProfileService {
|
|
519
|
+
constructor(
|
|
520
|
+
@Inject(FileManagerService) private readonly fileManager: FileManagerService,
|
|
521
|
+
) {}
|
|
522
|
+
|
|
523
|
+
async uploadAvatar(userId: string, fileBuffer: Buffer, filename: string): Promise<string> {
|
|
524
|
+
const result = await this.fileManager.uploadFile({
|
|
525
|
+
buffer: fileBuffer,
|
|
526
|
+
originalName: filename,
|
|
527
|
+
mimeType: 'image/jpeg',
|
|
528
|
+
folderId: 'avatars-folder-id',
|
|
529
|
+
});
|
|
530
|
+
return result.id; // Store file ID on user profile
|
|
1241
531
|
}
|
|
1242
|
-
}
|
|
1243
|
-
```
|
|
1244
|
-
|
|
1245
|
-
### Storage Config Validation
|
|
1246
532
|
|
|
1247
|
-
|
|
1248
|
-
|
|
1249
|
-
let storageConfig: StorageConfigBase;
|
|
1250
|
-
|
|
1251
|
-
if (storageConfigId) {
|
|
1252
|
-
const config = await this.storageProviderConfigService.findByIdDirect(storageConfigId);
|
|
1253
|
-
if (!config) throw new NotFoundException('Storage configuration not found');
|
|
1254
|
-
|
|
1255
|
-
// Validate company ownership
|
|
1256
|
-
validateCompanyOwnership(
|
|
1257
|
-
config,
|
|
1258
|
-
user,
|
|
1259
|
-
this.storageConfigService.isCompanyFeatureEnabled(),
|
|
1260
|
-
'Storage configuration',
|
|
1261
|
-
);
|
|
1262
|
-
|
|
1263
|
-
storageConfig = config;
|
|
1264
|
-
} else {
|
|
1265
|
-
const defaultConfig = await this.storageProviderConfigService.getDefaultConfig(user);
|
|
1266
|
-
if (!defaultConfig) {
|
|
1267
|
-
throw new NotFoundException('No default storage configuration found');
|
|
1268
|
-
}
|
|
1269
|
-
storageConfig = defaultConfig;
|
|
533
|
+
async getAvatarUrl(fileId: string): Promise<string> {
|
|
534
|
+
return this.fileManager.getFileUrl(fileId);
|
|
1270
535
|
}
|
|
1271
|
-
|
|
1272
|
-
return {
|
|
1273
|
-
provider: await this.createProviderFromConfig(storageConfig),
|
|
1274
|
-
location: storageConfig.storage,
|
|
1275
|
-
configId: storageConfig.id,
|
|
1276
|
-
};
|
|
1277
536
|
}
|
|
1278
537
|
```
|
|
1279
538
|
|
|
1280
539
|
---
|
|
1281
540
|
|
|
1282
|
-
##
|
|
541
|
+
## Troubleshooting
|
|
1283
542
|
|
|
1284
|
-
|
|
543
|
+
**`Provider not registered` error**
|
|
1285
544
|
|
|
1286
|
-
|
|
1287
|
-
import { storageSwaggerConfig } from '@flusys/nestjs-storage/docs';
|
|
1288
|
-
|
|
1289
|
-
// In your main.ts or app module
|
|
1290
|
-
const swaggerOptions = storageSwaggerConfig({
|
|
1291
|
-
enableCompanyFeature: true,
|
|
1292
|
-
databaseMode: 'single',
|
|
1293
|
-
});
|
|
1294
|
-
|
|
1295
|
-
// Returns:
|
|
1296
|
-
// {
|
|
1297
|
-
// title: 'Storage API',
|
|
1298
|
-
// description: '... dynamic description based on config ...',
|
|
1299
|
-
// version: '1.0',
|
|
1300
|
-
// path: 'api/docs/storage',
|
|
1301
|
-
// bearerAuth: true,
|
|
1302
|
-
// excludeSchemaProperties: enableCompanyFeature ? undefined : COMPANY_SCHEMA_EXCLUSIONS,
|
|
1303
|
-
// }
|
|
1304
|
-
```
|
|
545
|
+
You created a StorageConfig with `storage: 'aws'` but forgot to register the S3 provider. Call `StorageProviderRegistry.register()` before the module initializes.
|
|
1305
546
|
|
|
1306
547
|
---
|
|
1307
548
|
|
|
1308
|
-
|
|
1309
|
-
|
|
1310
|
-
### 1. Use Named Storage Configs
|
|
1311
|
-
|
|
1312
|
-
```typescript
|
|
1313
|
-
// ✅ Create meaningful config names
|
|
1314
|
-
await storageConfigService.insert({
|
|
1315
|
-
name: 'default', // Primary storage
|
|
1316
|
-
storage: 'aws',
|
|
1317
|
-
config: { bucket: 'main-files', ... },
|
|
1318
|
-
isDefault: true,
|
|
1319
|
-
}, user);
|
|
1320
|
-
|
|
1321
|
-
await storageConfigService.insert({
|
|
1322
|
-
name: 'media', // Images/videos
|
|
1323
|
-
storage: 'aws',
|
|
1324
|
-
config: { bucket: 'media-files', ... },
|
|
1325
|
-
}, user);
|
|
1326
|
-
|
|
1327
|
-
// ❌ Don't use generic names
|
|
1328
|
-
await storageConfigService.insert({
|
|
1329
|
-
name: 'config1',
|
|
1330
|
-
...
|
|
1331
|
-
});
|
|
1332
|
-
```
|
|
549
|
+
**Local files return 404**
|
|
1333
550
|
|
|
1334
|
-
|
|
551
|
+
Ensure `FileServeMiddleware` is registered and that `localStoragePath` points to an existing directory. The path must be accessible by the Node.js process.
|
|
1335
552
|
|
|
1336
|
-
|
|
1337
|
-
// ✅ Restrict file types in production
|
|
1338
|
-
StorageModule.forRoot({
|
|
1339
|
-
config: {
|
|
1340
|
-
maxFileSize: 10 * 1024 * 1024, // 10MB
|
|
1341
|
-
allowedFileTypes: [
|
|
1342
|
-
'image/jpeg',
|
|
1343
|
-
'image/png',
|
|
1344
|
-
'image/webp',
|
|
1345
|
-
'application/pdf',
|
|
1346
|
-
],
|
|
1347
|
-
},
|
|
1348
|
-
});
|
|
1349
|
-
|
|
1350
|
-
// ❌ Don't allow all file types
|
|
1351
|
-
allowedFileTypes: ['*/*'];
|
|
1352
|
-
```
|
|
1353
|
-
|
|
1354
|
-
### 3. Use Presigned URLs Properly
|
|
1355
|
-
|
|
1356
|
-
```typescript
|
|
1357
|
-
// ✅ Always use getFiles() for URL access
|
|
1358
|
-
const files = await fileManagerService.getFiles(
|
|
1359
|
-
[{ id: 'file-1' }, { id: 'file-2' }],
|
|
1360
|
-
req.protocol,
|
|
1361
|
-
req.get('host'),
|
|
1362
|
-
user,
|
|
1363
|
-
);
|
|
1364
|
-
// URLs are automatically refreshed if expired
|
|
1365
|
-
|
|
1366
|
-
// ❌ Don't cache URLs without expiration check
|
|
1367
|
-
const file = await fileManagerService.getById(id, user);
|
|
1368
|
-
return file.url; // May be expired!
|
|
1369
|
-
```
|
|
1370
|
-
|
|
1371
|
-
### 4. Handle File Deletion Properly
|
|
1372
|
-
|
|
1373
|
-
```typescript
|
|
1374
|
-
// ✅ Use permanent delete to remove from storage
|
|
1375
|
-
await fileManagerService.delete({
|
|
1376
|
-
id: fileId,
|
|
1377
|
-
type: 'permanent', // Deletes from storage provider
|
|
1378
|
-
}, user);
|
|
1379
|
-
|
|
1380
|
-
// Soft delete keeps file in storage (for recovery)
|
|
1381
|
-
await fileManagerService.delete({
|
|
1382
|
-
id: fileId,
|
|
1383
|
-
type: 'soft', // Only marks as deleted in DB
|
|
1384
|
-
}, user);
|
|
1385
|
-
```
|
|
553
|
+
---
|
|
1386
554
|
|
|
1387
|
-
|
|
555
|
+
**`File type not allowed` for a valid file**
|
|
1388
556
|
|
|
1389
|
-
|
|
1390
|
-
// ✅ Enable compression for images
|
|
1391
|
-
await uploadService.uploadSingleFile(file, {
|
|
1392
|
-
compress: true,
|
|
1393
|
-
maxWidth: 1920,
|
|
1394
|
-
maxHeight: 1080,
|
|
1395
|
-
quality: 80,
|
|
1396
|
-
format: 'webp', // Best compression/quality ratio
|
|
1397
|
-
}, user);
|
|
1398
|
-
```
|
|
557
|
+
Magic-byte validation is strict. If your file is being rejected incorrectly, check `allowedFileTypes` in your config and ensure the actual file header matches the declared MIME type.
|
|
1399
558
|
|
|
1400
559
|
---
|
|
1401
560
|
|
|
1402
|
-
|
|
1403
|
-
|
|
1404
|
-
### Main Exports
|
|
561
|
+
**Presigned URL expired immediately**
|
|
1405
562
|
|
|
1406
|
-
|
|
1407
|
-
|
|
1408
|
-
|
|
1409
|
-
|
|
1410
|
-
// Services
|
|
1411
|
-
import {
|
|
1412
|
-
UploadService,
|
|
1413
|
-
FileManagerService,
|
|
1414
|
-
FolderService,
|
|
1415
|
-
StorageProviderConfigService,
|
|
1416
|
-
StorageConfigService,
|
|
1417
|
-
StorageDataSourceProvider,
|
|
1418
|
-
} from '@flusys/nestjs-storage/services';
|
|
1419
|
-
|
|
1420
|
-
// Entities
|
|
1421
|
-
import {
|
|
1422
|
-
FileManager,
|
|
1423
|
-
FileManagerBase,
|
|
1424
|
-
FileManagerWithCompany,
|
|
1425
|
-
Folder,
|
|
1426
|
-
FolderBase,
|
|
1427
|
-
FolderWithCompany,
|
|
1428
|
-
StorageConfig,
|
|
1429
|
-
StorageConfigBase,
|
|
1430
|
-
StorageConfigWithCompany,
|
|
1431
|
-
StorageCoreEntities,
|
|
1432
|
-
StorageCompanyEntities,
|
|
1433
|
-
getStorageEntitiesByConfig,
|
|
1434
|
-
} from '@flusys/nestjs-storage/entities';
|
|
1435
|
-
|
|
1436
|
-
// DTOs
|
|
1437
|
-
import {
|
|
1438
|
-
UploadOptionsDto,
|
|
1439
|
-
DeleteSingleFileDto,
|
|
1440
|
-
DeleteMultipleFileDto,
|
|
1441
|
-
FileUploadResponsePayloadDto,
|
|
1442
|
-
CreateFileManagerDto,
|
|
1443
|
-
UpdateFileManagerDto,
|
|
1444
|
-
FileManagerResponseDto,
|
|
1445
|
-
GetFilesRequestDto,
|
|
1446
|
-
FilesResponseDto,
|
|
1447
|
-
CreateFolderDto,
|
|
1448
|
-
UpdateFolderDto,
|
|
1449
|
-
FolderResponseDto,
|
|
1450
|
-
CreateStorageConfigDto,
|
|
1451
|
-
UpdateStorageConfigDto,
|
|
1452
|
-
StorageConfigResponseDto,
|
|
1453
|
-
ImageFormat,
|
|
1454
|
-
} from '@flusys/nestjs-storage/dtos';
|
|
1455
|
-
|
|
1456
|
-
// Interfaces
|
|
1457
|
-
import {
|
|
1458
|
-
IFileManager,
|
|
1459
|
-
IFolder,
|
|
1460
|
-
IStorageConfig,
|
|
1461
|
-
IStorageModuleConfig,
|
|
1462
|
-
IStorageProvider,
|
|
1463
|
-
IStorageProviderConfig,
|
|
1464
|
-
IUploadedFileInfo,
|
|
1465
|
-
StorageModuleOptions,
|
|
1466
|
-
StorageModuleAsyncOptions,
|
|
1467
|
-
StorageOptionsFactory,
|
|
1468
|
-
} from '@flusys/nestjs-storage/interfaces';
|
|
1469
|
-
|
|
1470
|
-
// Enums
|
|
1471
|
-
import { FileLocationEnum } from '@flusys/nestjs-storage/enums';
|
|
1472
|
-
|
|
1473
|
-
// Providers
|
|
1474
|
-
import {
|
|
1475
|
-
StorageFactoryService,
|
|
1476
|
-
StorageProviderRegistry,
|
|
1477
|
-
LocalProvider,
|
|
1478
|
-
} from '@flusys/nestjs-storage/providers';
|
|
1479
|
-
|
|
1480
|
-
// Utils
|
|
1481
|
-
import { FileValidator, ImageCompressor } from '@flusys/nestjs-storage/utils';
|
|
1482
|
-
|
|
1483
|
-
// Docs
|
|
1484
|
-
import { storageSwaggerConfig } from '@flusys/nestjs-storage/docs';
|
|
1485
|
-
|
|
1486
|
-
// Middleware
|
|
1487
|
-
import { FileServeMiddleware } from '@flusys/nestjs-storage/middlewares';
|
|
1488
|
-
|
|
1489
|
-
// Constants
|
|
1490
|
-
import {
|
|
1491
|
-
STORAGE_MODULE_OPTIONS,
|
|
1492
|
-
DEFAULT_MAX_FILE_SIZE,
|
|
1493
|
-
DEFAULT_ALLOWED_FILE_TYPES,
|
|
1494
|
-
} from '@flusys/nestjs-storage/config';
|
|
563
|
+
S3 and Azure presigned URLs have a TTL. The default TTL is cached per file. Pass a custom TTL in the get-url request body:
|
|
564
|
+
```json
|
|
565
|
+
POST /storage/file/get-url/:id
|
|
566
|
+
{ "ttl": 3600 }
|
|
1495
567
|
```
|
|
1496
568
|
|
|
1497
569
|
---
|
|
1498
570
|
|
|
1499
|
-
##
|
|
1500
|
-
|
|
1501
|
-
The `@flusys/nestjs-storage` package provides:
|
|
571
|
+
## License
|
|
1502
572
|
|
|
1503
|
-
|
|
1504
|
-
- **Connection Reuse** - SHA256-based provider caching
|
|
1505
|
-
- **Security** - Magic bytes validation, path traversal prevention, filename sanitization
|
|
1506
|
-
- **Image Compression** - Sharp-based optimization with multiple formats
|
|
1507
|
-
- **File Validation** - Size, type, and content validation
|
|
1508
|
-
- **Folder Organization** - Simple folder-based file organization
|
|
1509
|
-
- **Presigned URLs** - Automatic URL refresh for cloud providers
|
|
1510
|
-
- **Multi-Tenant** - Company/branch file isolation
|
|
1511
|
-
- **Per-Company Storage** - Different providers per company
|
|
1512
|
-
- **REST API** - Complete CRUD endpoints with POST-only RPC
|
|
1513
|
-
- **Middleware** - File serving with multiple fallback strategies
|
|
573
|
+
MIT © FLUSYS
|
|
1514
574
|
|
|
1515
575
|
---
|
|
1516
576
|
|
|
1517
|
-
**
|
|
577
|
+
> Part of the **FLUSYS** framework — a full-stack monorepo powering Angular 21 + NestJS 11 applications.
|