@flusys/nestjs-storage 2.0.0 → 3.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -1,6 +1,7 @@
1
1
  # Storage Package Guide
2
2
 
3
3
  > **Package:** `@flusys/nestjs-storage`
4
+ > **Version:** 3.0.0
4
5
  > **Type:** File storage system with pluggable providers and multi-tenant support
5
6
 
6
7
  This comprehensive guide covers the storage package - flexible file storage with multiple provider support.
@@ -18,8 +19,13 @@ This comprehensive guide covers the storage package - flexible file storage with
18
19
  - [File Manager](#file-manager)
19
20
  - [Folder Management](#folder-management)
20
21
  - [Upload Service](#upload-service)
22
+ - [File Validation & Security](#file-validation--security)
23
+ - [Image Compression](#image-compression)
21
24
  - [REST API Endpoints](#rest-api-endpoints)
25
+ - [File Serve Middleware](#file-serve-middleware)
26
+ - [DataSource Provider Pattern](#datasource-provider-pattern)
22
27
  - [Multi-Tenant Support](#multi-tenant-support)
28
+ - [Swagger Configuration](#swagger-configuration)
23
29
  - [Best Practices](#best-practices)
24
30
  - [API Reference](#api-reference)
25
31
 
@@ -30,10 +36,11 @@ This comprehensive guide covers the storage package - flexible file storage with
30
36
  `@flusys/nestjs-storage` provides a comprehensive file storage system:
31
37
 
32
38
  - **Multiple Providers** - Local, AWS S3, Azure Blob, SFTP
33
- - **Provider Connection Reuse** - Efficient connection management
34
- - **File Validation** - Size and type validation
35
- - **Folder Hierarchy** - Nested folder structure
36
- - **Presigned URLs** - Secure time-limited access
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
37
44
  - **Multi-Tenant Support** - Company/branch file isolation
38
45
  - **Storage Configuration** - Per-company storage settings
39
46
 
@@ -56,10 +63,13 @@ This comprehensive guide covers the storage package - flexible file storage with
56
63
  ```bash
57
64
  npm install @flusys/nestjs-storage @flusys/nestjs-shared @flusys/nestjs-core
58
65
 
66
+ # Required dependencies
67
+ npm install sharp mime-types uuid
68
+
59
69
  # Optional: Install provider-specific packages
60
- npm install @aws-sdk/client-s3 @aws-sdk/s3-request-presigner # For AWS S3
61
- npm install @azure/storage-blob # For Azure Blob
62
- npm install ssh2-sftp-client # For SFTP
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
63
73
  ```
64
74
 
65
75
  ---
@@ -83,54 +93,51 @@ export const DEFAULT_ALLOWED_FILE_TYPES = ['*/*']; // All file types
83
93
  nestjs-storage/
84
94
  ├── src/
85
95
  │ ├── modules/
86
- │ │ └── storage.module.ts # Main module
96
+ │ │ └── storage.module.ts # Main module with provider registration
87
97
  │ │
88
98
  │ ├── config/
89
- │ │ ├── storage-config.service.ts # Configuration service
90
- │ │ ├── storage.constants.ts # Constants
99
+ │ │ ├── storage.constants.ts # Module constants
91
100
  │ │ └── index.ts
92
101
  │ │
93
102
  │ ├── providers/
94
- │ │ ├── local-provider.ts # Local filesystem
95
- │ │ ├── s3-provider.optional.ts # AWS S3
96
- │ │ ├── azure-provider.optional.ts # Azure Blob
97
- │ │ ├── sftp-provider.optional.ts # SFTP
98
- │ │ ├── storage-factory.service.ts # Provider factory
99
- │ │ ├── storage-provider.registry.ts
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
100
109
  │ │ └── index.ts
101
110
  │ │
102
111
  │ ├── services/
103
- │ │ ├── upload.service.ts # Upload operations
104
- │ │ ├── file-manager.service.ts # File metadata CRUD
105
- │ │ ├── folder.service.ts # Folder CRUD
106
- │ │ ├── storage-provider-config.service.ts
107
- │ │ ├── storage-datasource.provider.ts # Dynamic entity loading
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
108
118
  │ │ └── index.ts
109
119
  │ │
110
120
  │ ├── controllers/
111
- │ │ ├── upload.controller.ts
112
- │ │ ├── file-manager.controller.ts
113
- │ │ ├── folder.controller.ts
114
- │ │ ├── storage-config.controller.ts
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/*
115
125
  │ │ └── index.ts
116
126
  │ │
117
127
  │ ├── entities/
118
- │ │ ├── file-manager-base.entity.ts
119
- │ │ ├── file-manager.entity.ts
128
+ │ │ ├── file-manager.entity.ts # FileManager base
120
129
  │ │ ├── file-manager-with-company.entity.ts
121
- │ │ ├── folder-base.entity.ts
122
- │ │ ├── folder.entity.ts
130
+ │ │ ├── folder.entity.ts # Folder base
123
131
  │ │ ├── folder-with-company.entity.ts
124
- │ │ ├── storage-config-base.entity.ts
125
- │ │ ├── storage-config.entity.ts
132
+ │ │ ├── storage-config.entity.ts # StorageConfig base
126
133
  │ │ ├── storage-config-with-company.entity.ts
127
134
  │ │ └── index.ts
128
135
  │ │
129
136
  │ ├── dtos/
130
- │ │ ├── upload.dto.ts
131
- │ │ ├── file-manager.dto.ts
132
- │ │ ├── folder.dto.ts
133
- │ │ ├── storage-config.dto.ts
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
134
141
  │ │ └── index.ts
135
142
  │ │
136
143
  │ ├── interfaces/
@@ -142,11 +149,20 @@ nestjs-storage/
142
149
  │ │ └── index.ts
143
150
  │ │
144
151
  │ ├── enums/
145
- │ │ ├── file-location.enum.ts
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
146
161
  │ │ └── index.ts
147
162
  │ │
148
163
  │ └── utils/
149
- │ ├── image-compressor.util.ts
164
+ │ ├── file-validator.util.ts # Magic bytes validation
165
+ │ ├── image-compressor.util.ts # Sharp-based compression
150
166
  │ └── index.ts
151
167
  ```
152
168
 
@@ -168,22 +184,21 @@ import { StorageModule } from '@flusys/nestjs-storage';
168
184
  bootstrapAppConfig: {
169
185
  databaseMode: 'single',
170
186
  enableCompanyFeature: false,
171
- permissionMode: 'RBAC', // IAM permission mode
187
+ permissionMode: 'RBAC',
172
188
  },
173
189
  config: {
174
190
  defaultDatabaseConfig: {
175
- type: 'mysql',
191
+ type: 'postgres',
176
192
  host: 'localhost',
177
- port: 3306,
178
- username: 'root',
193
+ port: 5432,
194
+ username: 'postgres',
179
195
  password: 'password',
180
196
  database: 'myapp',
181
197
  },
182
- // File validation
183
198
  maxFileSize: 10 * 1024 * 1024, // 10MB
184
199
  allowedFileTypes: ['image/*', 'application/pdf', 'text/*'],
185
- // URL generation (optional - falls back to APP_URL env or PORT)
186
- appUrl: process.env.APP_URL || `http://localhost:${process.env.PORT || 3000}`,
200
+ localStoragePath: './uploads',
201
+ appUrl: process.env.APP_URL || 'http://localhost:3000',
187
202
  },
188
203
  }),
189
204
  ],
@@ -199,20 +214,15 @@ StorageModule.forRoot({
199
214
  includeController: true,
200
215
  bootstrapAppConfig: {
201
216
  databaseMode: 'single',
202
- enableCompanyFeature: true, // Enable company/branch isolation
203
- permissionMode: 'FULL', // Use both roles and direct permissions
217
+ enableCompanyFeature: true,
218
+ permissionMode: 'FULL',
204
219
  },
205
220
  config: {
206
- defaultDatabaseConfig: {
207
- type: 'mysql',
208
- host: 'localhost',
209
- port: 3306,
210
- username: 'root',
211
- password: 'password',
212
- database: 'myapp',
213
- },
221
+ defaultDatabaseConfig: { /* ... */ },
214
222
  maxFileSize: 10 * 1024 * 1024,
215
223
  allowedFileTypes: ['image/*', 'application/pdf'],
224
+ localStoragePath: './uploads',
225
+ appUrl: process.env.APP_URL,
216
226
  },
217
227
  });
218
228
  ```
@@ -233,6 +243,8 @@ StorageModule.forRootAsync({
233
243
  defaultDatabaseConfig: configService.getDatabaseConfig(),
234
244
  maxFileSize: configService.get('MAX_FILE_SIZE'),
235
245
  allowedFileTypes: configService.get('ALLOWED_FILE_TYPES'),
246
+ localStoragePath: configService.get('UPLOAD_PATH'),
247
+ appUrl: configService.get('APP_URL'),
236
248
  }),
237
249
  inject: [ConfigService],
238
250
  });
@@ -241,28 +253,28 @@ StorageModule.forRootAsync({
241
253
  ### Configuration Options
242
254
 
243
255
  ```typescript
244
- interface StorageModuleOptions {
245
- global?: boolean;
246
- includeController?: boolean;
247
- bootstrapAppConfig: {
248
- databaseMode: 'single' | 'multi-tenant';
249
- enableCompanyFeature: boolean;
250
- permissionMode?: 'FULL' | 'RBAC' | 'DIRECT'; // IAM permission mode
251
- };
252
- config: {
253
- defaultDatabaseConfig: IDatabaseConfig;
254
- maxFileSize?: number; // Max file size in bytes
255
- allowedFileTypes?: string[]; // MIME type patterns
256
- uploadPath?: string; // Local upload path
257
- };
256
+ interface IStorageModuleConfig extends IDataSourceServiceOptions {
257
+ /** Maximum file size in bytes (default: 100MB) */
258
+ maxFileSize?: number;
259
+ /** Allowed MIME types or patterns (default: ['*/*']) */
260
+ allowedFileTypes?: string[];
261
+ /** Default storage provider (optional) */
262
+ defaultStorageProvider?: string;
263
+ /** Local storage path (default: './uploads') */
264
+ localStoragePath?: string;
265
+ /** Application base URL for file URLs */
266
+ appUrl?: string;
258
267
  }
259
- ```
260
268
 
261
- **Note:** `permissionMode` in `bootstrapAppConfig` is used by IAM module for permission checks. Storage module respects these permissions for file access control.
269
+ interface StorageModuleOptions extends IDynamicModuleConfig {
270
+ bootstrapAppConfig?: IBootstrapAppConfig;
271
+ config?: IStorageModuleConfig;
272
+ }
273
+ ```
262
274
 
263
275
  ### Swagger Schema Behavior
264
276
 
265
- When `enableCompanyFeature: false`, the following properties are automatically hidden from Swagger documentation:
277
+ When `enableCompanyFeature: false`, the following properties are automatically hidden from Swagger:
266
278
 
267
279
  | DTO | Hidden Fields |
268
280
  | -------------------------- | ------------- |
@@ -270,12 +282,21 @@ When `enableCompanyFeature: false`, the following properties are automatically h
270
282
  | `FolderResponseDto` | `companyId` |
271
283
  | `StorageConfigResponseDto` | `companyId` |
272
284
 
273
- This ensures the API documentation accurately reflects the available fields based on your configuration.
274
-
275
285
  ---
276
286
 
277
287
  ## Entities
278
288
 
289
+ ### FileLocationEnum
290
+
291
+ ```typescript
292
+ export enum FileLocationEnum {
293
+ AWS = 'aws',
294
+ AZURE = 'azure',
295
+ SFTP = 'sftp',
296
+ LOCAL = 'local',
297
+ }
298
+ ```
299
+
279
300
  ### Entity Groups
280
301
 
281
302
  ```typescript
@@ -294,58 +315,201 @@ export function getStorageEntitiesByConfig(enableCompanyFeature: boolean): any[]
294
315
  return enableCompanyFeature ? StorageCompanyEntities : StorageCoreEntities;
295
316
  }
296
317
 
297
- // Base type aliases for backwards compatibility
318
+ // Base type aliases
298
319
  export { FileManager as FileManagerBase } from './file-manager.entity';
299
320
  export { Folder as FolderBase } from './folder.entity';
300
321
  export { StorageConfig as StorageConfigBase } from './storage-config.entity';
301
322
  ```
302
323
 
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
331
+
332
+ @Column({ type: 'varchar', length: 255 })
333
+ contentType!: string; // MIME type
334
+
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
+ }
367
+ ```
368
+
369
+ ### Folder Entity
370
+
371
+ ```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
+ }
383
+
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
+ }
390
+ ```
391
+
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;
422
+ }
423
+ ```
424
+
303
425
  ---
304
426
 
305
427
  ## Storage Providers
306
428
 
307
- ### Provider Types
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
+ }
441
+
442
+ interface IUploadedFileInfo {
443
+ key: string;
444
+ contentType: string;
445
+ size: number;
446
+ name: string;
447
+ location?: string;
448
+ storageConfigId?: string;
449
+ }
450
+ ```
451
+
452
+ ### Provider Registration
453
+
454
+ Providers are automatically registered when the module loads:
308
455
 
309
456
  ```typescript
310
- enum FileLocationEnum {
311
- LOCAL = 'LOCAL', // Local filesystem
312
- AWS = 'AWS', // AWS S3
313
- AZURE = 'AZURE', // Azure Blob Storage
314
- SFTP = 'SFTP', // SFTP server
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
+ }
315
474
  }
316
475
  ```
317
476
 
318
477
  ### Local Provider
319
478
 
320
479
  ```typescript
321
- // Default provider - always available
322
- // Stores files in local filesystem
480
+ // Built-in, no external dependencies required
481
+ // Uses Node.js fs module
323
482
 
324
483
  // Configuration:
325
484
  {
326
- storage: FileLocationEnum.LOCAL,
485
+ storage: 'local',
327
486
  config: {
328
- uploadPath: './uploads', // Base directory
487
+ basePath: './uploads', // Base directory
488
+ baseUrl: 'http://localhost:3000' // Optional, for URL generation
329
489
  }
330
490
  }
331
491
  ```
332
492
 
493
+ **Features:**
494
+ - Path traversal attack prevention
495
+ - Automatic directory creation
496
+ - UUID-prefixed filenames
497
+ - Image compression support
498
+
333
499
  ### AWS S3 Provider
334
500
 
335
501
  ```typescript
336
- // Requires: npm install @aws-sdk/client-s3 @aws-sdk/s3-request-presigner
502
+ // Requires: npm install @aws-sdk/client-s3 @aws-sdk/lib-storage @aws-sdk/s3-request-presigner
337
503
 
338
504
  // Configuration:
339
505
  {
340
- storage: FileLocationEnum.AWS,
506
+ storage: 'aws',
341
507
  config: {
342
508
  region: 'us-east-1',
343
509
  bucket: 'my-bucket',
344
510
  accessKeyId: 'AKIA...',
345
511
  secretAccessKey: 'secret...',
346
- // Optional
347
- endpoint: 'https://s3.us-east-1.amazonaws.com',
348
- forcePathStyle: false,
512
+ endpoint: 'https://s3.us-east-1.amazonaws.com' // Optional
349
513
  }
350
514
  }
351
515
  ```
@@ -357,13 +521,13 @@ enum FileLocationEnum {
357
521
 
358
522
  // Configuration:
359
523
  {
360
- storage: FileLocationEnum.AZURE,
524
+ storage: 'azure',
361
525
  config: {
362
- connectionString: 'DefaultEndpointsProtocol=https;...',
363
- containerName: 'my-container',
364
- // Or use SAS token
365
526
  accountName: 'myaccount',
366
- sasToken: '?sv=...',
527
+ accountKey: 'key...',
528
+ containerName: 'my-container',
529
+ // Or use connection string
530
+ connectionString: 'DefaultEndpointsProtocol=https;...'
367
531
  }
368
532
  }
369
533
  ```
@@ -375,7 +539,7 @@ enum FileLocationEnum {
375
539
 
376
540
  // Configuration:
377
541
  {
378
- storage: FileLocationEnum.SFTP,
542
+ storage: 'sftp',
379
543
  config: {
380
544
  host: 'sftp.example.com',
381
545
  port: 22,
@@ -383,60 +547,51 @@ enum FileLocationEnum {
383
547
  password: 'password',
384
548
  // Or use private key
385
549
  privateKey: '-----BEGIN RSA PRIVATE KEY-----...',
386
- basePath: '/uploads',
550
+ basePath: '/uploads'
387
551
  }
388
552
  }
389
553
  ```
390
554
 
391
- ### Provider Registration
392
-
393
- Providers are automatically registered via `UploadService.onModuleInit()`:
394
- - **Local** - Always available (no external dependencies)
395
- - **S3/Azure/SFTP** - Registered only if their SDK dependencies are installed
396
-
397
- ### Provider Connection Reuse
398
-
399
- The `StorageFactoryService` caches provider instances:
555
+ ### Provider Factory & Caching
400
556
 
401
557
  ```typescript
402
- // storage-factory.service.ts
403
558
  @Injectable()
404
559
  export class StorageFactoryService implements OnModuleDestroy {
405
- private providerCache = new Map<string, IStorageProvider>();
406
-
407
- // Generate unique cache key using SHA256 hash
408
- private generateCacheKey(config: IStorageProviderConfig): string {
409
- const configString = JSON.stringify(config.config, Object.keys(config.config || {}).sort());
410
- const configHash = crypto
411
- .createHash('sha256')
412
- .update(configString)
413
- .digest('hex')
414
- .substring(0, 16);
415
- return `${config.provider}-${configHash}`;
416
- }
560
+ private readonly cache = new Map<string, IStorageProvider>();
417
561
 
418
562
  async createProvider(config: IStorageProviderConfig): Promise<IStorageProvider> {
419
563
  const cacheKey = this.generateCacheKey(config);
420
564
 
421
- // Return cached provider if exists
422
- if (this.providerCache.has(cacheKey)) {
423
- return this.providerCache.get(cacheKey)!;
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`);
424
571
  }
425
572
 
426
- // Create new provider
427
- const provider = await StorageProviderRegistry.create(config);
428
- this.providerCache.set(cacheKey, provider);
429
- return provider;
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}`;
430
585
  }
431
586
 
432
587
  // Cleanup on module destroy
433
588
  async onModuleDestroy(): Promise<void> {
434
- for (const [key, provider] of this.providerCache.entries()) {
589
+ for (const [key, provider] of this.cache.entries()) {
435
590
  if ('close' in provider && typeof (provider as any).close === 'function') {
436
591
  await (provider as any).close();
437
592
  }
438
593
  }
439
- this.providerCache.clear();
594
+ this.cache.clear();
440
595
  }
441
596
  }
442
597
  ```
@@ -445,30 +600,53 @@ export class StorageFactoryService implements OnModuleDestroy {
445
600
 
446
601
  ## Storage Configuration
447
602
 
448
- ### StorageConfig Entity
449
-
450
- Each company can have multiple storage configurations:
603
+ ### StorageProviderConfigService
451
604
 
452
605
  ```typescript
453
- @Entity('storage_configs')
454
- export class StorageConfig extends Identity {
455
- @Column({ length: 100 })
456
- name: string; // e.g., 'default', 'backups', 'media'
606
+ @Injectable({ scope: Scope.REQUEST })
607
+ export class StorageProviderConfigService extends RequestScopedApiService<...> {
457
608
 
458
- @Column({ type: 'enum', enum: FileLocationEnum })
459
- storage: FileLocationEnum;
609
+ /** Direct lookup by ID (bypasses company filtering) */
610
+ async findByIdDirect(id: string): Promise<StorageConfigBase | null>;
460
611
 
461
- @Column({ type: 'json' })
462
- config: Record<string, any>; // Provider-specific config
612
+ /** Get default config for user's company */
613
+ async getDefaultConfig(user?: ILoggedUserInfo): Promise<StorageConfigBase | null>;
614
+
615
+ /** Get configs by storage type */
616
+ async getConfigByType(storage: string, user?: ILoggedUserInfo): Promise<StorageConfigBase[]>;
617
+ }
618
+ ```
463
619
 
464
- @Column({ default: true })
465
- isActive: boolean; // Whether config is active and usable
620
+ ### Default Configuration Resolution
466
621
 
467
- @Column({ default: false })
468
- isDefault: boolean; // Set as default configuration
622
+ When `storageConfigId` is not provided for uploads:
469
623
 
470
- @Column({ nullable: true })
471
- companyId: string; // Company scope (when enabled)
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
+ });
472
650
  }
473
651
  ```
474
652
 
@@ -483,737 +661,646 @@ export class SetupService {
483
661
 
484
662
  async setupStorageConfigs(user: ILoggedUserInfo) {
485
663
  // Create default S3 config
486
- await this.storageConfigService.insert(
487
- {
488
- name: 'default',
489
- storage: FileLocationEnum.AWS,
490
- config: {
491
- region: 'us-east-1',
492
- bucket: 'company-files',
493
- accessKeyId: process.env.AWS_ACCESS_KEY,
494
- secretAccessKey: process.env.AWS_SECRET_KEY,
495
- },
496
- isActive: true,
497
- isDefault: true, // Set as default configuration
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,
498
672
  },
499
- user,
500
- );
501
-
502
- // Create backup storage config
503
- await this.storageConfigService.insert(
504
- {
505
- name: 'backups',
506
- storage: FileLocationEnum.SFTP,
507
- config: {
508
- host: 'backup.example.com',
509
- username: 'backup-user',
510
- privateKey: process.env.SFTP_PRIVATE_KEY,
511
- basePath: '/backups',
512
- },
513
- isActive: true,
514
- isDefault: false, // Not the default
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',
515
683
  },
516
- user,
517
- );
684
+ isActive: true,
685
+ isDefault: false,
686
+ }, user);
518
687
  }
519
688
  }
520
689
  ```
521
690
 
522
- ### Storage Provider Resolution Flow
691
+ ---
523
692
 
524
- When performing upload operations, the `UploadService` resolves the storage provider using a multi-level fallback strategy:
693
+ ## File Manager
525
694
 
526
- **Resolution Flow Diagram:**
695
+ ### FileManagerService
527
696
 
528
- ```
529
- Upload Request (with optional storageConfigId)
530
-
531
-
532
- ┌─────────────────────────────────────────────────────────┐
533
- 1. Check if storageConfigId provided │
534
- └─────────────────────────────────────────────────────────┘
535
-
536
- provided? ─┴── no ──┐
537
- │ │
538
- ▼ ▼
539
- ┌──────────────────┐ ┌─────────────────────────────────────┐
540
- 2. Lookup config │ │ 3. Get default config for company │
541
- by ID │ │ - Priority 1: isDefault=true │
542
- └────────┬─────────┘ │ - Priority 2: oldest active │
543
- │ └─────────────────────────────────────┘
544
- │ │
545
- ▼ ▼
546
- ┌─────────────────────────────────────────────────────────┐
547
- │ 4. Validate company ownership (if company feature) │
548
- │ - Throws BadRequestException if wrong company │
549
- └─────────────────────────────────────────────────────────┘
550
-
551
-
552
- ┌─────────────────────────────────────────────────────────┐
553
- │ 5. Create/reuse storage provider instance │
554
- │ - Check provider cache by config hash │
555
- │ - Create new if not cached │
556
- └─────────────────────────────────────────────────────────┘
557
-
558
-
559
- [Perform Upload]
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
+ }
560
714
  ```
561
715
 
562
- **Code Implementation:**
716
+ ### URL Generation & Refresh
563
717
 
564
- ```typescript
565
- // UploadService
566
- private async getStorageProvider(
567
- storageConfigId?: string,
568
- user?: ILoggedUserInfo,
569
- ): Promise<{ provider: IStorageProvider; config: StorageConfigBase }> {
570
- let config: StorageConfigBase | null = null;
571
-
572
- // Step 1: Try to get by ID or fallback to default
573
- if (storageConfigId) {
574
- config = await this.storageProviderConfigService.findByIdDirect(storageConfigId);
575
- }
718
+ Files automatically get presigned URLs when needed:
576
719
 
577
- if (!config) {
578
- config = await this.storageProviderConfigService.getDefaultConfig(user);
579
- }
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[] = [];
580
725
 
581
- if (!config) {
582
- throw new BadRequestException('No storage configuration available');
583
- }
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
+ );
584
733
 
585
- // Step 2: Validate company ownership
586
- if (this.storageConfigService.isCompanyFeatureEnabled() && user?.companyId) {
587
- const configWithCompany = config as StorageConfigWithCompany;
588
- if (configWithCompany.companyId && configWithCompany.companyId !== user.companyId) {
589
- throw new BadRequestException('Storage configuration belongs to another company');
590
- }
734
+ if (updatedFiles.length) {
735
+ await this.repository.save(updatedFiles);
591
736
  }
592
737
 
593
- // Step 3: Get or create provider instance
594
- const provider = await this.storageFactory.createProvider({
595
- provider: config.storage,
596
- config: config.config,
597
- });
598
-
599
- return { provider, config };
738
+ return responses;
600
739
  }
601
- ```
602
-
603
- **Key Points:**
604
- - Always specify `storageConfigId` for deterministic behavior
605
- - Without `storageConfigId`, system uses company's default config
606
- - Company ownership is always validated when company feature is enabled
607
- - Provider instances are cached for connection reuse
608
-
609
- ### Default Configuration Resolution
610
-
611
- When `storageConfigId` is not provided for uploads, the system automatically resolves the default config:
612
-
613
- 1. **Priority 1:** Find config with `isDefault: true` and `isActive: true`
614
- 2. **Priority 2:** Fall back to oldest active config
615
-
616
- ```typescript
617
- // StorageProviderConfigService
618
- async getDefaultConfig(user?: ILoggedUserInfo): Promise<StorageConfigBase | null> {
619
- await this.ensureRepositoryInitialized();
620
740
 
621
- const baseWhere: any = { isActive: true };
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);
622
744
 
623
- // Filter by company if enabled
624
- if (this.storageConfig.isCompanyFeatureEnabled() && user?.companyId) {
625
- baseWhere.companyId = user.companyId;
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;
626
749
  }
627
750
 
628
- // First try to find config marked as default
629
- const defaultConfig = await this.repository.findOne({
630
- where: { ...baseWhere, isDefault: true },
631
- order: { createdAt: 'ASC' },
632
- });
633
-
634
- if (defaultConfig) return defaultConfig;
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
+ }
635
757
 
636
- // Fall back to oldest active config
637
- return await this.repository.findOne({
638
- where: baseWhere,
639
- order: { createdAt: 'ASC' },
640
- });
758
+ return false;
641
759
  }
642
760
  ```
643
761
 
644
762
  ---
645
763
 
646
- ## File Manager
764
+ ## Folder Management
647
765
 
648
- ### FileManager Entity
766
+ ### FolderService
649
767
 
650
768
  ```typescript
651
- @Entity('file_manager')
652
- export class FileManager extends Identity {
653
- @Column({ length: 255 })
654
- name: string; // Original filename
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
+ }
780
+ ```
655
781
 
656
- @Column({ length: 100 })
657
- contentType: string; // MIME type
782
+ ### Folder DTOs
658
783
 
659
- @Column()
660
- size: number; // File size in bytes
784
+ ```typescript
785
+ export class CreateFolderDto {
786
+ @IsNotEmpty()
787
+ @IsString()
788
+ name!: string;
789
+ }
661
790
 
662
- @Column({ length: 500 })
663
- key: string; // Storage key/path
791
+ export class UpdateFolderDto extends PartialType(CreateFolderDto) {
792
+ @IsUUID()
793
+ @IsNotEmpty()
794
+ id!: string;
795
+ }
664
796
 
665
- @Column({ length: 1000, nullable: true })
666
- url: string; // Public/presigned URL
797
+ export class FolderResponseDto {
798
+ id!: string;
799
+ name!: string;
800
+ slug!: string;
801
+ }
802
+ ```
667
803
 
668
- @Column({ type: 'enum', enum: FileLocationEnum })
669
- location: FileLocationEnum;
804
+ ---
670
805
 
671
- @Column({ default: false })
672
- isPrivate: boolean;
806
+ ## Upload Service
673
807
 
674
- @Column({ nullable: true })
675
- folderId: string;
808
+ ### UploadService
676
809
 
677
- @ManyToOne(() => Folder)
678
- folder: Folder;
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
+ ```
679
856
 
680
- @Column({ nullable: true })
681
- storageConfigId: string;
857
+ ### Upload Options DTO
682
858
 
683
- @Column({ type: 'bigint', nullable: true })
684
- expiresAt: number; // URL expiration timestamp
859
+ ```typescript
860
+ export enum ImageFormat {
861
+ ORIGINAL = 'original',
862
+ JPEG = 'jpeg',
863
+ PNG = 'png',
864
+ WEBP = 'webp',
865
+ }
685
866
 
686
- @Column({ nullable: true })
687
- companyId: string; // When company feature enabled
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;
688
901
  }
689
902
  ```
690
903
 
691
- ### FileManagerService
904
+ ---
692
905
 
693
- ```typescript
694
- import { FileManagerService } from '@flusys/nestjs-storage';
906
+ ## File Validation & Security
695
907
 
696
- @Injectable()
697
- export class MyService {
698
- constructor(private readonly fileManagerService: FileManagerService) {}
699
-
700
- async manageFiles(user: ILoggedUserInfo) {
701
- // Create file record
702
- const file = await this.fileManagerService.insert(
703
- {
704
- name: 'document.pdf',
705
- contentType: 'application/pdf',
706
- size: 1024000,
707
- key: 'documents/doc-123.pdf',
708
- location: FileLocationEnum.AWS,
709
- storageConfigId: 'config-id',
710
- folderId: 'folder-id',
711
- },
712
- user,
713
- );
908
+ ### FileValidator Utility
714
909
 
715
- // Get file by ID
716
- const fileData = await this.fileManagerService.getById(file.id, user);
910
+ The `FileValidator` class provides security features to prevent file upload attacks:
717
911
 
718
- // Get files with pagination
719
- const files = await this.fileManagerService.getAll(
720
- {
721
- filter: { contentType: 'application/pdf' },
722
- pagination: { page: 1, limit: 10 },
723
- },
724
- user,
725
- );
912
+ ```typescript
913
+ export class FileValidator {
914
+ /** Detect file type from buffer using magic bytes */
915
+ static detectFileType(buffer: Buffer): string | null;
726
916
 
727
- // Get multiple files with valid URLs
728
- const filesWithUrls = await this.fileManagerService.getFiles(
729
- [{ id: 'file-1' }, { id: 'file-2' }],
730
- 'https',
731
- 'example.com',
732
- user,
733
- );
917
+ /** Check if MIME type is text-based (no magic bytes) */
918
+ static isTextBasedType(mimeType: string): boolean;
734
919
 
735
- // Delete file
736
- await this.fileManagerService.delete(
737
- {
738
- id: file.id,
739
- type: 'permanent', // Also deletes from storage
740
- },
741
- user,
742
- );
743
- }
744
- }
745
- ```
920
+ /** Check if MIME type is dangerous (HTML, JS, SVG) */
921
+ static isDangerousTextType(mimeType: string): boolean;
746
922
 
747
- ### URL Generation
923
+ /** Check if two MIME types are compatible */
924
+ static mimeTypesMatch(detected: string, declared: string): boolean;
748
925
 
749
- Files automatically get presigned URLs when needed:
926
+ /** Check if MIME type is in allowed list */
927
+ static isTypeAllowed(mimeType: string, allowedTypes: string[]): boolean;
750
928
 
751
- ```typescript
752
- // FileManagerService.getFiles()
753
- async getFiles(
754
- dtos: GetFilesRequestDto[],
755
- protocol: string,
756
- host: string,
757
- user?: ILoggedUserInfo,
758
- ): Promise<FilesResponseDto[]> {
759
- const files = await this.repository.findBy({ id: In(ids) });
760
- const now = Date.now();
929
+ /** Validate file content matches declared MIME type */
930
+ static validateFileContent(
931
+ buffer: Buffer,
932
+ declaredMimeType: string,
933
+ allowedTypes?: string[]
934
+ ): FileValidationResult;
761
935
 
762
- return Promise.all(files.map(async (file) => {
763
- // Check if URL needs regeneration
764
- const needsNewUrl =
765
- !file.url ||
766
- (file.location === FileLocationEnum.AWS && now >= file.expiresAt) ||
767
- (file.location === FileLocationEnum.AZURE && now >= file.expiresAt);
768
-
769
- if (needsNewUrl && file.storageConfigId) {
770
- // Generate presigned URL (1 hour expiry)
771
- file.url = await this.uploadService.makeFileUrl(
772
- file.key,
773
- file.storageConfigId,
774
- 3600, // seconds
775
- user,
776
- );
777
- file.expiresAt = now + 3600 * 1000;
778
- await this.repository.save(file);
779
- }
936
+ /** Sanitize filename to prevent path traversal */
937
+ static sanitizeFilename(filename: string): string;
938
+ }
780
939
 
781
- return {
782
- id: file.id,
783
- name: file.name,
784
- contentType: file.contentType,
785
- url: file.url,
786
- };
787
- }));
940
+ interface FileValidationResult {
941
+ valid: boolean;
942
+ detectedType?: string;
943
+ declaredType?: string;
944
+ message?: string;
788
945
  }
789
946
  ```
790
947
 
791
- ---
948
+ ### Supported Magic Bytes
792
949
 
793
- ## Folder Management
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 |
794
957
 
795
- ### Folder Entity
958
+ ### Dangerous File Types
796
959
 
797
- ```typescript
798
- @Entity('folders')
799
- export class Folder extends Identity {
800
- @Column({ length: 100 })
801
- name: string;
960
+ These types bypass magic-bytes validation but require **explicit allowlisting**:
802
961
 
803
- @Column({ nullable: true })
804
- parentId: string;
962
+ - `text/html`
963
+ - `application/javascript`, `text/javascript`
964
+ - `image/svg+xml`
965
+ - `application/xhtml+xml`
805
966
 
806
- @TreeParent()
807
- parent: Folder;
967
+ ### Safe Text-Based Types
808
968
 
809
- @TreeChildren()
810
- children: Folder[];
969
+ These are allowed without magic bytes:
811
970
 
812
- @Column({ nullable: true })
813
- companyId: string; // When company feature enabled
814
- }
815
- ```
971
+ - `text/plain`, `text/csv`, `text/markdown`
972
+ - `application/json`, `application/xml`
973
+ - `application/typescript`, `text/css`
816
974
 
817
- ### FolderService
975
+ ### Filename Sanitization
818
976
 
819
977
  ```typescript
820
- import { FolderService } from '@flusys/nestjs-storage';
821
-
822
- @Injectable()
823
- export class MyService {
824
- constructor(private readonly folderService: FolderService) {}
825
-
826
- async manageFolders(user: ILoggedUserInfo) {
827
- // Create root folder
828
- const documents = await this.folderService.insert(
829
- {
830
- name: 'Documents',
831
- },
832
- user,
833
- );
834
-
835
- // Create subfolder
836
- const reports = await this.folderService.insert(
837
- {
838
- name: 'Reports',
839
- parentId: documents.id,
840
- },
841
- user,
842
- );
843
-
844
- // Get folder tree
845
- const tree = await this.folderService.getTree(user);
846
-
847
- // Get folder with children
848
- const folder = await this.folderService.getById(documents.id, user);
849
-
850
- // Delete folder
851
- await this.folderService.delete(
852
- {
853
- id: reports.id,
854
- type: 'soft',
855
- },
856
- user,
857
- );
858
- }
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
859
985
  }
860
986
  ```
861
987
 
862
988
  ---
863
989
 
864
- ## Upload Service
865
-
866
- ### UploadService
867
-
868
- ```typescript
869
- import { UploadService } from '@flusys/nestjs-storage';
990
+ ## Image Compression
870
991
 
871
- @Injectable()
872
- export class MyService {
873
- constructor(private readonly uploadService: UploadService) {}
874
-
875
- async uploadFiles(files: Express.Multer.File[], user: ILoggedUserInfo) {
876
- // Upload single file
877
- const result = await this.uploadService.uploadSingleFile(
878
- files[0],
879
- {
880
- folderPath: 'documents/reports',
881
- storageConfigId: 'my-s3-config', // Optional - uses default if not provided
882
- },
883
- user,
884
- );
885
- // Returns: { name, key, size, contentType, url }
886
-
887
- // Upload multiple files
888
- const results = await this.uploadService.uploadMultipleFiles(
889
- files,
890
- {
891
- folderPath: 'images',
892
- storageConfigId: 'my-azure-config',
893
- },
894
- user,
895
- );
992
+ ### ImageCompressor Utility
896
993
 
897
- // Delete file
898
- await this.uploadService.deleteSingleFile('documents/reports/report.pdf', 'my-s3-config', user);
994
+ Uses Sharp for image processing:
899
995
 
900
- // Delete multiple files
901
- await this.uploadService.deleteMultipleFile(['file1.pdf', 'file2.pdf'], 'my-s3-config', user);
996
+ ```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
+ }
902
1004
 
903
- // Generate presigned URL
904
- const url = await this.uploadService.makeFileUrl(
905
- 'documents/report.pdf',
906
- 'my-s3-config',
907
- 3600, // expiry in seconds
908
- user,
909
- );
910
- }
1005
+ interface CompressionOptions {
1006
+ maxWidth?: number; // Default: 1280
1007
+ maxHeight?: number; // Default: 1280
1008
+ quality?: number; // Default: 85
1009
+ format?: ImageFormat; // Default: 'original'
911
1010
  }
912
1011
  ```
913
1012
 
914
- ### File Validation
915
-
916
- ```typescript
917
- // StorageConfigService
918
- validateFileSize(size: number): { valid: boolean; message?: string } {
919
- const maxSize = this.config.maxFileSize || 10 * 1024 * 1024; // 10MB default
920
- if (size > maxSize) {
921
- return {
922
- valid: false,
923
- message: `File size exceeds maximum allowed size of ${maxSize / 1024 / 1024}MB`,
924
- };
925
- }
926
- return { valid: true };
927
- }
1013
+ ### Supported Output Formats
928
1014
 
929
- validateFileType(mimeType: string): { valid: boolean; message?: string } {
930
- const allowed = this.config.allowedFileTypes || ['*/*'];
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% |
931
1024
 
932
- const isAllowed = allowed.some(pattern => {
933
- if (pattern === '*/*') return true;
934
- if (pattern.endsWith('/*')) {
935
- const type = pattern.replace('/*', '');
936
- return mimeType.startsWith(type);
937
- }
938
- return mimeType === pattern;
939
- });
1025
+ ### Usage
940
1026
 
941
- if (!isAllowed) {
942
- return {
943
- valid: false,
944
- message: `File type ${mimeType} is not allowed`,
945
- };
946
- }
947
- return { valid: true };
948
- }
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);
949
1036
  ```
950
1037
 
951
1038
  ---
952
1039
 
953
1040
  ## REST API Endpoints
954
1041
 
1042
+ All storage endpoints are prefixed with `/storage`.
1043
+
955
1044
  ### Upload Endpoints
956
1045
 
957
- | Endpoint | Method | Description |
958
- | ------------------------------ | ------ | ---------------------------------- |
959
- | `/upload/single-file` | POST | Upload single file |
960
- | `/upload/multiple-file` | POST | Upload multiple files (max 50) |
961
- | `/upload/delete-single-file` | POST | Delete single file from storage |
962
- | `/upload/delete-multiple-file` | POST | Delete multiple files |
963
- | `/upload/file/*filePath` | GET | Serve file by path (local storage) |
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 |
964
1053
 
965
1054
  ### File Manager Endpoints
966
1055
 
967
- | Endpoint | Method | Description |
968
- | ------------------------- | ------ | ---------------------------------- |
969
- | `/file-manager/insert` | POST | Create file record |
970
- | `/file-manager/get/:id` | POST | Get file by ID |
971
- | `/file-manager/get-all` | POST | Get files (paginated) |
972
- | `/file-manager/update` | POST | Update file metadata |
973
- | `/file-manager/delete` | POST | Delete file |
974
- | `/file-manager/get-files` | POST | Get multiple files with valid URLs |
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 |
975
1064
 
976
1065
  ### Folder Endpoints
977
1066
 
978
- | Endpoint | Method | Description |
979
- | ----------------- | ------ | ----------------------- |
980
- | `/folder/insert` | POST | Create folder |
981
- | `/folder/get/:id` | POST | Get folder by ID |
982
- | `/folder/get-all` | POST | Get folders (paginated) |
983
- | `/folder/tree` | GET | Get folder tree |
984
- | `/folder/update` | POST | Update folder |
985
- | `/folder/delete` | POST | Delete folder |
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` |
986
1074
 
987
1075
  ### Storage Config Endpoints
988
1076
 
989
- | Endpoint | Method | Description |
990
- | ------------------------- | ------ | ---------------------------- |
991
- | `/storage-config/insert` | POST | Create storage configuration |
992
- | `/storage-config/get/:id` | POST | Get config by ID |
993
- | `/storage-config/get-all` | POST | Get configs (paginated) |
994
- | `/storage-config/update` | POST | Update configuration |
995
- | `/storage-config/delete` | POST | Delete configuration |
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` |
996
1084
 
997
1085
  ### Upload Request Example
998
1086
 
999
1087
  ```bash
1000
1088
  # Upload single file
1001
- curl -X POST http://localhost:3000/upload/single-file \
1089
+ curl -X POST http://localhost:3000/storage/upload/single-file \
1002
1090
  -H "Authorization: Bearer <token>" \
1003
1091
  -H "Content-Type: multipart/form-data" \
1004
1092
  -F "file=@document.pdf" \
1005
- -F "folderPath=documents"
1093
+ -F "folderPath=documents" \
1094
+ -F "compress=true"
1006
1095
 
1007
1096
  # Response:
1008
1097
  {
1009
- "name": "document.pdf",
1010
- "key": "documents/abc123-document.pdf",
1011
- "size": 102.4,
1012
- "contentType": "application/pdf"
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
+ }
1013
1108
  }
1014
1109
  ```
1015
1110
 
1016
1111
  ---
1017
1112
 
1018
- ## DataSource Provider Pattern
1019
-
1020
- ### Overview
1113
+ ## File Serve Middleware
1021
1114
 
1022
- All Storage services now use the **DataSource Provider pattern** for dynamic entity loading. This ensures correct entity selection based on runtime configuration (`enableCompanyFeature`) and supports both single-database and multi-tenant modes.
1115
+ The `FileServeMiddleware` handles file serving for local storage with multiple fallback strategies:
1023
1116
 
1024
- ### Architecture
1025
-
1026
- ```
1027
- ┌─────────────────────────────┐
1028
- │ Configuration │
1029
- │ enableCompanyFeature │
1030
- └──────────┬──────────────────┘
1031
-
1032
-
1033
- ┌──────────────────────────────┐
1034
- │ StorageDataSourceProvider │
1035
- │ (REQUEST-scoped) │
1036
- │ • Extends MultiTenant... │
1037
- │ • Dynamic entity loading │
1038
- └──────────┬───────────────────┘
1039
-
1040
-
1041
- ┌─────────────────────────────────┐
1042
- │ Services (REQUEST-scoped) │
1043
- │ • FileManagerService │
1044
- │ • FolderService │
1045
- │ • StorageProviderConfigService │
1046
- └─────────────────────────────────┘
1047
- ```
1048
-
1049
- ### Key Changes
1050
-
1051
- #### Before (Static Entity Injection)
1052
1117
  ```typescript
1053
1118
  @Injectable()
1054
- export class FileManagerService {
1055
- constructor(
1056
- @InjectRepository(FileManager) repository: Repository<FileManagerBase>,
1057
- ) {
1058
- // Static injection - doesn't work with multi-tenant
1059
- }
1060
- }
1061
- ```
1062
-
1063
- #### After (Dynamic Entity Loading)
1064
- ```typescript
1065
- @Injectable({ scope: Scope.REQUEST })
1066
- export class FileManagerService {
1067
- constructor(
1068
- private readonly storageConfig: StorageConfigService,
1069
- private readonly dataSourceProvider: StorageDataSourceProvider,
1070
- ) {
1071
- super('file_manager', null as any, ...);
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
1072
1124
  }
1073
1125
 
1074
- async onModuleInit() {
1075
- // Get repository dynamically based on configuration
1076
- const enableCompanyFeature = this.storageConfig.isCompanyFeatureEnabled();
1077
- const entity = enableCompanyFeature ? FileManagerWithCompany : FileManager;
1078
- this.repository = await this.dataSourceProvider.getRepository(entity);
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
1079
1130
  }
1080
1131
  }
1081
1132
  ```
1082
1133
 
1083
- ### Benefits
1134
+ ### MIME Types for Inline Display
1084
1135
 
1085
- 1. **Dynamic Entity Selection**: Entities are chosen at runtime based on `enableCompanyFeature`
1086
- 2. **Multi-Tenant Support**: Works correctly in both single and multi-tenant modes
1087
- 3. **No TypeORM Conflicts**: Avoids "No metadata for entity" errors
1088
- 4. **Consistent Pattern**: All services follow the same approach
1089
- 5. **REQUEST-scoped**: Ensures correct entity per request in multi-tenant scenarios
1136
+ ```typescript
1137
+ const VIEWABLE_TYPE_PREFIXES = [
1138
+ 'image/',
1139
+ 'video/',
1140
+ 'audio/',
1141
+ 'text/',
1142
+ 'application/pdf',
1143
+ 'application/json',
1144
+ 'application/xml',
1145
+ ];
1146
+ ```
1090
1147
 
1091
- ### Affected Services
1148
+ ### Response Headers
1092
1149
 
1093
- All Storage services now use this pattern:
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
+ ```
1094
1162
 
1095
- - **FileManagerService** - Uses `FileManager` or `FileManagerWithCompany`
1096
- - **FolderService** - Uses `Folder` or `FolderWithCompany`
1097
- - **StorageProviderConfigService** - Uses `StorageConfig` or `StorageConfigWithCompany`
1163
+ ---
1098
1164
 
1099
- ### Module Configuration
1165
+ ## DataSource Provider Pattern
1100
1166
 
1101
- Repository providers have been removed from `StorageModule`:
1167
+ ### StorageDataSourceProvider
1102
1168
 
1103
1169
  ```typescript
1104
- // OLD: Static repository providers (removed)
1105
- getRepositoryProviders() {
1106
- return entities.map(entity => ({
1107
- provide: getRepositoryToken(entity),
1108
- useFactory: ...
1109
- }));
1110
- }
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;
1111
1175
 
1112
- // NEW: Services get repositories dynamically at runtime
1113
- providers: [
1114
- StorageConfigService,
1115
- StorageDataSourceProvider,
1116
- FileManagerService,
1117
- FolderService,
1118
- StorageProviderConfigService,
1119
- ...
1120
- ]
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
+ }
1121
1182
  ```
1122
1183
 
1123
- ### Isolated DataSource Cache
1184
+ ### Dynamic Entity Selection
1124
1185
 
1125
- Each module (Auth, IAM, Storage) maintains isolated datasource caches by overriding both static properties AND methods (`getSingleDataSource`, `getOrCreateTenantConnection`) in `StorageDataSourceProvider`. This prevents "No metadata for entity" errors regardless of module initialization order.
1186
+ Services select entities at runtime:
1126
1187
 
1127
- **Key Points:**
1128
- - Isolated cache per module - no shared state between Auth/IAM/Storage
1129
- - Must override methods that access static properties, not just the properties
1130
- - Use explicit subclass reference (e.g., `StorageDataSourceProvider.singleDataSource`)
1131
- - Required when module has entity variants based on `enableCompanyFeature`
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
+ }
1132
1196
 
1133
- See [storage-datasource.provider.ts](../projects/nestjs-storage/src/services/storage-datasource.provider.ts) for implementation.
1197
+ protected getDataSourceProvider() {
1198
+ return this.dataSourceProvider;
1199
+ }
1200
+ }
1201
+ ```
1134
1202
 
1135
1203
  ---
1136
1204
 
1137
1205
  ## Multi-Tenant Support
1138
1206
 
1139
- ### Company Isolation
1207
+ ### Company Filtering
1140
1208
 
1141
1209
  When company feature is enabled:
1142
1210
 
1143
- 1. **Storage Configs** - Each company has its own storage configurations
1144
- 2. **Files** - Files are tagged with companyId
1145
- 3. **Folders** - Folders are company-scoped
1146
- 4. **Access Control** - Users can only access their company's files
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);
1147
1219
 
1148
- ### Company Filtering
1220
+ await this.applyPrivateFileFilter(query, user);
1221
+
1222
+ return result;
1223
+ }
1224
+ ```
1225
+
1226
+ ### Private File Access
1149
1227
 
1150
1228
  ```typescript
1151
- // FileManagerService
1152
- override async getExtraManipulateQuery(
1153
- query: SelectQueryBuilder<FileManager>,
1154
- dto: FilterAndPaginationDto,
1155
- user: ILoggedUserInfo,
1156
- ) {
1157
- // Filter by company
1158
- if (this.storageConfig.isCompanyFeatureEnabled() && user.companyId) {
1159
- query.andWhere(`file_manager.companyId = :companyId`, {
1160
- companyId: user.companyId,
1161
- });
1229
+ private async applyPrivateFileFilter(query, user): Promise<void> {
1230
+ if (!user) {
1231
+ query.andWhere('file_manager.isPrivate = :isPrivate', { isPrivate: false });
1232
+ return;
1162
1233
  }
1163
1234
 
1164
- // Filter private files by permission
1165
- const userActions = await this.cacheManager.get(
1166
- `user_action_permission_${user.id}_${user.companyId}`
1167
- );
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');
1168
1238
 
1169
- if (!userActions?.includes('storage.file.viewPrivate')) {
1239
+ if (!canViewPrivate) {
1170
1240
  query.andWhere('file_manager.isPrivate = :isPrivate', { isPrivate: false });
1171
1241
  }
1172
-
1173
- return { query, isRaw: false };
1174
1242
  }
1175
1243
  ```
1176
1244
 
1177
- ### Company-Scoped Storage Config
1245
+ ### Storage Config Validation
1178
1246
 
1179
1247
  ```typescript
1180
- // StorageProviderConfigService
1181
- async getDefaultConfig(user?: ILoggedUserInfo): Promise<StorageConfigBase | null> {
1182
- const where: any = {};
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
+ );
1183
1262
 
1184
- // Filter by company
1185
- if (this.storageConfig.isCompanyFeatureEnabled() && user?.companyId) {
1186
- where.companyId = user.companyId;
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;
1187
1270
  }
1188
1271
 
1189
- return await this.repository.findOne({
1190
- where: { ...where, name: 'default' },
1191
- });
1272
+ return {
1273
+ provider: await this.createProviderFromConfig(storageConfig),
1274
+ location: storageConfig.storage,
1275
+ configId: storageConfig.id,
1276
+ };
1192
1277
  }
1193
1278
  ```
1194
1279
 
1195
- ### Storage Config Validation
1280
+ ---
1281
+
1282
+ ## Swagger Configuration
1283
+
1284
+ ### Using storageSwaggerConfig
1196
1285
 
1197
1286
  ```typescript
1198
- // UploadService.getStorageProvider()
1199
- private async getStorageProvider(
1200
- storageConfigId?: string,
1201
- user?: ILoggedUserInfo,
1202
- ): Promise<IStorageProvider> {
1203
- const config = await this.storageProviderConfigService.findById(storageConfigId, user);
1204
-
1205
- // Validate company ownership
1206
- if (this.storageConfigService.isCompanyFeatureEnabled() && user?.companyId) {
1207
- if (config.companyId && config.companyId !== user.companyId) {
1208
- throw new BadRequestException('Storage configuration belongs to another company');
1209
- }
1210
- }
1287
+ import { storageSwaggerConfig } from '@flusys/nestjs-storage/docs';
1211
1288
 
1212
- return await this.storageFactory.createProvider({
1213
- provider: config.storage,
1214
- config: config.config,
1215
- });
1216
- }
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
+ // }
1217
1304
  ```
1218
1305
 
1219
1306
  ---
@@ -1226,20 +1313,15 @@ private async getStorageProvider(
1226
1313
  // ✅ Create meaningful config names
1227
1314
  await storageConfigService.insert({
1228
1315
  name: 'default', // Primary storage
1229
- storage: FileLocationEnum.AWS,
1230
- config: { bucket: 'main-files', ... }
1316
+ storage: 'aws',
1317
+ config: { bucket: 'main-files', ... },
1318
+ isDefault: true,
1231
1319
  }, user);
1232
1320
 
1233
1321
  await storageConfigService.insert({
1234
1322
  name: 'media', // Images/videos
1235
- storage: FileLocationEnum.AWS,
1236
- config: { bucket: 'media-files', ... }
1237
- }, user);
1238
-
1239
- await storageConfigService.insert({
1240
- name: 'backups', // Backup storage
1241
- storage: FileLocationEnum.SFTP,
1242
- config: { host: 'backup.server.com', ... }
1323
+ storage: 'aws',
1324
+ config: { bucket: 'media-files', ... },
1243
1325
  }, user);
1244
1326
 
1245
1327
  // ❌ Don't use generic names
@@ -1249,76 +1331,34 @@ await storageConfigService.insert({
1249
1331
  });
1250
1332
  ```
1251
1333
 
1252
- ### 2. Organize Files in Folders
1253
-
1254
- ```typescript
1255
- // ✅ Create logical folder structure
1256
- const docs = await folderService.insert({ name: 'Documents' }, user);
1257
- const reports = await folderService.insert({ name: 'Reports', parentId: docs.id }, user);
1258
- const invoices = await folderService.insert({ name: 'Invoices', parentId: docs.id }, user);
1259
-
1260
- // Upload with folder association
1261
- await uploadService.uploadSingleFile(
1262
- file,
1263
- {
1264
- folderPath: 'Documents/Reports',
1265
- // Files will be stored in organized structure
1266
- },
1267
- user,
1268
- );
1269
-
1270
- // ❌ Don't dump all files in root
1271
- ```
1272
-
1273
- ### 3. Set Appropriate File Validation
1334
+ ### 2. Configure File Validation
1274
1335
 
1275
1336
  ```typescript
1276
- // ✅ Configure validation per use case
1337
+ // ✅ Restrict file types in production
1277
1338
  StorageModule.forRoot({
1278
1339
  config: {
1279
1340
  maxFileSize: 10 * 1024 * 1024, // 10MB
1280
1341
  allowedFileTypes: [
1281
1342
  'image/jpeg',
1282
1343
  'image/png',
1283
- 'image/gif',
1344
+ 'image/webp',
1284
1345
  'application/pdf',
1285
- 'text/plain',
1286
- 'text/csv',
1287
1346
  ],
1288
1347
  },
1289
1348
  });
1290
1349
 
1291
- // ❌ Don't allow all file types in production
1350
+ // ❌ Don't allow all file types
1292
1351
  allowedFileTypes: ['*/*'];
1293
1352
  ```
1294
1353
 
1295
- ### 4. Use @CurrentUser Decorator
1296
-
1297
- ```typescript
1298
- // ✅ Use decorator for user context
1299
- @Post('upload')
1300
- async upload(
1301
- @UploadedFile() file: Express.Multer.File,
1302
- @CurrentUser() user: ILoggedUserInfo,
1303
- ) {
1304
- return this.uploadService.uploadSingleFile(file, {}, user);
1305
- }
1306
-
1307
- // ❌ Don't extract user manually
1308
- @Post('upload')
1309
- async upload(@Req() req: Request) {
1310
- const user = req.user; // Not type-safe
1311
- }
1312
- ```
1313
-
1314
- ### 5. Handle Presigned URL Expiration
1354
+ ### 3. Use Presigned URLs Properly
1315
1355
 
1316
1356
  ```typescript
1317
1357
  // ✅ Always use getFiles() for URL access
1318
1358
  const files = await fileManagerService.getFiles(
1319
1359
  [{ id: 'file-1' }, { id: 'file-2' }],
1320
- protocol,
1321
- host,
1360
+ req.protocol,
1361
+ req.get('host'),
1322
1362
  user,
1323
1363
  );
1324
1364
  // URLs are automatically refreshed if expired
@@ -1328,26 +1368,33 @@ const file = await fileManagerService.getById(id, user);
1328
1368
  return file.url; // May be expired!
1329
1369
  ```
1330
1370
 
1331
- ### 6. Clean Up on File Deletion
1371
+ ### 4. Handle File Deletion Properly
1332
1372
 
1333
1373
  ```typescript
1334
1374
  // ✅ Use permanent delete to remove from storage
1335
- await fileManagerService.delete(
1336
- {
1337
- id: fileId,
1338
- type: 'permanent', // Deletes file from storage provider
1339
- },
1340
- user,
1341
- );
1375
+ await fileManagerService.delete({
1376
+ id: fileId,
1377
+ type: 'permanent', // Deletes from storage provider
1378
+ }, user);
1342
1379
 
1343
- // Soft delete keeps file in storage (useful for recovery)
1344
- await fileManagerService.delete(
1345
- {
1346
- id: fileId,
1347
- type: 'soft', // Only marks as deleted in database
1348
- },
1349
- user,
1350
- );
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
+ ```
1386
+
1387
+ ### 5. Optimize Image Uploads
1388
+
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);
1351
1398
  ```
1352
1399
 
1353
1400
  ---
@@ -1366,6 +1413,7 @@ import {
1366
1413
  FileManagerService,
1367
1414
  FolderService,
1368
1415
  StorageProviderConfigService,
1416
+ StorageConfigService,
1369
1417
  StorageDataSourceProvider,
1370
1418
  } from '@flusys/nestjs-storage/services';
1371
1419
 
@@ -1380,19 +1428,29 @@ import {
1380
1428
  StorageConfig,
1381
1429
  StorageConfigBase,
1382
1430
  StorageConfigWithCompany,
1431
+ StorageCoreEntities,
1432
+ StorageCompanyEntities,
1433
+ getStorageEntitiesByConfig,
1383
1434
  } from '@flusys/nestjs-storage/entities';
1384
1435
 
1385
1436
  // DTOs
1386
1437
  import {
1387
1438
  UploadOptionsDto,
1439
+ DeleteSingleFileDto,
1440
+ DeleteMultipleFileDto,
1441
+ FileUploadResponsePayloadDto,
1388
1442
  CreateFileManagerDto,
1389
1443
  UpdateFileManagerDto,
1444
+ FileManagerResponseDto,
1390
1445
  GetFilesRequestDto,
1391
1446
  FilesResponseDto,
1392
1447
  CreateFolderDto,
1393
1448
  UpdateFolderDto,
1449
+ FolderResponseDto,
1394
1450
  CreateStorageConfigDto,
1395
1451
  UpdateStorageConfigDto,
1452
+ StorageConfigResponseDto,
1453
+ ImageFormat,
1396
1454
  } from '@flusys/nestjs-storage/dtos';
1397
1455
 
1398
1456
  // Interfaces
@@ -1406,6 +1464,7 @@ import {
1406
1464
  IUploadedFileInfo,
1407
1465
  StorageModuleOptions,
1408
1466
  StorageModuleAsyncOptions,
1467
+ StorageOptionsFactory,
1409
1468
  } from '@flusys/nestjs-storage/interfaces';
1410
1469
 
1411
1470
  // Enums
@@ -1418,8 +1477,21 @@ import {
1418
1477
  LocalProvider,
1419
1478
  } from '@flusys/nestjs-storage/providers';
1420
1479
 
1421
- // Config
1422
- import { StorageConfigService } from '@flusys/nestjs-storage/config';
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';
1423
1495
  ```
1424
1496
 
1425
1497
  ---
@@ -1429,15 +1501,17 @@ import { StorageConfigService } from '@flusys/nestjs-storage/config';
1429
1501
  The `@flusys/nestjs-storage` package provides:
1430
1502
 
1431
1503
  - **Multiple Providers** - Local, AWS S3, Azure Blob, SFTP
1432
- - **Connection Reuse** - Efficient provider instance caching
1433
- - **File Validation** - Size and type validation
1434
- - **Folder Hierarchy** - Nested folder structure
1435
- - **Presigned URLs** - Secure time-limited access
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
1436
1510
  - **Multi-Tenant** - Company/branch file isolation
1437
1511
  - **Per-Company Storage** - Different providers per company
1438
- - **REST API** - Complete CRUD endpoints
1439
- - **Auto URL Refresh** - Automatic presigned URL regeneration
1512
+ - **REST API** - Complete CRUD endpoints with POST-only RPC
1513
+ - **Middleware** - File serving with multiple fallback strategies
1440
1514
 
1441
1515
  ---
1442
1516
 
1443
- **Last Updated:** 2026-02-21
1517
+ **Last Updated:** 2026-02-25