@flusys/nestjs-storage 1.1.0-beta → 2.0.0-rc.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.
Files changed (118) hide show
  1. package/README.md +148 -6
  2. package/cjs/config/index.js +0 -1
  3. package/cjs/config/storage.constants.js +0 -17
  4. package/cjs/controllers/file-manager.controller.js +44 -1
  5. package/cjs/controllers/folder.controller.js +44 -1
  6. package/cjs/controllers/storage-config.controller.js +44 -1
  7. package/cjs/controllers/upload.controller.js +18 -29
  8. package/cjs/docs/storage-swagger.config.js +24 -136
  9. package/cjs/dtos/file-manager.dto.js +70 -34
  10. package/cjs/dtos/folder.dto.js +15 -9
  11. package/cjs/dtos/storage-config.dto.js +4 -85
  12. package/cjs/dtos/upload.dto.js +24 -17
  13. package/cjs/entities/file-manager-with-company.entity.js +3 -4
  14. package/cjs/entities/file-manager.entity.js +71 -3
  15. package/cjs/entities/folder-with-company.entity.js +3 -4
  16. package/cjs/entities/folder.entity.js +19 -3
  17. package/cjs/entities/index.js +9 -10
  18. package/cjs/entities/storage-config-with-company.entity.js +3 -4
  19. package/cjs/entities/storage-config.entity.js +74 -3
  20. package/cjs/interfaces/index.js +0 -1
  21. package/cjs/middlewares/file-serve.middleware.js +113 -100
  22. package/cjs/modules/storage.module.js +82 -136
  23. package/cjs/providers/azure-provider.optional.js +10 -38
  24. package/cjs/providers/local-provider.js +38 -31
  25. package/cjs/providers/s3-provider.optional.js +19 -40
  26. package/cjs/providers/storage-factory.service.js +54 -99
  27. package/cjs/providers/storage-provider.registry.js +8 -18
  28. package/cjs/services/file-manager.service.js +238 -323
  29. package/cjs/services/folder.service.js +8 -11
  30. package/cjs/services/index.js +1 -0
  31. package/cjs/{config → services}/storage-config.service.js +32 -76
  32. package/cjs/services/storage-datasource.provider.js +16 -26
  33. package/cjs/services/storage-provider-config.service.js +15 -37
  34. package/cjs/services/upload.service.js +72 -88
  35. package/cjs/utils/file-validator.util.js +458 -0
  36. package/cjs/utils/image-compressor.util.js +3 -8
  37. package/config/index.d.ts +0 -1
  38. package/config/storage.constants.d.ts +0 -8
  39. package/controllers/upload.controller.d.ts +3 -6
  40. package/dtos/file-manager.dto.d.ts +12 -5
  41. package/dtos/folder.dto.d.ts +5 -5
  42. package/dtos/storage-config.dto.d.ts +7 -13
  43. package/entities/file-manager-with-company.entity.d.ts +2 -2
  44. package/entities/file-manager.entity.d.ts +12 -2
  45. package/entities/folder-with-company.entity.d.ts +2 -2
  46. package/entities/folder.entity.d.ts +4 -2
  47. package/entities/index.d.ts +3 -4
  48. package/entities/storage-config-with-company.entity.d.ts +2 -2
  49. package/entities/storage-config.entity.d.ts +8 -2
  50. package/fesm/config/index.js +0 -1
  51. package/fesm/config/storage.constants.js +0 -8
  52. package/fesm/controllers/file-manager.controller.js +45 -2
  53. package/fesm/controllers/folder.controller.js +45 -2
  54. package/fesm/controllers/storage-config.controller.js +45 -2
  55. package/fesm/controllers/upload.controller.js +19 -30
  56. package/fesm/docs/storage-swagger.config.js +27 -142
  57. package/fesm/dtos/file-manager.dto.js +71 -35
  58. package/fesm/dtos/folder.dto.js +16 -10
  59. package/fesm/dtos/storage-config.dto.js +8 -95
  60. package/fesm/dtos/upload.dto.js +25 -19
  61. package/fesm/entities/file-manager-with-company.entity.js +3 -4
  62. package/fesm/entities/file-manager.entity.js +72 -4
  63. package/fesm/entities/folder-with-company.entity.js +3 -4
  64. package/fesm/entities/folder.entity.js +20 -4
  65. package/fesm/entities/index.js +5 -13
  66. package/fesm/entities/storage-config-with-company.entity.js +3 -4
  67. package/fesm/entities/storage-config.entity.js +75 -4
  68. package/fesm/interfaces/index.js +0 -1
  69. package/fesm/interfaces/storage-config.interface.js +1 -3
  70. package/fesm/middlewares/file-serve.middleware.js +114 -101
  71. package/fesm/modules/storage.module.js +83 -136
  72. package/fesm/providers/azure-provider.optional.js +11 -42
  73. package/fesm/providers/local-provider.js +38 -31
  74. package/fesm/providers/s3-provider.optional.js +20 -44
  75. package/fesm/providers/storage-factory.service.js +52 -97
  76. package/fesm/providers/storage-provider.registry.js +10 -20
  77. package/fesm/services/file-manager.service.js +237 -322
  78. package/fesm/services/folder.service.js +6 -9
  79. package/fesm/services/index.js +1 -0
  80. package/fesm/{config → services}/storage-config.service.js +32 -76
  81. package/fesm/services/storage-datasource.provider.js +16 -26
  82. package/fesm/services/storage-provider-config.service.js +13 -35
  83. package/fesm/services/upload.service.js +71 -87
  84. package/fesm/utils/file-validator.util.js +451 -0
  85. package/fesm/utils/image-compressor.util.js +3 -8
  86. package/interfaces/file-manager.interface.d.ts +7 -4
  87. package/interfaces/index.d.ts +0 -1
  88. package/interfaces/storage-config.interface.d.ts +0 -20
  89. package/interfaces/storage-module-options.interface.d.ts +0 -5
  90. package/middlewares/file-serve.middleware.d.ts +9 -1
  91. package/modules/storage.module.d.ts +1 -2
  92. package/package.json +6 -6
  93. package/providers/azure-provider.optional.d.ts +8 -6
  94. package/providers/local-provider.d.ts +2 -7
  95. package/providers/s3-provider.optional.d.ts +9 -7
  96. package/providers/storage-factory.service.d.ts +8 -9
  97. package/providers/storage-provider.registry.d.ts +4 -4
  98. package/services/file-manager.service.d.ts +23 -16
  99. package/services/folder.service.d.ts +4 -4
  100. package/services/index.d.ts +1 -0
  101. package/services/storage-config.service.d.ts +24 -0
  102. package/services/storage-datasource.provider.d.ts +3 -4
  103. package/services/storage-provider-config.service.d.ts +4 -4
  104. package/services/upload.service.d.ts +3 -2
  105. package/utils/file-validator.util.d.ts +19 -0
  106. package/cjs/entities/file-manager-base.entity.js +0 -115
  107. package/cjs/entities/folder-base.entity.js +0 -55
  108. package/cjs/entities/storage-config-base.entity.js +0 -93
  109. package/cjs/interfaces/file-upload-response.interface.js +0 -4
  110. package/config/storage-config.service.d.ts +0 -22
  111. package/entities/file-manager-base.entity.d.ts +0 -13
  112. package/entities/folder-base.entity.d.ts +0 -5
  113. package/entities/storage-config-base.entity.d.ts +0 -9
  114. package/fesm/entities/file-manager-base.entity.js +0 -108
  115. package/fesm/entities/folder-base.entity.js +0 -48
  116. package/fesm/entities/storage-config-base.entity.js +0 -83
  117. package/fesm/interfaces/file-upload-response.interface.js +0 -1
  118. package/interfaces/file-upload-response.interface.d.ts +0 -6
