@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
- # Storage Package Guide
1
+ # @flusys/nestjs-storage
2
2
 
3
- > **Package:** `@flusys/nestjs-storage`
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
- This comprehensive guide covers the storage package - flexible file storage with multiple provider support.
5
+ [![npm version](https://img.shields.io/npm/v/@flusys/nestjs-storage.svg)](https://www.npmjs.com/package/@flusys/nestjs-storage)
6
+ [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT)
7
+ [![NestJS](https://img.shields.io/badge/NestJS-11.x-red.svg)](https://nestjs.com/)
8
+ [![TypeScript](https://img.shields.io/badge/TypeScript-5.x-blue.svg)](https://www.typescriptlang.org/)
9
+ [![Node.js](https://img.shields.io/badge/Node.js-%3E%3D18.x-green.svg)](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
- - [Constants](#constants)
14
- - [Package Architecture](#package-architecture)
15
- - [Module Setup](#module-setup)
16
- - [Entities](#entities)
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
- - [Storage Configuration](#storage-configuration)
19
- - [File Manager](#file-manager)
20
- - [Folder Management](#folder-management)
21
- - [Upload Service](#upload-service)
22
- - [File Validation & Security](#file-validation--security)
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
- - [REST API Endpoints](#rest-api-endpoints)
25
- - [File Serve Middleware](#file-serve-middleware)
26
- - [DataSource Provider Pattern](#datasource-provider-pattern)
27
- - [Multi-Tenant Support](#multi-tenant-support)
28
- - [Swagger Configuration](#swagger-configuration)
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 comprehensive file storage system:
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
- - **Multiple Providers** - Local, AWS S3, Azure Blob, SFTP
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
- ### Package Hierarchy
49
+ ## Features
48
50
 
49
- ```
50
- @flusys/nestjs-core ← Foundation
51
-
52
- @flusys/nestjs-shared ← Shared utilities
53
-
54
- @flusys/nestjs-auth ← User/Company management
55
-
56
- @flusys/nestjs-storage ← File storage (THIS PACKAGE)
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
- ## Installation
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
- # Optional: Install provider-specific packages
70
- npm install @aws-sdk/client-s3 @aws-sdk/lib-storage @aws-sdk/s3-request-presigner # For AWS S3
71
- npm install @azure/storage-blob # For Azure Blob
72
- npm install ssh2-sftp-client # For SFTP
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
- ## Constants
78
-
79
- ```typescript
80
- // Injection Token
81
- export const STORAGE_MODULE_OPTIONS = 'STORAGE_MODULE_OPTIONS';
79
+ ## Installation
82
80
 
83
- // Default Configuration
84
- export const DEFAULT_MAX_FILE_SIZE = 100 * 1024 * 1024; // 100MB
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
- ## Package Architecture
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
- nestjs-storage/
94
- ├── src/
95
- │ ├── modules/
96
- │ │ └── storage.module.ts # Main module with provider registration
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
- ## Module Setup
101
+ ## Quick Start
172
102
 
173
- ### Basic Setup
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: 'localhost',
193
- port: 5432,
194
- username: 'postgres',
195
- password: 'password',
196
- database: 'myapp',
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, // 10MB
199
- allowedFileTypes: ['image/*', 'application/pdf', 'text/*'],
127
+ maxFileSize: 10 * 1024 * 1024, // 10 MB
128
+ allowedFileTypes: ['image/*', 'application/pdf'],
200
129
  localStoragePath: './uploads',
201
- appUrl: process.env.APP_URL || 'http://localhost:3000',
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
- ### With Company Feature
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: true,
214
- includeController: true,
160
+ global?: boolean;
161
+ includeController?: boolean;
215
162
  bootstrapAppConfig: {
216
- databaseMode: 'single',
217
- enableCompanyFeature: true,
218
- permissionMode: 'FULL',
219
- },
163
+ databaseMode: 'single' | 'multi-tenant';
164
+ enableCompanyFeature: boolean;
165
+ };
220
166
  config: {
221
- defaultDatabaseConfig: { /* ... */ },
222
- maxFileSize: 10 * 1024 * 1024,
223
- allowedFileTypes: ['image/*', 'application/pdf'],
224
- localStoragePath: './uploads',
225
- appUrl: process.env.APP_URL,
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
- ### Async Configuration
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: async (configService: ConfigService) => ({
243
- defaultDatabaseConfig: configService.getDatabaseConfig(),
244
- maxFileSize: configService.get('MAX_FILE_SIZE'),
245
- allowedFileTypes: configService.get('ALLOWED_FILE_TYPES'),
246
- localStoragePath: configService.get('UPLOAD_PATH'),
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
- ### Configuration Options
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
- /** Allowed MIME types or patterns (default: ['*/*']) */
214
+
215
+ /** Allowed MIME types. Use '*/*' for all. (default: ['*/*']) */
260
216
  allowedFileTypes?: string[];
261
- /** Default storage provider (optional) */
262
- defaultStorageProvider?: string;
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
- interface StorageModuleOptions extends IDynamicModuleConfig {
270
- bootstrapAppConfig?: IBootstrapAppConfig;
271
- config?: IStorageModuleConfig;
221
+ /** Application base URL for building file URLs (local provider) */
222
+ appUrl?: string;
272
223
  }
273
224
  ```
274
225
 
275
- ### Swagger Schema Behavior
226
+ ---
276
227
 
277
- When `enableCompanyFeature: false`, the following properties are automatically hidden from Swagger:
228
+ ## Feature Toggles
278
229
 
279
- | DTO | Hidden Fields |
280
- | -------------------------- | ------------- |
281
- | `FileManagerResponseDto` | `companyId` |
282
- | `FolderResponseDto` | `companyId` |
283
- | `StorageConfigResponseDto` | `companyId` |
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
- ## Entities
238
+ ## Storage Providers
288
239
 
289
- ### FileLocationEnum
240
+ ### Local Provider
290
241
 
291
- ```typescript
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
- ### Entity Groups
244
+ Create a `StorageConfig` record:
301
245
 
302
- ```typescript
303
- // Core entities (no company feature)
304
- export const StorageCoreEntities = [FileManager, Folder, StorageConfig];
305
-
306
- // Company-specific entities
307
- export const StorageCompanyEntities = [
308
- FileManagerWithCompany,
309
- FolderWithCompany,
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
- ### FileManager Entity
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
- @Column({ type: 'varchar', length: 255 })
333
- contentType!: string; // MIME type
259
+ ### AWS S3
334
260
 
335
- @Column({ type: 'varchar', length: 255 })
336
- size!: string; // File size as string
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
- ### Folder Entity
370
-
266
+ **2. Register the provider at startup (before module init):**
371
267
  ```typescript
372
- @Entity({ name: 'folder' })
373
- export class Folder extends Identity {
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
- // With company feature
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
- ### StorageConfig Entity
393
-
394
- ```typescript
395
- @Entity({ name: 'storage_config' })
396
- @Index(['name'])
397
- @Index(['storage'])
398
- @Index(['isActive'])
399
- @Index(['isDefault'])
400
- export class StorageConfig extends Identity {
401
- @Column({ type: 'varchar', length: 255 })
402
- name!: string; // e.g., 'default', 'backups', 'media'
403
-
404
- @Column({ type: 'varchar', length: 50 })
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
- interface IUploadedFileInfo {
443
- key: string;
444
- contentType: string;
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
- ### Provider Registration
453
-
454
- Providers are automatically registered when the module loads:
455
-
297
+ **2. Register:**
456
298
  ```typescript
457
- // storage.module.ts
458
- StorageProviderRegistry.register(FileLocationEnum.LOCAL, LocalProvider);
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
- ### Local Provider
478
-
479
- ```typescript
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
- storage: 'local',
486
- config: {
487
- basePath: './uploads', // Base directory
488
- baseUrl: 'http://localhost:3000' // Optional, for URL generation
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
- **Features:**
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
- // Configuration:
505
- {
506
- storage: 'aws',
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
- ### Azure Blob Provider
518
-
324
+ **2. Register:**
519
325
  ```typescript
520
- // Requires: npm install @azure/storage-blob
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
- ### SFTP Provider
536
-
537
- ```typescript
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
- storage: 'sftp',
543
- config: {
544
- host: 'sftp.example.com',
545
- port: 22,
546
- username: 'user',
547
- password: 'password',
548
- // Or use private key
549
- privateKey: '-----BEGIN RSA PRIVATE KEY-----...',
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 Factory & Caching
346
+ ### Custom Provider
556
347
 
557
348
  ```typescript
558
- @Injectable()
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
- // Cleanup on module destroy
588
- async onModuleDestroy(): Promise<void> {
589
- for (const [key, provider] of this.cache.entries()) {
590
- if ('close' in provider && typeof (provider as any).close === 'function') {
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
- /** Get configs by storage type */
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
- ## File Manager
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
- ### URL Generation & Refresh
364
+ All endpoints use **POST** and require JWT authentication.
717
365
 
718
- Files automatically get presigned URLs when needed:
366
+ ### Storage Config `POST /storage/storage-config/*`
719
367
 
720
- ```typescript
721
- async getFiles(dtos: GetFilesRequestDto[], protocol: string, host: string, user?: ILoggedUserInfo): Promise<FilesResponseDto[]> {
722
- const files = await this.repository.findBy({ id: In(ids) });
723
- const now = Date.now();
724
- const updatedFiles: FileManagerBase[] = [];
725
-
726
- const responses = await Promise.all(
727
- files.map(async (file) => {
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
- if (updatedFiles.length) {
735
- await this.repository.save(updatedFiles);
736
- }
377
+ ### Folders — `POST /storage/folder/*`
737
378
 
738
- return responses;
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
- private async refreshFileUrl(file, protocol, host, now, user): Promise<boolean> {
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
- if (needsNewUrl && file.storageConfigId) {
746
- file.url = await this.uploadService.makeFileUrl(file.key, file.storageConfigId, 3600, user);
747
- file.expiresAt = now + 3600 * 1000;
748
- return true;
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
- // For local/SFTP, generate serving URL
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
- ### Folder DTOs
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
- ## Upload Service
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
- ## File Validation & Security
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
- ### FileValidator Utility
420
+ ### Company Feature Entities (`enableCompanyFeature: true`)
909
421
 
910
- The `FileValidator` class provides security features to prevent file upload attacks:
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
- export class FileValidator {
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
- ```typescript
978
- static sanitizeFilename(filename: string): string {
979
- return filename
980
- .replace(/^.*[\\\/]/, '') // Remove path components
981
- .replace(/\0/g, '') // Remove null bytes
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
- ## Image Compression
991
-
992
- ### ImageCompressor Utility
440
+ ## File Serving (Local)
993
441
 
994
- Uses Sharp for image processing:
442
+ Register `FileServeMiddleware` to serve uploaded local files over HTTP:
995
443
 
996
444
  ```typescript
997
- export class ImageCompressor {
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
- interface CompressionOptions {
1006
- maxWidth?: number; // Default: 1280
1007
- maxHeight?: number; // Default: 1280
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
- ### Supported Output Formats
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
- ## REST API Endpoints
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
- | Endpoint | Method | Permission |
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
- ### Storage Config Endpoints
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
- | Endpoint | Method | Permission |
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
- ### Upload Request Example
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
- "success": true,
1099
- "message": "File uploaded successfully",
1100
- "data": {
1101
- "name": "abc123-document.pdf",
1102
- "key": "uploads/documents/abc123-document.pdf",
1103
- "size": 102.4,
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 Serve Middleware
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
- ### MIME Types for Inline Display
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
- const VIEWABLE_TYPE_PREFIXES = [
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
- ### Response Headers
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
- ## DataSource Provider Pattern
1166
-
1167
- ### StorageDataSourceProvider
501
+ ## Exported Services
1168
502
 
1169
- ```typescript
1170
- @Injectable({ scope: Scope.REQUEST })
1171
- export class StorageDataSourceProvider extends MultiTenantDataSourceService {
1172
- // Storage-specific static cache (isolated from Auth/IAM)
1173
- protected static override readonly tenantConnections = new Map<string, DataSource>();
1174
- protected static override singleDataSource: DataSource | null = null;
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
- ## Multi-Tenant Support
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
- private async applyPrivateFileFilter(query, user): Promise<void> {
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
- const cacheKey = `user_action_permission_${user.id}_${user.companyId}`;
1236
- const actions = await this.cacheManager.get(cacheKey);
1237
- const canViewPrivate = actions?.some((a) => a.url === 'storage.file.viewPrivate');
1238
-
1239
- if (!canViewPrivate) {
1240
- query.andWhere('file_manager.isPrivate = :isPrivate', { isPrivate: false });
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
- ```typescript
1248
- private async getStorageProviderWithConfig(storageConfigId?: string, user?: ILoggedUserInfo) {
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
- ## Swagger Configuration
541
+ ## Troubleshooting
1283
542
 
1284
- ### Using storageSwaggerConfig
543
+ **`Provider not registered` error**
1285
544
 
1286
- ```typescript
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
- ## Best Practices
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
- ### 2. Configure File Validation
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
- ```typescript
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
- ### 5. Optimize Image Uploads
555
+ **`File type not allowed` for a valid file**
1388
556
 
1389
- ```typescript
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
- ## API Reference
1403
-
1404
- ### Main Exports
561
+ **Presigned URL expired immediately**
1405
562
 
1406
- ```typescript
1407
- // Module
1408
- import { StorageModule } from '@flusys/nestjs-storage';
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
- ## Summary
1500
-
1501
- The `@flusys/nestjs-storage` package provides:
571
+ ## License
1502
572
 
1503
- - **Multiple Providers** - Local, AWS S3, Azure Blob, SFTP
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
- **Last Updated:** 2026-02-25
577
+ > Part of the **FLUSYS** framework — a full-stack monorepo powering Angular 21 + NestJS 11 applications.