@flusys/nestjs-storage 1.0.0-rc → 1.0.1

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 (103) hide show
  1. package/README.md +44 -1
  2. package/cjs/config/index.js +0 -1
  3. package/cjs/config/storage.constants.js +0 -9
  4. package/cjs/controllers/upload.controller.js +12 -17
  5. package/cjs/docs/storage-swagger.config.js +24 -136
  6. package/cjs/dtos/file-manager.dto.js +65 -32
  7. package/cjs/dtos/folder.dto.js +15 -9
  8. package/cjs/dtos/storage-config.dto.js +5 -86
  9. package/cjs/dtos/upload.dto.js +17 -17
  10. package/cjs/entities/file-manager-with-company.entity.js +3 -4
  11. package/cjs/entities/file-manager.entity.js +71 -3
  12. package/cjs/entities/folder-with-company.entity.js +3 -4
  13. package/cjs/entities/folder.entity.js +19 -3
  14. package/cjs/entities/index.js +9 -10
  15. package/cjs/entities/storage-config-with-company.entity.js +3 -4
  16. package/cjs/entities/storage-config.entity.js +73 -3
  17. package/cjs/middlewares/file-serve.middleware.js +107 -100
  18. package/cjs/modules/storage.module.js +82 -136
  19. package/cjs/providers/azure-provider.optional.js +10 -38
  20. package/cjs/providers/local-provider.js +0 -43
  21. package/cjs/providers/s3-provider.optional.js +19 -40
  22. package/cjs/providers/storage-factory.service.js +54 -99
  23. package/cjs/providers/storage-provider.registry.js +8 -18
  24. package/cjs/services/file-manager.service.js +239 -337
  25. package/cjs/services/folder.service.js +3 -3
  26. package/cjs/services/index.js +1 -0
  27. package/cjs/{config → services}/storage-config.service.js +30 -79
  28. package/cjs/services/storage-datasource.provider.js +16 -26
  29. package/cjs/services/storage-provider-config.service.js +3 -3
  30. package/cjs/services/upload.service.js +33 -61
  31. package/cjs/utils/file-validator.util.js +54 -66
  32. package/cjs/utils/image-compressor.util.js +2 -5
  33. package/config/index.d.ts +0 -1
  34. package/config/storage.constants.d.ts +0 -6
  35. package/controllers/upload.controller.d.ts +1 -0
  36. package/dtos/file-manager.dto.d.ts +11 -3
  37. package/dtos/folder.dto.d.ts +3 -1
  38. package/dtos/storage-config.dto.d.ts +7 -11
  39. package/entities/file-manager-with-company.entity.d.ts +2 -2
  40. package/entities/file-manager.entity.d.ts +11 -2
  41. package/entities/folder-with-company.entity.d.ts +2 -2
  42. package/entities/folder.entity.d.ts +4 -2
  43. package/entities/index.d.ts +3 -4
  44. package/entities/storage-config-with-company.entity.d.ts +2 -2
  45. package/entities/storage-config.entity.d.ts +7 -2
  46. package/fesm/config/index.js +0 -1
  47. package/fesm/config/storage.constants.js +0 -6
  48. package/fesm/controllers/upload.controller.js +12 -17
  49. package/fesm/docs/storage-swagger.config.js +27 -142
  50. package/fesm/dtos/file-manager.dto.js +66 -33
  51. package/fesm/dtos/folder.dto.js +16 -10
  52. package/fesm/dtos/storage-config.dto.js +7 -88
  53. package/fesm/dtos/upload.dto.js +17 -18
  54. package/fesm/entities/file-manager-with-company.entity.js +3 -4
  55. package/fesm/entities/file-manager.entity.js +72 -4
  56. package/fesm/entities/folder-with-company.entity.js +3 -4
  57. package/fesm/entities/folder.entity.js +20 -4
  58. package/fesm/entities/index.js +4 -8
  59. package/fesm/entities/storage-config-with-company.entity.js +3 -4
  60. package/fesm/entities/storage-config.entity.js +74 -4
  61. package/fesm/middlewares/file-serve.middleware.js +107 -100
  62. package/fesm/modules/storage.module.js +83 -136
  63. package/fesm/providers/azure-provider.optional.js +14 -45
  64. package/fesm/providers/local-provider.js +0 -43
  65. package/fesm/providers/s3-provider.optional.js +23 -47
  66. package/fesm/providers/storage-factory.service.js +52 -97
  67. package/fesm/providers/storage-provider.registry.js +10 -20
  68. package/fesm/services/file-manager.service.js +237 -335
  69. package/fesm/services/folder.service.js +1 -1
  70. package/fesm/services/index.js +1 -0
  71. package/fesm/{config → services}/storage-config.service.js +30 -79
  72. package/fesm/services/storage-datasource.provider.js +16 -26
  73. package/fesm/services/storage-provider-config.service.js +1 -1
  74. package/fesm/services/upload.service.js +31 -59
  75. package/fesm/utils/file-validator.util.js +54 -66
  76. package/fesm/utils/image-compressor.util.js +2 -5
  77. package/interfaces/storage-config.interface.d.ts +1 -2
  78. package/interfaces/storage-module-options.interface.d.ts +0 -5
  79. package/middlewares/file-serve.middleware.d.ts +9 -1
  80. package/modules/storage.module.d.ts +1 -2
  81. package/package.json +3 -3
  82. package/providers/azure-provider.optional.d.ts +8 -6
  83. package/providers/local-provider.d.ts +0 -7
  84. package/providers/s3-provider.optional.d.ts +9 -7
  85. package/providers/storage-factory.service.d.ts +8 -9
  86. package/providers/storage-provider.registry.d.ts +4 -4
  87. package/services/file-manager.service.d.ts +21 -14
  88. package/services/folder.service.d.ts +4 -4
  89. package/services/index.d.ts +1 -0
  90. package/{config → services}/storage-config.service.d.ts +9 -10
  91. package/services/storage-datasource.provider.d.ts +3 -4
  92. package/services/storage-provider-config.service.d.ts +5 -6
  93. package/services/upload.service.d.ts +5 -5
  94. package/utils/file-validator.util.d.ts +3 -0
  95. package/cjs/entities/file-manager-base.entity.js +0 -115
  96. package/cjs/entities/folder-base.entity.js +0 -55
  97. package/cjs/entities/storage-config-base.entity.js +0 -93
  98. package/entities/file-manager-base.entity.d.ts +0 -13
  99. package/entities/folder-base.entity.d.ts +0 -5
  100. package/entities/storage-config-base.entity.d.ts +0 -9
  101. package/fesm/entities/file-manager-base.entity.js +0 -108
  102. package/fesm/entities/folder-base.entity.js +0 -48
  103. package/fesm/entities/storage-config-base.entity.js +0 -83
