@flusys/nestjs-storage 1.0.0-rc → 1.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.
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 +11 -42
  64. package/fesm/providers/local-provider.js +0 -43
  65. package/fesm/providers/s3-provider.optional.js +20 -44
  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
@@ -29,7 +29,7 @@ import { RequestScopedApiService, HybridCache } from '@flusys/nestjs-shared/clas
29
29
  import { UtilsService } from '@flusys/nestjs-shared/modules';
30
30
  import { applyCompanyFilter } from '@flusys/nestjs-shared/utils';
31
31
  import { Inject, Injectable, Scope } from '@nestjs/common';
32
- import { StorageConfigService } from '../config';
32
+ import { StorageConfigService } from './storage-config.service';
33
33
  import { Folder, FolderWithCompany } from '../entities';
34
34
  import { StorageDataSourceProvider } from './storage-datasource.provider';
35
35
  export class FolderService extends RequestScopedApiService {
@@ -1,5 +1,6 @@
1
1
  export * from './file-manager.service';
2
2
  export * from './folder.service';
3
+ export * from './storage-config.service';
3
4
  export * from './storage-provider-config.service';
4
5
  export * from './storage-datasource.provider';
5
6
  export * from './upload.service';
@@ -27,120 +27,71 @@ function _ts_param(paramIndex, decorator) {
27
27
  }
28
28
  import { Inject, Injectable } from '@nestjs/common';
29
29
  import { StorageModuleOptions } from '../interfaces';
30
- import { STORAGE_MODULE_OPTIONS, DEFAULT_MAX_FILE_SIZE, DEFAULT_ALLOWED_FILE_TYPES } from './storage.constants';
30
+ import { STORAGE_MODULE_OPTIONS, DEFAULT_MAX_FILE_SIZE, DEFAULT_ALLOWED_FILE_TYPES } from '../config/storage.constants';
31
+ const BYTES_PER_MB = 1024 * 1024;
32
+ const DEFAULT_LOCAL_PATH = './uploads';
33
+ const DEFAULT_PORT = '3000';
31
34
  export class StorageConfigService {
32
- /**
33
- * Check if company feature is enabled
34
- */ isCompanyFeatureEnabled() {
35
+ // ─── IModuleConfigService Implementation ────────────────────────────────────
36
+ isCompanyFeatureEnabled() {
35
37
  return this.options.bootstrapAppConfig?.enableCompanyFeature ?? false;
36
38
  }
37
- /**
38
- * Get database mode
39
- */ getDatabaseMode() {
39
+ getDatabaseMode() {
40
40
  return this.options.bootstrapAppConfig?.databaseMode ?? 'single';
41
41
  }
42
- /**
43
- * Check if running in multi-tenant mode
44
- */ isMultiTenant() {
42
+ isMultiTenant() {
45
43
  return this.getDatabaseMode() === 'multi-tenant';
46
44
  }
47
- /**
48
- * Get maximum file size in bytes
49
- */ getMaxFileSize() {
45
+ // ─── Config Getters ─────────────────────────────────────────────────────────
46
+ getMaxFileSize() {
50
47
  return this.options.config?.maxFileSize ?? DEFAULT_MAX_FILE_SIZE;
51
48
  }
52
- /**
53
- * Get allowed file types (MIME types or patterns)
54
- */ getAllowedFileTypes() {
49
+ getAllowedFileTypes() {
55
50
  return this.options.config?.allowedFileTypes ?? DEFAULT_ALLOWED_FILE_TYPES;
56
51
  }
57
- /**
58
- * Check if file type is allowed
59
- */ isFileTypeAllowed(mimeType) {
52
+ getOptions() {
53
+ return this.options;
54
+ }
55
+ getDefaultLocalStoragePath() {
56
+ return this.options.config?.localStoragePath ?? DEFAULT_LOCAL_PATH;
57
+ }
58
+ getAppUrl() {
59
+ return this.options.config?.appUrl ?? process.env.APP_URL ?? `http://localhost:${process.env.PORT ?? DEFAULT_PORT}`;
60
+ }
61
+ // ─── Validation Methods ─────────────────────────────────────────────────────
62
+ isFileTypeAllowed(mimeType) {
60
63
  const allowedTypes = this.getAllowedFileTypes();
61
- // If wildcard, allow all
62
64
  if (allowedTypes.includes('*/*')) {
63
65
  return true;
64
66
  }
65
- // Check exact match or wildcard pattern
66
- return allowedTypes.some((allowedType)=>{
67
- if (allowedType.endsWith('/*')) {
68
- const prefix = allowedType.slice(0, -2);
69
- return mimeType.startsWith(prefix);
70
- }
71
- return allowedType === mimeType;
72
- });
67
+ return allowedTypes.some((type)=>type.endsWith('/*') ? mimeType.startsWith(type.slice(0, -2)) : type === mimeType);
73
68
  }
74
- /**
75
- * Validate file size
76
- */ validateFileSize(fileSize) {
69
+ validateFileSize(fileSize) {
77
70
  const maxSize = this.getMaxFileSize();
78
71
  if (fileSize > maxSize) {
79
- const maxSizeMB = (maxSize / (1024 * 1024)).toFixed(2);
80
- const fileSizeMB = (fileSize / (1024 * 1024)).toFixed(2);
81
72
  return {
82
73
  valid: false,
83
- message: `File size (${fileSizeMB}MB) exceeds the maximum allowed size of ${maxSizeMB}MB`
74
+ message: `File size (${this.toMB(fileSize)}MB) exceeds maximum ${this.toMB(maxSize)}MB`
84
75
  };
85
76
  }
86
77
  return {
87
78
  valid: true
88
79
  };
89
80
  }
90
- /**
91
- * Validate file type
92
- */ validateFileType(mimeType) {
81
+ validateFileType(mimeType) {
93
82
  if (!this.isFileTypeAllowed(mimeType)) {
94
- const allowedTypes = this.getAllowedFileTypes();
95
83
  return {
96
84
  valid: false,
97
- message: `File type "${mimeType}" is not allowed. Allowed types: ${allowedTypes.join(', ')}`
85
+ message: `File type "${mimeType}" not allowed. Allowed: ${this.getAllowedFileTypes().join(', ')}`
98
86
  };
99
87
  }
100
88
  return {
101
89
  valid: true
102
90
  };
103
91
  }
104
- /**
105
- * Get default database config
106
- */ getDefaultDatabaseConfig() {
107
- return this.options.config?.defaultDatabaseConfig;
108
- }
109
- /**
110
- * Get all options
111
- */ getOptions() {
112
- return this.options;
113
- }
114
- /**
115
- * Get default local storage base path
116
- * Used for serving local files without database lookup
117
- * Falls back to './uploads' if not configured
118
- */ getDefaultLocalStoragePath() {
119
- // Check if localStoragePath is configured in module options
120
- const configuredPath = this.options.config?.localStoragePath;
121
- if (configuredPath) {
122
- return configuredPath;
123
- }
124
- // Default to ./uploads in the project root
125
- return './uploads';
126
- }
127
- /**
128
- * Get application base URL for generating file URLs
129
- * - First tries module config
130
- * - Falls back to APP_URL env var
131
- * - Finally constructs from PORT env var
132
- */ getAppUrl() {
133
- // Try from module config first
134
- if (this.options.config?.appUrl) {
135
- return this.options.config.appUrl;
136
- }
137
- // Fallback: read directly from environment
138
- if (process.env.APP_URL) {
139
- return process.env.APP_URL;
140
- }
141
- // Last resort: construct from PORT
142
- const port = process.env.PORT || '3000';
143
- return `http://localhost:${port}`;
92
+ // ─── Private Helpers ────────────────────────────────────────────────────────
93
+ toMB(bytes) {
94
+ return (bytes / BYTES_PER_MB).toFixed(2);
144
95
  }
145
96
  constructor(options){
146
97
  _define_property(this, "options", void 0);
@@ -29,8 +29,7 @@ import { MultiTenantDataSourceService } from '@flusys/nestjs-shared/modules';
29
29
  import { Inject, Injectable, Logger, Optional, Scope } from '@nestjs/common';
30
30
  import { REQUEST } from '@nestjs/core';
31
31
  import { Request } from 'express';
32
- import { StorageModuleOptions } from '../interfaces';
33
- import { STORAGE_MODULE_OPTIONS } from '../config/storage.constants';
32
+ import { StorageConfigService } from './storage-config.service';
34
33
  export class StorageDataSourceProvider extends MultiTenantDataSourceService {
35
34
  // Factory Methods
36
35
  /** Build parent options from StorageModuleOptions */ static buildParentOptions(options) {
@@ -42,14 +41,11 @@ export class StorageDataSourceProvider extends MultiTenantDataSourceService {
42
41
  };
43
42
  }
44
43
  // Feature Flags
45
- /** Get global enable company feature flag */ getEnableCompanyFeature() {
46
- return this.storageOptions.bootstrapAppConfig?.enableCompanyFeature ?? false;
47
- }
48
44
  /**
49
45
  * Get enable company feature for specific tenant
50
46
  * Falls back to global setting if not specified per-tenant
51
47
  */ getEnableCompanyFeatureForTenant(tenant) {
52
- return tenant?.enableCompanyFeature ?? this.getEnableCompanyFeature();
48
+ return tenant?.enableCompanyFeature ?? this.configService.isCompanyFeatureEnabled();
53
49
  }
54
50
  /**
55
51
  * Get enable company feature for current request context
@@ -62,22 +58,16 @@ export class StorageDataSourceProvider extends MultiTenantDataSourceService {
62
58
  * For TypeORM repositories, we always use the base entities (FileManager, etc.)
63
59
  * but for migrations, we need the correct entity based on the feature flag.
64
60
  */ async getStorageEntities(enableCompanyFeature) {
65
- const enable = enableCompanyFeature ?? this.getEnableCompanyFeature();
66
- const { FileManager, Folder, StorageConfig } = await import('../entities');
67
- // For migrations and schema sync, return the appropriate entity
68
- // Both versions map to the same table, but with different columns
69
- if (enable) {
70
- const { FileManagerWithCompany, FolderWithCompany, StorageConfigWithCompany } = await import('../entities');
71
- return [
72
- FileManagerWithCompany,
73
- FolderWithCompany,
74
- StorageConfigWithCompany
75
- ];
76
- }
77
- return [
78
- FileManager,
79
- Folder,
80
- StorageConfig
61
+ const enable = enableCompanyFeature ?? this.configService.isCompanyFeatureEnabled();
62
+ const entities = await import('../entities');
63
+ return enable ? [
64
+ entities.FileManagerWithCompany,
65
+ entities.FolderWithCompany,
66
+ entities.StorageConfigWithCompany
67
+ ] : [
68
+ entities.FileManager,
69
+ entities.Folder,
70
+ entities.StorageConfig
81
71
  ];
82
72
  }
83
73
  // Overrides
@@ -138,8 +128,8 @@ export class StorageDataSourceProvider extends MultiTenantDataSourceService {
138
128
  StorageDataSourceProvider.connectionLocks.delete(tenant.id);
139
129
  }
140
130
  }
141
- constructor(storageOptions, request){
142
- super(StorageDataSourceProvider.buildParentOptions(storageOptions), request), _define_property(this, "storageOptions", void 0), _define_property(this, "logger", void 0), this.storageOptions = storageOptions, this.logger = new Logger(StorageDataSourceProvider.name);
131
+ constructor(configService, request){
132
+ super(StorageDataSourceProvider.buildParentOptions(configService.getOptions()), request), _define_property(this, "configService", void 0), _define_property(this, "logger", void 0), this.configService = configService, this.logger = new Logger(StorageDataSourceProvider.name);
143
133
  }
144
134
  }
145
135
  // Override parent's static properties to have Storage-specific cache
@@ -153,12 +143,12 @@ StorageDataSourceProvider = _ts_decorate([
153
143
  Injectable({
154
144
  scope: Scope.REQUEST
155
145
  }),
156
- _ts_param(0, Inject(STORAGE_MODULE_OPTIONS)),
146
+ _ts_param(0, Inject(StorageConfigService)),
157
147
  _ts_param(1, Optional()),
158
148
  _ts_param(1, Inject(REQUEST)),
159
149
  _ts_metadata("design:type", Function),
160
150
  _ts_metadata("design:paramtypes", [
161
- typeof StorageModuleOptions === "undefined" ? Object : StorageModuleOptions,
151
+ typeof StorageConfigService === "undefined" ? Object : StorageConfigService,
162
152
  typeof Request === "undefined" ? Object : Request
163
153
  ])
164
154
  ], StorageDataSourceProvider);
@@ -29,7 +29,7 @@ import { RequestScopedApiService, HybridCache } from '@flusys/nestjs-shared/clas
29
29
  import { UtilsService } from '@flusys/nestjs-shared/modules';
30
30
  import { applyCompanyFilter, buildCompanyWhereCondition } from '@flusys/nestjs-shared/utils';
31
31
  import { Inject, Injectable, Scope } from '@nestjs/common';
32
- import { StorageConfigService } from '../config';
32
+ import { StorageConfigService } from './storage-config.service';
33
33
  import { StorageConfig, StorageConfigWithCompany } from '../entities';
34
34
  import { StorageDataSourceProvider } from './storage-datasource.provider';
35
35
  export class StorageProviderConfigService extends RequestScopedApiService {
@@ -27,7 +27,7 @@ function _ts_param(paramIndex, decorator) {
27
27
  }
28
28
  import { ErrorHandler, validateCompanyOwnership } from '@flusys/nestjs-shared/utils';
29
29
  import { BadRequestException, Inject, Injectable, Logger, NotFoundException, Scope } from '@nestjs/common';
30
- import { StorageConfigService } from '../config';
30
+ import { StorageConfigService } from './storage-config.service';
31
31
  import { FileLocationEnum } from '../enums/file-location.enum';
32
32
  import { StorageFactoryService } from '../providers/storage-factory.service';
33
33
  import { StorageProviderConfigService } from './storage-provider-config.service';
@@ -62,6 +62,25 @@ export class UploadService {
62
62
  file.originalname = FileValidator.sanitizeFilename(file.originalname);
63
63
  }
64
64
  /**
65
+ * Create provider from storage config entity
66
+ */ async createProviderFromConfig(config) {
67
+ return this.storageFactory.createProvider({
68
+ provider: config.storage,
69
+ config: config.config
70
+ });
71
+ }
72
+ /**
73
+ * Create fallback local provider
74
+ */ async createFallbackLocalProvider() {
75
+ this.logger.warn('No storage config found, using fallback local provider');
76
+ return this.storageFactory.createProvider({
77
+ provider: FileLocationEnum.LOCAL,
78
+ config: {
79
+ basePath: this.storageConfigService.getDefaultLocalStoragePath()
80
+ }
81
+ });
82
+ }
83
+ /**
65
84
  * Get storage provider and config info based on storage config ID
66
85
  * Validates company ownership when company feature is enabled
67
86
  */ async getStorageProviderWithConfig(storageConfigId, user) {
@@ -84,13 +103,7 @@ export class UploadService {
84
103
  }
85
104
  storageConfig = defaultConfig;
86
105
  }
87
- // Create provider config
88
- const providerConfig = {
89
- provider: storageConfig.storage,
90
- config: storageConfig.config
91
- };
92
- // Get or create provider instance
93
- const provider = await this.storageFactory.createProvider(providerConfig);
106
+ const provider = await this.createProviderFromConfig(storageConfig);
94
107
  return {
95
108
  provider,
96
109
  location: storageConfig.storage,
@@ -98,74 +111,33 @@ export class UploadService {
98
111
  };
99
112
  }
100
113
  /**
101
- * Get storage provider based on storage config ID (convenience method)
102
- */ async getStorageProvider(storageConfigId, user) {
103
- const { provider } = await this.getStorageProviderWithConfig(storageConfigId, user);
104
- return provider;
105
- }
106
- /**
107
114
  * Get storage provider for delete operations with fallback
108
115
  * If the original storage config doesn't exist, falls back based on locationHint
109
- * This ensures files can still be deleted even if storage config was removed
110
- * @param storageConfigId - The storage config ID to look up
111
- * @param user - User context for company filtering
112
- * @param locationHint - The file's original location type (used for fallback)
113
116
  */ async getStorageProviderForDelete(storageConfigId, user, locationHint) {
114
- // If storageConfigId provided, try to find it
117
+ // Try to find by storageConfigId
115
118
  if (storageConfigId) {
116
119
  const config = await this.storageProviderConfigService.findByIdDirect(storageConfigId);
117
120
  if (config) {
118
- const providerConfig = {
119
- provider: config.storage,
120
- config: config.config
121
- };
122
- return await this.storageFactory.createProvider(providerConfig);
121
+ return this.createProviderFromConfig(config);
123
122
  }
124
- // Config not found, log warning and try fallback
125
- this.logger.warn(`Storage config ${storageConfigId} not found, trying fallback for delete`);
123
+ this.logger.warn(`Storage config ${storageConfigId} not found, trying fallback`);
126
124
  }
127
- // Fallback: Use locationHint to find a matching config or create provider
125
+ // Fallback: Use locationHint to find a matching config
128
126
  if (locationHint) {
129
- // Try to find a config matching the file's original location type
130
127
  const matchingConfigs = await this.storageProviderConfigService.getConfigByType(locationHint, user);
131
128
  if (matchingConfigs.length > 0) {
132
- const providerConfig = {
133
- provider: matchingConfigs[0].storage,
134
- config: matchingConfigs[0].config
135
- };
136
- this.logger.debug(`Using matching ${locationHint} config for delete fallback`);
137
- return await this.storageFactory.createProvider(providerConfig);
129
+ return this.createProviderFromConfig(matchingConfigs[0]);
138
130
  }
139
- // No matching config found, create a basic provider based on locationHint
140
131
  if (locationHint === FileLocationEnum.LOCAL) {
141
- this.logger.warn('No local config found, using fallback local provider for delete');
142
- const localProviderConfig = {
143
- provider: FileLocationEnum.LOCAL,
144
- config: {
145
- basePath: this.storageConfigService.getDefaultLocalStoragePath()
146
- }
147
- };
148
- return await this.storageFactory.createProvider(localProviderConfig);
132
+ return this.createFallbackLocalProvider();
149
133
  }
150
134
  }
151
- // Last resort: Try default config (might be different provider type)
135
+ // Last resort: Try default config
152
136
  const defaultConfig = await this.storageProviderConfigService.getDefaultConfig(user);
153
137
  if (defaultConfig) {
154
- const providerConfig = {
155
- provider: defaultConfig.storage,
156
- config: defaultConfig.config
157
- };
158
- return await this.storageFactory.createProvider(providerConfig);
138
+ return this.createProviderFromConfig(defaultConfig);
159
139
  }
160
- // Final fallback: Create a basic local provider
161
- this.logger.warn('No storage config found, using fallback local provider for delete');
162
- const localProviderConfig = {
163
- provider: FileLocationEnum.LOCAL,
164
- config: {
165
- basePath: this.storageConfigService.getDefaultLocalStoragePath()
166
- }
167
- };
168
- return await this.storageFactory.createProvider(localProviderConfig);
140
+ return this.createFallbackLocalProvider();
169
141
  }
170
142
  async uploadSingleFile(file, options, user) {
171
143
  try {
@@ -263,7 +235,7 @@ export class UploadService {
263
235
  * For Local/SFTP: returns direct path
264
236
  */ async makeFileUrl(key, storageConfigId, expiresIn = 3600, user) {
265
237
  try {
266
- const provider = await this.getStorageProvider(storageConfigId, user);
238
+ const { provider } = await this.getStorageProviderWithConfig(storageConfigId, user);
267
239
  // Check if provider supports presigned URLs
268
240
  if (provider.generatePresignedUrl) {
269
241
  return await provider.generatePresignedUrl(key, expiresIn);
@@ -293,10 +293,33 @@ import { Logger } from '@nestjs/common';
293
293
  'image/svg+xml',
294
294
  'application/xhtml+xml'
295
295
  ];
296
+ /**
297
+ * ZIP-based format prefixes that are valid when detected as application/zip.
298
+ */ const ZIP_VARIANT_PREFIXES = [
299
+ 'application/vnd.openxmlformats-officedocument',
300
+ 'application/x-zip',
301
+ 'application/x-compressed'
302
+ ];
296
303
  /**
297
304
  * Utility class for validating file content using magic bytes.
298
305
  * Prevents file type spoofing by checking actual file content.
299
306
  */ export class FileValidator {
307
+ // Result Helpers
308
+ static failureResult(message, detectedType, declaredType) {
309
+ return {
310
+ valid: false,
311
+ detectedType,
312
+ declaredType,
313
+ message
314
+ };
315
+ }
316
+ static successResult(detectedType, declaredType) {
317
+ return {
318
+ valid: true,
319
+ detectedType,
320
+ declaredType
321
+ };
322
+ }
300
323
  /**
301
324
  * Detect file type from buffer using magic bytes.
302
325
  * @param buffer - File buffer to analyze
@@ -342,15 +365,8 @@ import { Logger } from '@nestjs/common';
342
365
  const detectedCategory = detected.split('/')[0];
343
366
  const declaredCategory = declared.split('/')[0];
344
367
  // For ZIP-based formats, allow any ZIP-detected file if declared is a ZIP variant
345
- if (detected === 'application/zip') {
346
- const zipVariants = [
347
- 'application/vnd.openxmlformats-officedocument',
348
- 'application/x-zip',
349
- 'application/x-compressed'
350
- ];
351
- if (zipVariants.some((v)=>declared.startsWith(v))) {
352
- return true;
353
- }
368
+ if (detected === 'application/zip' && ZIP_VARIANT_PREFIXES.some((v)=>declared.startsWith(v))) {
369
+ return true;
354
370
  }
355
371
  return detectedCategory === declaredCategory;
356
372
  }
@@ -378,74 +394,46 @@ import { Logger } from '@nestjs/common';
378
394
  '*/*'
379
395
  ]) {
380
396
  try {
381
- // Detect actual file type from magic bytes
382
397
  const detectedType = this.detectFileType(buffer);
383
- // If no type detected, check if it's a text-based type
398
+ // No magic bytes detected - handle text-based types
384
399
  if (!detectedType) {
385
- // Check for dangerous text types first (HTML, JS, SVG)
386
- if (this.isDangerousTextType(declaredMimeType)) {
387
- // Only allow dangerous types if explicitly in allowedTypes (not via wildcard)
388
- const explicitlyAllowed = allowedTypes.some((t)=>t === declaredMimeType && t !== '*/*' && !t.endsWith('/*'));
389
- if (!explicitlyAllowed) {
390
- this.logger.warn(`Blocked dangerous file type: ${declaredMimeType} - requires explicit allowlisting`);
391
- return {
392
- valid: false,
393
- detectedType: declaredMimeType,
394
- declaredType: declaredMimeType,
395
- message: `File type "${declaredMimeType}" is potentially dangerous and not explicitly allowed`
396
- };
397
- }
398
- this.logger.warn(`Allowing explicitly permitted dangerous file type: ${declaredMimeType}`);
399
- }
400
- if (this.isTextBasedType(declaredMimeType)) {
401
- // Safe text-based files don't have magic bytes, trust the declared type
402
- const isAllowed = this.isTypeAllowed(declaredMimeType, allowedTypes);
403
- return {
404
- valid: isAllowed,
405
- detectedType: declaredMimeType,
406
- declaredType: declaredMimeType,
407
- message: isAllowed ? undefined : `File type "${declaredMimeType}" is not allowed`
408
- };
409
- }
410
- // For binary files without recognized signatures, be cautious
411
- this.logger.warn(`Unable to detect file type for declared type: ${declaredMimeType}`);
412
- return {
413
- valid: false,
414
- declaredType: declaredMimeType,
415
- message: 'Unable to verify file type. File may be corrupted or unsupported.'
416
- };
400
+ return this.validateUndetectedType(declaredMimeType, allowedTypes);
417
401
  }
418
- // Check if detected type matches declared type
402
+ // Verify detected type matches declared type
419
403
  if (!this.mimeTypesMatch(detectedType, declaredMimeType)) {
420
404
  this.logger.warn(`MIME type mismatch: declared=${declaredMimeType}, detected=${detectedType}`);
421
- return {
422
- valid: false,
423
- detectedType,
424
- declaredType: declaredMimeType,
425
- message: `File content does not match declared type. Detected: ${detectedType}, Declared: ${declaredMimeType}`
426
- };
405
+ return this.failureResult(`File content does not match declared type. Detected: ${detectedType}, Declared: ${declaredMimeType}`, detectedType, declaredMimeType);
427
406
  }
428
- // Check if detected type is allowed
407
+ // Verify type is in allowed list
429
408
  if (!this.isTypeAllowed(detectedType, allowedTypes)) {
430
- return {
431
- valid: false,
432
- detectedType,
433
- declaredType: declaredMimeType,
434
- message: `File type "${detectedType}" is not allowed`
435
- };
409
+ return this.failureResult(`File type "${detectedType}" is not allowed`, detectedType, declaredMimeType);
436
410
  }
437
- return {
438
- valid: true,
439
- detectedType,
440
- declaredType: declaredMimeType
441
- };
411
+ return this.successResult(detectedType, declaredMimeType);
442
412
  } catch (error) {
443
413
  this.logger.error('File validation error:', error);
444
- return {
445
- valid: false,
446
- message: 'File validation failed'
447
- };
414
+ return this.failureResult('File validation failed');
415
+ }
416
+ }
417
+ /**
418
+ * Handle validation for files without detectable magic bytes.
419
+ */ static validateUndetectedType(declaredMimeType, allowedTypes) {
420
+ // Check for dangerous text types first (HTML, JS, SVG)
421
+ if (this.isDangerousTextType(declaredMimeType)) {
422
+ const explicitlyAllowed = allowedTypes.some((t)=>t === declaredMimeType && t !== '*/*' && !t.endsWith('/*'));
423
+ if (!explicitlyAllowed) {
424
+ this.logger.warn(`Blocked dangerous file type: ${declaredMimeType} - requires explicit allowlisting`);
425
+ return this.failureResult(`File type "${declaredMimeType}" is potentially dangerous and not explicitly allowed`, declaredMimeType, declaredMimeType);
426
+ }
427
+ this.logger.warn(`Allowing explicitly permitted dangerous file type: ${declaredMimeType}`);
428
+ }
429
+ // Safe text-based files don't have magic bytes, trust declared type
430
+ if (this.isTextBasedType(declaredMimeType)) {
431
+ const isAllowed = this.isTypeAllowed(declaredMimeType, allowedTypes);
432
+ return isAllowed ? this.successResult(declaredMimeType, declaredMimeType) : this.failureResult(`File type "${declaredMimeType}" is not allowed`, declaredMimeType, declaredMimeType);
448
433
  }
434
+ // Binary files without recognized signatures - be cautious
435
+ this.logger.warn(`Unable to detect file type for declared type: ${declaredMimeType}`);
436
+ return this.failureResult('Unable to verify file type. File may be corrupted or unsupported.', undefined, declaredMimeType);
449
437
  }
450
438
  /**
451
439
  * Sanitize filename to prevent path traversal and special character issues.
@@ -99,12 +99,9 @@ const sharp = sharpModule.default || sharpModule;
99
99
  break;
100
100
  }
101
101
  try {
102
- // Process main image
103
- const { data, info } = await image.toBuffer({
104
- resolveWithObject: true
105
- });
102
+ const compressedBuffer = await image.toBuffer();
106
103
  return {
107
- buffer: data,
104
+ buffer: compressedBuffer,
108
105
  format: `image/${targetFormat}`
109
106
  };
110
107
  } catch {
@@ -1,8 +1,7 @@
1
1
  import { IIdentity } from '@flusys/nestjs-shared/interfaces';
2
- import { FileLocationEnum } from '../enums';
3
2
  export interface IStorageConfig extends IIdentity {
4
3
  name: string;
5
- storage: FileLocationEnum;
4
+ storage: string;
6
5
  config: Record<string, any>;
7
6
  isActive: boolean;
8
7
  isDefault: boolean;
@@ -7,17 +7,12 @@ export interface IStorageModuleConfig extends IDataSourceServiceOptions {
7
7
  localStoragePath?: string;
8
8
  appUrl?: string;
9
9
  }
10
- export interface IStorageModuleConfigFull {
11
- bootstrapAppConfig?: IBootstrapAppConfig;
12
- config?: IStorageModuleConfig;
13
- }
14
10
  export interface StorageModuleOptions extends IDynamicModuleConfig {
15
11
  bootstrapAppConfig?: IBootstrapAppConfig;
16
12
  config?: IStorageModuleConfig;
17
13
  }
18
14
  export interface StorageOptionsFactory extends IModuleOptionsFactory<IStorageModuleConfig> {
19
15
  createStorageOptions(): Promise<IStorageModuleConfig> | IStorageModuleConfig;
20
- createOptions(): Promise<IStorageModuleConfig> | IStorageModuleConfig;
21
16
  }
22
17
  export interface StorageModuleAsyncOptions extends Pick<ModuleMetadata, 'imports'>, IDynamicModuleConfig {
23
18
  bootstrapAppConfig: IBootstrapAppConfig;
@@ -3,7 +3,15 @@ import { Request, Response, NextFunction } from 'express';
3
3
  import { UploadService } from '../services/upload.service';
4
4
  export declare class FileServeMiddleware implements NestMiddleware {
5
5
  private uploadService;
6
- private logger;
6
+ private readonly logger;
7
+ private readonly uploadDir;
7
8
  constructor(uploadService: UploadService);
8
9
  use(req: Request, res: Response, next: NextFunction): Promise<void>;
10
+ private extractFilePath;
11
+ private resolveFilePath;
12
+ private tryFallbackPaths;
13
+ private getMimeType;
14
+ private setResponseHeaders;
15
+ private streamFile;
16
+ private sendErrorResponse;
9
17
  }
@@ -4,7 +4,6 @@ export declare class StorageModule implements NestModule {
4
4
  configure(consumer: MiddlewareConsumer): void;
5
5
  static forRoot(options: StorageModuleOptions): DynamicModule;
6
6
  static forRootAsync(options: StorageModuleAsyncOptions): DynamicModule;
7
- private static getControllers;
8
- private static getProviders;
7
+ private static buildProviders;
9
8
  private static createAsyncProviders;
10
9
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@flusys/nestjs-storage",
3
- "version": "1.0.0-rc",
3
+ "version": "1.0.0",
4
4
  "description": "Modular storage package with optional AWS S3, Azure Blob, and SFTP providers",
5
5
  "main": "cjs/index.js",
6
6
  "module": "fesm/index.js",
@@ -128,7 +128,7 @@
128
128
  }
129
129
  },
130
130
  "dependencies": {
131
- "@flusys/nestjs-core": "1.0.0-rc",
132
- "@flusys/nestjs-shared": "1.0.0-rc"
131
+ "@flusys/nestjs-core": "1.0.0",
132
+ "@flusys/nestjs-shared": "1.0.0"
133
133
  }
134
134
  }