@@ -5,9 +5,9 @@ function _ts_decorate(decorators, target, key, desc) {
5
5
  return c > 3 && r && Object.defineProperty(target, key, r), r;
6
6
  }
7
7
  import { CacheModule, UtilsModule } from '@flusys/nestjs-shared/modules';
8
- import { Module, Logger, RequestMethod } from '@nestjs/common';
8
+ import { Logger, Module, RequestMethod } from '@nestjs/common';
9
9
  import { FileServeMiddleware } from '../middlewares';
10
- import { StorageConfigService } from '../config';
10
+ import { StorageConfigService } from '../services';
11
11
  import { STORAGE_MODULE_OPTIONS } from '../config/storage.constants';
12
12
  import { FileManagerController, FolderController, StorageConfigController, UploadController } from '../controllers';
13
13
  import { FileLocationEnum } from '../enums/file-location.enum';
@@ -15,48 +15,62 @@ import { StorageFactoryService, StorageProviderRegistry } from '../providers';
15
15
  import { LocalProvider } from '../providers/local-provider';
16
16
  import { FileManagerService, FolderService, StorageDataSourceProvider, UploadService } from '../services';
17
17
  import { StorageProviderConfigService } from '../services/storage-provider-config.service';
18
- // Auto-register built-in storage providers
18
+ // ─── Module Constants ─────────────────────────────────────────────────────────
19
19
  const logger = new Logger('StorageModule');