@@ -30,123 +30,130 @@ import { createReadStream, existsSync, statSync } from 'fs';
30
30
  import { join, resolve } from 'path';
31
31
  import * as mime from 'mime-types';
32
32
  import { UploadService } from '../services/upload.service';
33
+ /** MIME type prefixes that can be displayed inline in browser */ const VIEWABLE_TYPE_PREFIXES = [
34
+ 'image/',
35
+ 'video/',
36
+ 'audio/',
37
+ 'text/',
38
+ 'application/pdf',
39
+ 'application/json',
40
+ 'application/xml'
41
+ ];
33
42
  export class FileServeMiddleware {
34
43
  async use(req, res, next) {
35
44
  try {
36
- const uploadDir = process.cwd();
37
- // Extract path after /storage/upload/file/
38
- const urlPath = req.path || req.url;
39
- const match = urlPath.match(/\/storage\/upload\/file\/(.+)/);
40
- const normalizedPath = match ? match[1] : '';
41
- if (!normalizedPath) {
42
- throw new NotFoundException('File path is required');
43
- }
44
- this.logger.debug(`Attempting to serve file, normalizedPath: ${normalizedPath}`);
45
- // Get the local storage basePath for fallback lookups
46
- const basePath = await this.uploadService.getLocalStorageBasePath();
47
- const normalizedBasePath = basePath ? basePath.replace(/^\.\//, '').replace(/\/$/, '') : null;
48
- // Strategy 1: Try path relative to CWD (new format: key includes basePath)
49
- let fullPath = join(uploadDir, normalizedPath);
50
- let resolvedPath = resolve(fullPath);
51
- this.logger.debug(`Strategy 1 - CWD relative: ${fullPath}`);
52
- // Validate path is inside the project directory (prevent path traversal)
53
- if (!resolvedPath.startsWith(uploadDir)) {
54
- throw new NotFoundException('Invalid file path');
55
- }
56
- // Check if file exists
57
- if (!existsSync(fullPath)) {
58
- // Strategy 2: If key already starts with basePath, try using basePath directly
59
- if (basePath && normalizedBasePath) {
60
- const pathStartsWithBase = normalizedPath.startsWith(normalizedBasePath + '/') || normalizedPath.startsWith(normalizedBasePath);
61
- if (pathStartsWithBase) {
62
- const remainingPath = normalizedPath.substring(normalizedBasePath.length).replace(/^\//, '');
63
- const fallbackPath = join(basePath, remainingPath);
64
- this.logger.debug(`Strategy 2 - basePath + remaining: ${fallbackPath}`);
65
- if (existsSync(fallbackPath)) {
66
- fullPath = fallbackPath;
67
- resolvedPath = resolve(fullPath);
68
- }
69
- } else {
70
- // Old format: key doesn't include basePath, prepend it
71
- const fallbackPath = join(basePath, normalizedPath);
72
- this.logger.debug(`Strategy 3 - basePath + full key (old format): ${fallbackPath}`);
73
- if (existsSync(fallbackPath)) {
74
- fullPath = fallbackPath;
75
- resolvedPath = resolve(fullPath);
76
- }
77
- }
78
- }
79
- // Final check if file was found
80
- if (!existsSync(fullPath)) {
81
- throw new NotFoundException(`File not found: ${normalizedPath}`);
82
- }
83
- }
45
+ const normalizedPath = this.extractFilePath(req);
46
+ const fullPath = await this.resolveFilePath(normalizedPath);
84
47
  const stats = statSync(fullPath);
85
48
  if (!stats.isFile()) {
86
49
  throw new NotFoundException(`Not a file: ${normalizedPath}`);
87
50
  }
88
- // Enhanced MIME type detection
89
- let mimeType = mime.lookup(fullPath) || 'application/octet-stream';
90
- // Special handling for SVG files
91
- if (fullPath.toLowerCase().endsWith('.svg')) {
92
- mimeType = 'image/svg+xml';
93
- }
94
- // Determine if file should be displayed inline or downloaded
95
- const viewableTypes = [
96
- 'image/',
97
- 'video/',
98
- 'audio/',
99
- 'text/',
100
- 'application/pdf',
101
- 'application/json',
102
- 'application/xml'
103
- ];
104
- const isViewable = viewableTypes.some((type)=>mimeType.startsWith(type));
105
- const contentDisposition = isViewable ? 'inline' : `attachment; filename="${encodeURIComponent(normalizedPath.split('/').pop() || 'download')}"`;
106
- // Set headers
107
- res.set({
108
- 'Content-Type': mimeType,
109
- 'Content-Length': stats.size,
110
- 'Content-Disposition': contentDisposition,
111
- 'Cache-Control': 'public, max-age=3600',
112
- 'Accept-Ranges': 'bytes',
113
- 'Cross-Origin-Resource-Policy': 'cross-origin',
114
- 'Access-Control-Allow-Origin': '*',
115
- 'X-Content-Type-Options': 'nosniff'
116
- });
117
- // Stream the file
118
- const stream = createReadStream(fullPath);
119
- stream.pipe(res);
120
- stream.on('error', (err)=>{
121
- this.logger.error('File stream error', err);
122
- if (!res.headersSent) {
123
- res.status(404).json({
124
- message: `Failed to serve file: ${normalizedPath}`,
125
- error: 'Not Found',
126
- statusCode: 404
127
- });
128
- }
129
- });
130
- // Handle client disconnection
131
- res.on('close', ()=>{
132
- stream.destroy();
133
- });
51
+ const mimeType = this.getMimeType(fullPath);
52
+ this.setResponseHeaders(res, stats.size, mimeType, normalizedPath);
53
+ this.streamFile(res, fullPath, normalizedPath);
134
54
  } catch (error) {
135
- this.logger.error('File retrieval error:', error);
55
+ this.sendErrorResponse(res, error);
56
+ }
57
+ }
58
+ /** Extract and validate file path from request URL */ extractFilePath(req) {
59
+ const urlPath = req.path || req.url;
60
+ const match = urlPath.match(/\/storage\/upload\/file\/(.+)/);
61
+ const normalizedPath = match ? match[1] : '';
62
+ if (!normalizedPath) {
63
+ throw new NotFoundException('File path is required');
64
+ }
65
+ this.logger.debug(`Attempting to serve file, normalizedPath: ${normalizedPath}`);
66
+ return normalizedPath;
67
+ }
68
+ /** Resolve file path using multiple fallback strategies */ async resolveFilePath(normalizedPath) {
69
+ // Strategy 1: Path relative to CWD (new format: key includes basePath)
70
+ let fullPath = join(this.uploadDir, normalizedPath);
71
+ const resolvedPath = resolve(fullPath);
72
+ this.logger.debug(`Strategy 1 - CWD relative: ${fullPath}`);
73
+ // Prevent path traversal attacks
74
+ if (!resolvedPath.startsWith(this.uploadDir)) {
75
+ throw new NotFoundException('Invalid file path');
76
+ }
77
+ if (existsSync(fullPath)) {
78
+ return fullPath;
79
+ }
80
+ // Try fallback strategies with basePath
81
+ const basePath = await this.uploadService.getLocalStorageBasePath();
82
+ if (basePath) {
83
+ const fallbackPath = this.tryFallbackPaths(normalizedPath, basePath);
84
+ if (fallbackPath) {
85
+ return fallbackPath;
86
+ }
87
+ }
88
+ throw new NotFoundException(`File not found: ${normalizedPath}`);
89
+ }
90
+ /** Try basePath-based fallback strategies */ tryFallbackPaths(normalizedPath, basePath) {
91
+ const normalizedBasePath = basePath.replace(/^\.\//, '').replace(/\/$/, '');
92
+ const pathStartsWithBase = normalizedPath.startsWith(normalizedBasePath + '/') || normalizedPath.startsWith(normalizedBasePath);
93
+ if (pathStartsWithBase) {
94
+ // Strategy 2: basePath + remaining path
95
+ const remainingPath = normalizedPath.substring(normalizedBasePath.length).replace(/^\//, '');
96
+ const fallbackPath = join(basePath, remainingPath);
97
+ this.logger.debug(`Strategy 2 - basePath + remaining: ${fallbackPath}`);
98
+ if (existsSync(fallbackPath)) return fallbackPath;
99
+ } else {
100
+ // Strategy 3: Old format - prepend basePath
101
+ const fallbackPath = join(basePath, normalizedPath);
102
+ this.logger.debug(`Strategy 3 - basePath + full key (old format): ${fallbackPath}`);
103
+ if (existsSync(fallbackPath)) return fallbackPath;
104
+ }
105
+ return null;
106
+ }
107
+ /** Get MIME type with SVG special handling */ getMimeType(fullPath) {
108
+ if (fullPath.toLowerCase().endsWith('.svg')) {
109
+ return 'image/svg+xml';
110
+ }
111
+ return mime.lookup(fullPath) || 'application/octet-stream';
112
+ }
113
+ /** Set response headers for file serving */ setResponseHeaders(res, size, mimeType, normalizedPath) {
114
+ const isViewable = VIEWABLE_TYPE_PREFIXES.some((type)=>mimeType.startsWith(type));
115
+ const filename = normalizedPath.split('/').pop() || 'download';
116
+ const contentDisposition = isViewable ? 'inline' : `attachment; filename="${encodeURIComponent(filename)}"`;
117
+ res.set({
118
+ 'Content-Type': mimeType,
119
+ 'Content-Length': size,
120
+ 'Content-Disposition': contentDisposition,
121
+ 'Cache-Control': 'public, max-age=3600',
122
+ 'Accept-Ranges': 'bytes',
123
+ 'Cross-Origin-Resource-Policy': 'cross-origin',
124
+ 'Access-Control-Allow-Origin': '*',
125
+ 'X-Content-Type-Options': 'nosniff'
126
+ });
127
+ }
128
+ /** Stream file to response with error handling */ streamFile(res, fullPath, normalizedPath) {
129
+ const stream = createReadStream(fullPath);
130
+ stream.pipe(res);
131
+ stream.on('error', (err)=>{
132
+ this.logger.error('File stream error', err);
136
133
  if (!res.headersSent) {
137
- res.status(404).json({
138
- message: error instanceof NotFoundException ? error.message : `File not found`,
139
- error: 'Not Found',
140
- statusCode: 404
141
- });
134
+ this.sendErrorResponse(res, new NotFoundException(`Failed to serve file: ${normalizedPath}`));
142
135
  }
136
+ });
137
+ res.on('close', ()=>stream.destroy());
138
+ }
139
+ /** Send consistent error response */ sendErrorResponse(res, error) {
140
+ this.logger.error('File retrieval error:', error);
141
+ if (!res.headersSent) {
142
+ const message = error instanceof NotFoundException ? error.message : 'File not found';
143
+ res.status(404).json({
144
+ message,
145
+ error: 'Not Found',
146
+ statusCode: 404
147
+ });
143
148
  }
144
149
  }
145
150
  constructor(uploadService){
146
151
  _define_property(this, "uploadService", void 0);
147
152
  _define_property(this, "logger", void 0);
153
+ _define_property(this, "uploadDir", void 0);
148
154
  this.uploadService = uploadService;
149
155
  this.logger = new Logger(FileServeMiddleware.name);
156
+ this.uploadDir = process.cwd();
150
157
  }
151
158
  }
152
159
  FileServeMiddleware = _ts_decorate([
@@ -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,18 +1,4 @@
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);
15
- */ function _define_property(obj, key, value) {
1
+ function _define_property(obj, key, value) {
16
2
  if (key in obj) {
17
3
  Object.defineProperty(obj, key, {
18
4
  value: value,
@@ -25,44 +11,30 @@
25
11
  }
26
12
  return obj;
27
13
  }
28
- import { ImageCompressor } from '../utils/image-compressor.util';
29
- import { Logger } from '@nestjs/common';
30
- import { v4 as uuidv4 } from 'uuid';
31
14
  /**
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) {
15
+ * Optional Azure Blob Storage Provider
16
+ * Requires: npm install @azure/storage-blob
17
+ */ import { Logger } from '@nestjs/common';
18
+ import { v4 as uuidv4 } from 'uuid';
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) {
@@ -171,49 +171,6 @@ import { v4 as uuidv4 } from 'uuid';
171
171
  return false;
172
172
  }
173
173
  }
174
- /**
175
- * Get the absolute path for a file key
176
- * Key now includes basePath, so resolve from cwd
177
- */ getAbsolutePath(key) {
178
- // SECURITY: Validate key format
179
- this.validateKeyFormat(key);
180
- const filePath = path.resolve(key);
181
- // SECURITY: Validate path is within base directory
182
- this.validatePathWithinBase(filePath);
183
- return filePath;
184
- }
185
- /**
186
- * Check if a file exists
187
- */ async fileExists(key) {
188
- try {
189
- // SECURITY: Validate key format
190
- this.validateKeyFormat(key);
191
- // Key now includes basePath, resolve from cwd
192
- const filePath = path.resolve(key);
193
- // SECURITY: Validate path is within base directory
194
- this.validatePathWithinBase(filePath);
195
- await fs.access(filePath);
196
- return true;
197
- } catch {
198
- return false;
199
- }
200
- }
201
- /**
202
- * Get file stats
203
- */ async getFileStats(key) {
204
- // SECURITY: Validate key format
205
- this.validateKeyFormat(key);
206
- // Key now includes basePath, resolve from cwd
207
- const filePath = path.resolve(key);
208
- // SECURITY: Validate path is within base directory
209
- this.validatePathWithinBase(filePath);
210
- const stats = await fs.stat(filePath);
211
- return {
212
- size: stats.size,
213
- createdAt: stats.birthtime,
214
- modifiedAt: stats.mtime
215
- };
216
- }
217
174
  constructor(){
218
175
  _define_property(this, "logger", new Logger(LocalProvider.name));
219
176
  _define_property(this, "basePath", '');