20
- // Register LocalProvider (always available - uses Node.js built-in fs)
20
+ const CONTROLLERS = [
21
+ FileManagerController,
22
+ FolderController,
23
+ StorageConfigController,
24
+ UploadController
25
+ ];
26
+ const EXPORTED_SERVICES = [
27
+ StorageConfigService,
28
+ StorageDataSourceProvider,
29
+ FileManagerService,
30
+ FolderService,
31
+ StorageProviderConfigService,
32
+ UploadService,
33
+ StorageFactoryService
34
+ ];
35
+ // ─── Provider Registration ────────────────────────────────────────────────────
21
36
  StorageProviderRegistry.register(FileLocationEnum.LOCAL, LocalProvider);
22
37
  logger.log('Registered LocalProvider');
23
- // Try to register optional providers (only if dependencies are installed)
24
- try {
25
- const { S3Provider } = require('../providers/s3-provider.optional');
26
- StorageProviderRegistry.register(FileLocationEnum.AWS, S3Provider);
27
- logger.log('Registered S3Provider');
28
- } catch {
29
- logger.debug('S3Provider not available (install @aws-sdk/client-s3 to enable)');
30
- }
31
- try {
32
- const { AzureProvider } = require('../providers/azure-provider.optional');
33
- StorageProviderRegistry.register(FileLocationEnum.AZURE, AzureProvider);
34
- logger.log('Registered AzureProvider');
35
- } catch {
36
- logger.debug('AzureProvider not available (install @azure/storage-blob to enable)');
37
- }
38
- try {
39
- const { SftpProvider } = require('../providers/sftp-provider.optional');
40
- StorageProviderRegistry.register(FileLocationEnum.SFTP, SftpProvider);
41
- logger.log('Registered SftpProvider');
42
- } catch {
43
- logger.debug('SftpProvider not available (install ssh2-sftp-client to enable)');
38
+ const OPTIONAL_PROVIDERS = [
39
+ {
40
+ location: FileLocationEnum.AWS,
41
+ path: '../providers/s3-provider.optional',
42
+ name: 'S3Provider',
43
+ dep: '@aws-sdk/client-s3'
44
+ },
45
+ {
46
+ location: FileLocationEnum.AZURE,
47
+ path: '../providers/azure-provider.optional',
48
+ name: 'AzureProvider',
49
+ dep: '@azure/storage-blob'
50
+ },
51
+ {
52
+ location: FileLocationEnum.SFTP,
53
+ path: '../providers/sftp-provider.optional',
54
+ name: 'SftpProvider',
55
+ dep: 'ssh2-sftp-client'
56
+ }
57
+ ];
58
+ for (const { location, path, name, dep } of OPTIONAL_PROVIDERS){
59
+ try {
60
+ StorageProviderRegistry.register(location, require(path)[name]);
61
+ logger.log(`Registered ${name}`);
62
+ } catch {
63
+ logger.debug(`${name} not available (install ${dep} to enable)`);
64
+ }
44
65
  }
45
66
  export class StorageModule {
46
- /**
47
- * Configure middleware for file serving
48
- * This bypasses path-to-regexp issues with wildcard routes
49
- */ configure(consumer) {
67
+ configure(consumer) {
50
68
  consumer.apply(FileServeMiddleware).forRoutes({
51
69
  path: 'storage/upload/file/*',
52
70
  method: RequestMethod.GET
53
71
  });
54
72
  }
55
- /**
56
- * Register StorageModule synchronously
57
- */ static forRoot(options) {
58
- const controllers = this.getControllers(options);
59
- const providers = this.getProviders(options);
73
+ static forRoot(options) {
60
74
  return {
61
75
  module: StorageModule,
62
76
  global: options.global ?? false,
@@ -64,24 +78,12 @@ export class StorageModule {
64
78
  CacheModule,
65
79
  UtilsModule
66
80
  ],
67
- controllers: options.includeController !== false ? controllers : [],
68
- providers,
69
- exports: [
70
- StorageConfigService,
71
- StorageDataSourceProvider,
72
- FileManagerService,
73
- FolderService,
74
- StorageProviderConfigService,
75
- UploadService,
76
- StorageFactoryService
77
- ]
81
+ controllers: options.includeController !== false ? CONTROLLERS : [],
82
+ providers: this.buildProviders(options),
83
+ exports: EXPORTED_SERVICES
78
84
  };
79
85
  }
80
- /**
81
- * Register StorageModule asynchronously
82
- */ static forRootAsync(options) {
83
- const controllers = this.getControllers(options);
84
- const asyncProviders = this.createAsyncProviders(options);
86
+ static forRootAsync(options) {
85
87
  return {
86
88
  module: StorageModule,
87
89
  global: options.global ?? false,
@@ -90,53 +92,21 @@ export class StorageModule {
90
92
  CacheModule,
91
93
  UtilsModule
92
94
  ],
93
- controllers: options.includeController !== false ? controllers : [],
94
- // Pass false to exclude STORAGE_MODULE_OPTIONS - it's already in asyncProviders
95
+ controllers: options.includeController !== false ? CONTROLLERS : [],
95
96
  providers: [
96
- ...asyncProviders,
97
- ...this.getProviders(options, false)
97
+ ...this.createAsyncProviders(options),
98
+ ...this.buildProviders()
98
99
  ],
99
- exports: [
100
- StorageConfigService,
101
- StorageDataSourceProvider,
102
- FileManagerService,
103
- FolderService,
104
- StorageProviderConfigService,
105
- UploadService,
106
- StorageFactoryService
107
- ]
100
+ exports: EXPORTED_SERVICES
108
101
  };
109
102
  }
110
- // Private Helper Methods
111
- /** Get controllers (all controllers always loaded) */ static getControllers(options) {
112
- return [
113
- FileManagerController,
114
- FolderController,
115
- StorageConfigController,
116
- UploadController
117
- ];
118
- }
119
- /**
120
- * NOTE: Repository providers removed - services now use StorageDataSourceProvider directly
121
- * This ensures dynamic entity loading based on runtime configuration
122
- */ /**
123
- * Get providers (all providers always loaded)
124
- * @param options Module options
125
- * @param includeOptionsProvider Whether to include the STORAGE_MODULE_OPTIONS provider (false for async registration)
126
- */ static getProviders(options, includeOptionsProvider = true) {
103
+ // ─── Private Helpers ──────────────────────────────────────────────────────────
104
+ static buildProviders(options) {
127
105
  const providers = [
128
- StorageConfigService,
129
- StorageDataSourceProvider,
130
- FileManagerService,
131
- FolderService,
132
- StorageProviderConfigService,
133
- UploadService,
134
- StorageFactoryService,
106
+ ...EXPORTED_SERVICES,
135
107
  FileServeMiddleware
136
108
  ];
137
- // Only include options provider for sync registration
138
- // For async registration, createAsyncProviders handles it
139
- if (includeOptionsProvider) {
109
+ if (options) {
140
110
  providers.unshift({
141
111
  provide: STORAGE_MODULE_OPTIONS,
142
112
  useValue: options
@@ -144,63 +114,40 @@ export class StorageModule {
144
114
  }
145
115
  return providers;
146
116
  }
147
- /**
148
- * Create async providers for forRootAsync
149
- */ static createAsyncProviders(options) {
117
+ static createAsyncProviders(options) {
150
118
  if (options.useFactory) {
151
119
  return [
152
120
  {
153
121
  provide: STORAGE_MODULE_OPTIONS,
154
- useFactory: async (...args)=>{
155
- const config = await options.useFactory(...args);
156
- return {
122
+ useFactory: async (...args)=>({
157
123
  ...options,
158
- config
159
- };
160
- },
124
+ config: await options.useFactory(...args)
125
+ }),
161
126
  inject: options.inject || []
162
127
  }
163
128
  ];
164
129
  }
130
+ const factoryClass = options.useClass || options.useExisting;
131
+ if (!factoryClass) return [];
132
+ const providers = [
133
+ {
134
+ provide: STORAGE_MODULE_OPTIONS,
135
+ useFactory: async (factory)=>({
136
+ ...options,
137
+ config: await factory.createStorageOptions()
138
+ }),
139
+ inject: [
140
+ factoryClass
141
+ ]
142
+ }
143
+ ];
165
144
  if (options.useClass) {
166
- return [
167
- {
168
- provide: STORAGE_MODULE_OPTIONS,
169
- useFactory: async (optionsFactory)=>{
170
- const config = await optionsFactory.createStorageOptions();
171
- return {
172
- ...options,
173
- config
174
- };
175
- },
176
- inject: [
177
- options.useClass
178
- ]
179
- },
180
- {
181
- provide: options.useClass,
182
- useClass: options.useClass
183
- }
184
- ];
185
- }
186
- if (options.useExisting) {
187
- return [
188
- {
189
- provide: STORAGE_MODULE_OPTIONS,
190
- useFactory: async (optionsFactory)=>{
191
- const config = await optionsFactory.createStorageOptions();
192
- return {
193
- ...options,
194
- config
195
- };
196
- },
197
- inject: [
198
- options.useExisting
199
- ]
200
- }
201
- ];
145
+ providers.push({
146
+ provide: options.useClass,
147
+ useClass: options.useClass
148
+ });
202
149
  }
203
- return [];
150
+ return providers;
204
151
  }
205
152
  }
206
153
  StorageModule = _ts_decorate([
@@ -1,17 +1,6 @@
1
1
  /**
2
- * OPTIONAL Azure Blob Storage Provider
3
- *
4
- * This provider requires @azure/storage-blob package to be installed.
5
- * Only import this if you need Azure Blob storage.
6
- *
7
- * Installation:
8
- * npm install @azure/storage-blob
9
- *
10
- * Usage:
11
- * import { AzureProvider } from '@flusys/nestjs-storage/providers/azure-provider.optional';
12
- * import { StorageProviderRegistry, FileLocationEnum } from '@flusys/nestjs-storage';
13
- *
14
- * StorageProviderRegistry.register(FileLocationEnum.AZURE, AzureProvider);
2
+ * Optional Azure Blob Storage Provider
3
+ * Requires: npm install @azure/storage-blob
15
4
  */ function _define_property(obj, key, value) {
16
5
  if (key in obj) {
17
6
  Object.defineProperty(obj, key, {
@@ -25,44 +14,27 @@
25
14
  }
26
15
  return obj;
27
16
  }
28
- import { ImageCompressor } from '../utils/image-compressor.util';
29
17
  import { Logger } from '@nestjs/common';
30
18
  import { v4 as uuidv4 } from 'uuid';
31
- /**
32
- * Azure Blob Storage Provider
33
- * Requires @azure/storage-blob package to be installed
34
- */ export class AzureProvider {
35
- /**
36
- * Initialize Azure provider with configuration
37
- */ async initialize(config) {
19
+ import { ImageCompressor } from '../utils/image-compressor.util';
20
+ export class AzureProvider {
21
+ async initialize(config) {
38
22
  try {
39
- // Dynamic import - only loads Azure SDK if this provider is used
40
- const { BlobServiceClient } = await import('@azure/storage-blob');
23
+ const { BlobServiceClient, StorageSharedKeyCredential } = await import('@azure/storage-blob');
41
24
  this.accountName = config.accountName;
42
25
  this.containerName = config.containerName;
43
- // Create BlobServiceClient
44
- if (config.connectionString) {
45
- this.blobServiceClient = BlobServiceClient.fromConnectionString(config.connectionString);
46
- } else {
47
- const { StorageSharedKeyCredential } = await import('@azure/storage-blob');
48
- const sharedKeyCredential = new StorageSharedKeyCredential(config.accountName, config.accountKey);
49
- this.blobServiceClient = new BlobServiceClient(`https://${config.accountName}.blob.core.windows.net`, sharedKeyCredential);
50
- }
51
- // Get container client
26
+ this.blobServiceClient = config.connectionString ? BlobServiceClient.fromConnectionString(config.connectionString) : new BlobServiceClient(`https://${config.accountName}.blob.core.windows.net`, new StorageSharedKeyCredential(config.accountName, config.accountKey));
52
27
  this.containerClient = this.blobServiceClient.getContainerClient(config.containerName);
53
- // Ensure container exists
54
28
  await this.containerClient.createIfNotExists();
55
29
  this.logger.log(`Azure Provider initialized: account=${config.accountName}, container=${config.containerName}`);
56
- } catch (_error) {
57
- this.logger.error('Failed to initialize AzureProvider. Make sure @azure/storage-blob is installed.');
58
- throw new Error('Azure Storage SDK not found. Install it with: npm install @azure/storage-blob');
30
+ } catch {
31
+ throw new Error('Azure Storage SDK not found. Install: npm install @azure/storage-blob');
59
32
  }
60
33
  }
61
34
  async uploadFile(file, options) {
62
35
  let processedBuffer = file.buffer;
63
36
  let contentType = file.mimetype;
64
37
  const fileName = `${uuidv4()}-${file.originalname}`;
65
- // Compress image if needed
66
38
  if (options.compress && file.mimetype.startsWith('image/')) {
67
39
  const result = await ImageCompressor.compress(file.buffer, file.mimetype, {
68
40
  maxWidth: options.maxWidth,
@@ -74,9 +46,7 @@ import { v4 as uuidv4 } from 'uuid';
74
46
  contentType = result.format;
75
47
  }
76
48
  const blobName = options.folderPath ? `${options.folderPath}/${fileName}` : fileName;
77
- const blockBlobClient = this.containerClient.getBlockBlobClient(blobName);
78
- // Upload to Azure
79
- await blockBlobClient.upload(processedBuffer, processedBuffer.length, {
49
+ await this.containerClient.getBlockBlobClient(blobName).upload(processedBuffer, processedBuffer.length, {
80
50
  blobHTTPHeaders: {
81
51
  blobContentType: contentType
82
52
  }
@@ -92,8 +62,7 @@ import { v4 as uuidv4 } from 'uuid';
92
62
  return Promise.all(files.map((file)=>this.uploadFile(file, options)));
93
63
  }
94
64
  async deleteFile(key) {
95
- const blockBlobClient = this.containerClient.getBlockBlobClient(key);
96
- await blockBlobClient.deleteIfExists();
65
+ await this.containerClient.getBlockBlobClient(key).deleteIfExists();
97
66
  this.logger.log(`Deleted file from Azure: ${key}`);
98
67
  }
99
68
  async deleteMultipleFiles(keys) {
@@ -32,6 +32,29 @@ import { v4 as uuidv4 } from 'uuid';
32
32
  * Uses Node.js built-in fs module - no external dependencies
33
33
  */ export class LocalProvider {
34
34
  /**
35
+ * SECURITY: Validates that a target path does not escape the base directory
36
+ * Prevents path traversal attacks using ../ sequences
37
+ * @throws Error if path traversal is detected
38
+ */ validatePathWithinBase(targetPath) {
39
+ const normalizedBasePath = path.resolve(this.basePath);
40
+ const normalizedTargetPath = path.resolve(targetPath);
41
+ if (!normalizedTargetPath.startsWith(normalizedBasePath + path.sep) && normalizedTargetPath !== normalizedBasePath) {
42
+ this.logger.warn(`Path traversal attempt detected: ${targetPath}`);
43
+ throw new Error('Invalid path: Path traversal attempt detected');
44
+ }
45
+ }
46
+ /**
47
+ * SECURITY: Validates file key format to prevent malicious input
48
+ * @throws Error if key contains suspicious patterns
49
+ */ validateKeyFormat(key) {
50
+ if (!key || typeof key !== 'string' || key.trim().length === 0) {
51
+ throw new Error('Invalid file key: empty or invalid');
52
+ }
53
+ if (key.includes('\0')) {
54
+ throw new Error('Invalid file key: contains null bytes');
55
+ }
56
+ }
57
+ /**
35
58
  * Initialize Local File System provider with configuration
36
59
  * @param config.basePath - Base path for file storage (default: './uploads')
37
60
  * @param config.baseUrl - Optional base URL for generating file URLs
@@ -64,7 +87,11 @@ import { v4 as uuidv4 } from 'uuid';
64
87
  }
65
88
  // Build file path
66
89
  const folderPath = options.folderPath ? path.join(this.basePath, options.folderPath) : this.basePath;
90
+ // SECURITY: Validate path does not escape base directory
91
+ this.validatePathWithinBase(folderPath);
67
92
  const filePath = path.join(folderPath, fileName);
93
+ // SECURITY: Double-check final file path
94
+ this.validatePathWithinBase(filePath);
68
95
  // Ensure directory exists
69
96
  await fs.mkdir(folderPath, {
70
97
  recursive: true
@@ -87,14 +114,20 @@ import { v4 as uuidv4 } from 'uuid';
87
114
  }
88
115
  async deleteFile(key) {
89
116
  try {
117
+ // SECURITY: Validate key format first
118
+ this.validateKeyFormat(key);
90
119
  // Key now includes the basePath, resolve from cwd
91
120
  let filePath = path.resolve(key);
121
+ // SECURITY: Validate resolved path is within base directory
122
+ this.validatePathWithinBase(filePath);
92
123
  // Check if file exists at the resolved path
93
124
  try {
94
125
  await fs.access(filePath);
95
126
  } catch {
96
127
  // Fallback: try with basePath prefix (for old keys without basePath)
97
128
  const fallbackPath = path.join(this.basePath, key);
129
+ // SECURITY: Validate fallback path as well
130
+ this.validatePathWithinBase(fallbackPath);
98
131
  try {
99
132
  await fs.access(fallbackPath);
100
133
  filePath = fallbackPath;
@@ -107,7 +140,11 @@ import { v4 as uuidv4 } from 'uuid';
107
140
  }
108
141
  await fs.unlink(filePath);
109
142
  this.logger.log(`Deleted file from local storage: ${key}`);
110
- } catch (_error) {
143
+ } catch (error) {
144
+ // Re-throw security errors
145
+ if (error instanceof Error && error.message.includes('Invalid')) {
146
+ throw error;
147
+ }
111
148
  this.logger.warn(`Failed to delete file from local storage: ${key}`);
112
149
  }
113
150
  }
@@ -134,36 +171,6 @@ import { v4 as uuidv4 } from 'uuid';
134
171
  return false;
135
172
  }
136
173
  }
137
- /**
138
- * Get the absolute path for a file key
139
- * Key now includes basePath, so resolve from cwd
140
- */ getAbsolutePath(key) {
141
- return path.resolve(key);
142
- }
143
- /**
144
- * Check if a file exists
145
- */ async fileExists(key) {
146
- try {
147
- // Key now includes basePath, resolve from cwd
148
- const filePath = path.resolve(key);
149
- await fs.access(filePath);
150
- return true;
151
- } catch {
152
- return false;
153
- }
154
- }
155
- /**
156
- * Get file stats
157
- */ async getFileStats(key) {
158
- // Key now includes basePath, resolve from cwd
159
- const filePath = path.resolve(key);
160
- const stats = await fs.stat(filePath);
161
- return {
162
- size: stats.size,
163
- createdAt: stats.birthtime,
164
- modifiedAt: stats.mtime
165
- };
166
- }
167
174
  constructor(){
168
175
  _define_property(this, "logger", new Logger(LocalProvider.name));
169
176
  _define_property(this, "basePath", '');
@@ -1,17 +1,6 @@
1
1
  /**
2
- * OPTIONAL S3 Provider
3
- *
4
- * This provider requires @aws-sdk packages to be installed.
5
- * Only import this if you need AWS S3 storage.
6
- *
7
- * Installation:
8
- * npm install @aws-sdk/client-s3 @aws-sdk/lib-storage @aws-sdk/s3-request-presigner
9
- *
10
- * Usage:
11
- * import { S3Provider } from '@flusys/nestjs-storage/providers/s3-provider.optional';
12
- * import { StorageProviderRegistry, FileLocationEnum } from '@flusys/nestjs-storage';
13
- *
14
- * StorageProviderRegistry.register(FileLocationEnum.AWS, S3Provider);
2
+ * Optional AWS S3 Storage Provider
3
+ * Requires: npm install @aws-sdk/client-s3 @aws-sdk/lib-storage @aws-sdk/s3-request-presigner
15
4
  */ function _define_property(obj, key, value) {
16
5
  if (key in obj) {
17
6
  Object.defineProperty(obj, key, {
@@ -25,33 +14,26 @@
25
14
  }
26
15
  return obj;
27
16
  }
28
- import { ImageCompressor } from '../utils/image-compressor.util';
29
17
  import { Logger } from '@nestjs/common';
30
18
  import { v4 as uuidv4 } from 'uuid';
31
- /**
32
- * AWS S3 Storage Provider
33
- * Requires @aws-sdk packages to be installed
34
- */ export class S3Provider {
35
- /**
36
- * Initialize S3 provider with configuration
37
- */ async initialize(config) {
19
+ import { ImageCompressor } from '../utils/image-compressor.util';
20
+ export class S3Provider {
21
+ async initialize(config) {
38
22
  try {
39
- // Dynamic import - only loads AWS SDK if this provider is used
40
23
  const { S3Client } = await import('@aws-sdk/client-s3');
41
24
  this.region = config.region;
42
25
  this.bucketName = config.bucket;
43
26
  this.s3 = new S3Client({
44
27
  region: config.region,
28
+ endpoint: config.endpoint,
45
29
  credentials: config.accessKeyId && config.secretAccessKey ? {
46
30
  accessKeyId: config.accessKeyId,
47
31
  secretAccessKey: config.secretAccessKey
48
- } : undefined,
49
- endpoint: config.endpoint
32
+ } : undefined
50
33
  });
51
34
  this.logger.log(`S3 Provider initialized: region=${config.region}, bucket=${config.bucket}`);
52
- } catch (_error) {
53
- this.logger.error('Failed to initialize S3Provider. Make sure @aws-sdk packages are installed.');
54
- throw new Error('AWS SDK not found. Install it with: npm install @aws-sdk/client-s3 @aws-sdk/lib-storage @aws-sdk/s3-request-presigner');
35
+ } catch {
36
+ throw new Error('AWS SDK not found. Install: npm install @aws-sdk/client-s3 @aws-sdk/lib-storage @aws-sdk/s3-request-presigner');
55
37
  }
56
38
  }
57
39
  async uploadFile(file, options) {
@@ -59,7 +41,6 @@ import { v4 as uuidv4 } from 'uuid';
59
41
  let processedBuffer = file.buffer;
60
42
  let contentType = file.mimetype;
61
43
  const fileName = `${uuidv4()}-${file.originalname}`;
62
- // Compress image if needed
63
44
  if (options.compress && file.mimetype.startsWith('image/')) {
64
45
  const result = await ImageCompressor.compress(file.buffer, file.mimetype, {
65
46
  maxWidth: options.maxWidth,
@@ -71,7 +52,7 @@ import { v4 as uuidv4 } from 'uuid';
71
52
  contentType = result.format;
72
53
  }
73
54
  const key = options.folderPath ? `${options.folderPath}/${fileName}` : fileName;
74
- const upload = new Upload({
55
+ await new Upload({
75
56
  client: this.s3,
76
57
  params: {
77
58
  Bucket: this.bucketName,
@@ -79,8 +60,7 @@ import { v4 as uuidv4 } from 'uuid';
79
60
  Body: processedBuffer,
80
61
  ContentType: contentType
81
62
  }
82
- });
83
- await upload.done();
63
+ }).done();
84
64
  return {
85
65
  name: fileName,
86
66
  key,
@@ -93,44 +73,40 @@ import { v4 as uuidv4 } from 'uuid';
93
73
  }
94
74
  async deleteFile(key) {
95
75
  const { DeleteObjectCommand } = await import('@aws-sdk/client-s3');
96
- const command = new DeleteObjectCommand({
76
+ await this.s3.send(new DeleteObjectCommand({
97
77
  Bucket: this.bucketName,
98
78
  Key: key
99
- });
100
- await this.s3.send(command);
79
+ }));
101
80
  this.logger.log(`Deleted file from S3: ${key}`);
102
81
  }
103
82
  async deleteMultipleFiles(keys) {
104
83
  const { DeleteObjectsCommand } = await import('@aws-sdk/client-s3');
105
- const command = new DeleteObjectsCommand({
84
+ await this.s3.send(new DeleteObjectsCommand({
106
85
  Bucket: this.bucketName,
107
86
  Delete: {
108
87
  Objects: keys.map((key)=>({
109
88
  Key: key
110
89
  }))
111
90
  }
112
- });
113
- await this.s3.send(command);
91
+ }));
114
92
  this.logger.log(`Deleted ${keys.length} files from S3`);
115
93
  }
116
94
  async generatePresignedUrl(key, expiresInSeconds = 3600) {
117
95
  const { GetObjectCommand } = await import('@aws-sdk/client-s3');
118
96
  const { getSignedUrl } = await import('@aws-sdk/s3-request-presigner');
119
- const command = new GetObjectCommand({
97
+ return getSignedUrl(this.s3, new GetObjectCommand({
120
98
  Bucket: this.bucketName,
121
99
  Key: key
122
- });
123
- return getSignedUrl(this.s3, command, {
100
+ }), {
124
101
  expiresIn: expiresInSeconds
125
102
  });
126
103
  }
127
104
  async healthCheck() {
128
105
  try {
129
106
  const { HeadBucketCommand } = await import('@aws-sdk/client-s3');
130
- const command = new HeadBucketCommand({
107
+ await this.s3.send(new HeadBucketCommand({
131
108
  Bucket: this.bucketName
132
- });
133
- await this.s3.send(command);
109
+ }));
134
110
  return true;
135
111
  } catch {
136
112
  return false;
@@ -138,7 +114,7 @@ import { v4 as uuidv4 } from 'uuid';
138
114
  }
139
115
  constructor(){
140
116
  _define_property(this, "logger", new Logger(S3Provider.name));
141
- _define_property(this, "s3", void 0); // S3Client - typed as any to avoid import
117
+ _define_property(this, "s3", void 0);
142
118
  _define_property(this, "bucketName", '');
143
119
  _define_property(this, "region", '');
144
120
  }