@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.
- package/README.md +44 -1
- package/cjs/config/index.js +0 -1
- package/cjs/config/storage.constants.js +0 -9
- package/cjs/controllers/upload.controller.js +12 -17
- package/cjs/docs/storage-swagger.config.js +24 -136
- package/cjs/dtos/file-manager.dto.js +65 -32
- package/cjs/dtos/folder.dto.js +15 -9
- package/cjs/dtos/storage-config.dto.js +5 -86
- package/cjs/dtos/upload.dto.js +17 -17
- package/cjs/entities/file-manager-with-company.entity.js +3 -4
- package/cjs/entities/file-manager.entity.js +71 -3
- package/cjs/entities/folder-with-company.entity.js +3 -4
- package/cjs/entities/folder.entity.js +19 -3
- package/cjs/entities/index.js +9 -10
- package/cjs/entities/storage-config-with-company.entity.js +3 -4
- package/cjs/entities/storage-config.entity.js +73 -3
- package/cjs/middlewares/file-serve.middleware.js +107 -100
- package/cjs/modules/storage.module.js +82 -136
- package/cjs/providers/azure-provider.optional.js +10 -38
- package/cjs/providers/local-provider.js +0 -43
- package/cjs/providers/s3-provider.optional.js +19 -40
- package/cjs/providers/storage-factory.service.js +54 -99
- package/cjs/providers/storage-provider.registry.js +8 -18
- package/cjs/services/file-manager.service.js +239 -337
- package/cjs/services/folder.service.js +3 -3
- package/cjs/services/index.js +1 -0
- package/cjs/{config → services}/storage-config.service.js +30 -79
- package/cjs/services/storage-datasource.provider.js +16 -26
- package/cjs/services/storage-provider-config.service.js +3 -3
- package/cjs/services/upload.service.js +33 -61
- package/cjs/utils/file-validator.util.js +54 -66
- package/cjs/utils/image-compressor.util.js +2 -5
- package/config/index.d.ts +0 -1
- package/config/storage.constants.d.ts +0 -6
- package/controllers/upload.controller.d.ts +1 -0
- package/dtos/file-manager.dto.d.ts +11 -3
- package/dtos/folder.dto.d.ts +3 -1
- package/dtos/storage-config.dto.d.ts +7 -11
- package/entities/file-manager-with-company.entity.d.ts +2 -2
- package/entities/file-manager.entity.d.ts +11 -2
- package/entities/folder-with-company.entity.d.ts +2 -2
- package/entities/folder.entity.d.ts +4 -2
- package/entities/index.d.ts +3 -4
- package/entities/storage-config-with-company.entity.d.ts +2 -2
- package/entities/storage-config.entity.d.ts +7 -2
- package/fesm/config/index.js +0 -1
- package/fesm/config/storage.constants.js +0 -6
- package/fesm/controllers/upload.controller.js +12 -17
- package/fesm/docs/storage-swagger.config.js +27 -142
- package/fesm/dtos/file-manager.dto.js +66 -33
- package/fesm/dtos/folder.dto.js +16 -10
- package/fesm/dtos/storage-config.dto.js +7 -88
- package/fesm/dtos/upload.dto.js +17 -18
- package/fesm/entities/file-manager-with-company.entity.js +3 -4
- package/fesm/entities/file-manager.entity.js +72 -4
- package/fesm/entities/folder-with-company.entity.js +3 -4
- package/fesm/entities/folder.entity.js +20 -4
- package/fesm/entities/index.js +4 -8
- package/fesm/entities/storage-config-with-company.entity.js +3 -4
- package/fesm/entities/storage-config.entity.js +74 -4
- package/fesm/middlewares/file-serve.middleware.js +107 -100
- package/fesm/modules/storage.module.js +83 -136
- package/fesm/providers/azure-provider.optional.js +14 -45
- package/fesm/providers/local-provider.js +0 -43
- package/fesm/providers/s3-provider.optional.js +23 -47
- package/fesm/providers/storage-factory.service.js +52 -97
- package/fesm/providers/storage-provider.registry.js +10 -20
- package/fesm/services/file-manager.service.js +237 -335
- package/fesm/services/folder.service.js +1 -1
- package/fesm/services/index.js +1 -0
- package/fesm/{config → services}/storage-config.service.js +30 -79
- package/fesm/services/storage-datasource.provider.js +16 -26
- package/fesm/services/storage-provider-config.service.js +1 -1
- package/fesm/services/upload.service.js +31 -59
- package/fesm/utils/file-validator.util.js +54 -66
- package/fesm/utils/image-compressor.util.js +2 -5
- package/interfaces/storage-config.interface.d.ts +1 -2
- package/interfaces/storage-module-options.interface.d.ts +0 -5
- package/middlewares/file-serve.middleware.d.ts +9 -1
- package/modules/storage.module.d.ts +1 -2
- package/package.json +3 -3
- package/providers/azure-provider.optional.d.ts +8 -6
- package/providers/local-provider.d.ts +0 -7
- package/providers/s3-provider.optional.d.ts +9 -7
- package/providers/storage-factory.service.d.ts +8 -9
- package/providers/storage-provider.registry.d.ts +4 -4
- package/services/file-manager.service.d.ts +21 -14
- package/services/folder.service.d.ts +4 -4
- package/services/index.d.ts +1 -0
- package/{config → services}/storage-config.service.d.ts +9 -10
- package/services/storage-datasource.provider.d.ts +3 -4
- package/services/storage-provider-config.service.d.ts +5 -6
- package/services/upload.service.d.ts +5 -5
- package/utils/file-validator.util.d.ts +3 -0
- package/cjs/entities/file-manager-base.entity.js +0 -115
- package/cjs/entities/folder-base.entity.js +0 -55
- package/cjs/entities/storage-config-base.entity.js +0 -93
- package/entities/file-manager-base.entity.d.ts +0 -13
- package/entities/folder-base.entity.d.ts +0 -5
- package/entities/storage-config-base.entity.d.ts +0 -9
- package/fesm/entities/file-manager-base.entity.js +0 -108
- package/fesm/entities/folder-base.entity.js +0 -48
- package/fesm/entities/storage-config-base.entity.js +0 -83
|
@@ -12,7 +12,7 @@ const _classes = require("@flusys/nestjs-shared/classes");
|
|
|
12
12
|
const _modules = require("@flusys/nestjs-shared/modules");
|
|
13
13
|
const _utils = require("@flusys/nestjs-shared/utils");
|
|
14
14
|
const _common = require("@nestjs/common");
|
|
15
|
-
const
|
|
15
|
+
const _storageconfigservice = require("./storage-config.service");
|
|
16
16
|
const _entities = require("../entities");
|
|
17
17
|
const _storagedatasourceprovider = require("./storage-datasource.provider");
|
|
18
18
|
function _define_property(obj, key, value) {
|
|
@@ -93,13 +93,13 @@ FolderService = _ts_decorate([
|
|
|
93
93
|
}),
|
|
94
94
|
_ts_param(0, (0, _common.Inject)('CACHE_INSTANCE')),
|
|
95
95
|
_ts_param(1, (0, _common.Inject)(_modules.UtilsService)),
|
|
96
|
-
_ts_param(2, (0, _common.Inject)(
|
|
96
|
+
_ts_param(2, (0, _common.Inject)(_storageconfigservice.StorageConfigService)),
|
|
97
97
|
_ts_param(3, (0, _common.Inject)(_storagedatasourceprovider.StorageDataSourceProvider)),
|
|
98
98
|
_ts_metadata("design:type", Function),
|
|
99
99
|
_ts_metadata("design:paramtypes", [
|
|
100
100
|
typeof _classes.HybridCache === "undefined" ? Object : _classes.HybridCache,
|
|
101
101
|
typeof _modules.UtilsService === "undefined" ? Object : _modules.UtilsService,
|
|
102
|
-
typeof
|
|
102
|
+
typeof _storageconfigservice.StorageConfigService === "undefined" ? Object : _storageconfigservice.StorageConfigService,
|
|
103
103
|
typeof _storagedatasourceprovider.StorageDataSourceProvider === "undefined" ? Object : _storagedatasourceprovider.StorageDataSourceProvider
|
|
104
104
|
])
|
|
105
105
|
], FolderService);
|
package/cjs/services/index.js
CHANGED
|
@@ -4,6 +4,7 @@ Object.defineProperty(exports, "__esModule", {
|
|
|
4
4
|
});
|
|
5
5
|
_export_star(require("./file-manager.service"), exports);
|
|
6
6
|
_export_star(require("./folder.service"), exports);
|
|
7
|
+
_export_star(require("./storage-config.service"), exports);
|
|
7
8
|
_export_star(require("./storage-provider-config.service"), exports);
|
|
8
9
|
_export_star(require("./storage-datasource.provider"), exports);
|
|
9
10
|
_export_star(require("./upload.service"), exports);
|
|
@@ -10,7 +10,7 @@ Object.defineProperty(exports, "StorageConfigService", {
|
|
|
10
10
|
});
|
|
11
11
|
const _common = require("@nestjs/common");
|
|
12
12
|
const _interfaces = require("../interfaces");
|
|
13
|
-
const _storageconstants = require("
|
|
13
|
+
const _storageconstants = require("../config/storage.constants");
|
|
14
14
|
function _define_property(obj, key, value) {
|
|
15
15
|
if (key in obj) {
|
|
16
16
|
Object.defineProperty(obj, key, {
|
|
@@ -38,119 +38,70 @@ function _ts_param(paramIndex, decorator) {
|
|
|
38
38
|
decorator(target, key, paramIndex);
|
|
39
39
|
};
|
|
40
40
|
}
|
|
41
|
+
const BYTES_PER_MB = 1024 * 1024;
|
|
42
|
+
const DEFAULT_LOCAL_PATH = './uploads';
|
|
43
|
+
const DEFAULT_PORT = '3000';
|
|
41
44
|
let StorageConfigService = class StorageConfigService {
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
*/ isCompanyFeatureEnabled() {
|
|
45
|
+
// ─── IModuleConfigService Implementation ────────────────────────────────────
|
|
46
|
+
isCompanyFeatureEnabled() {
|
|
45
47
|
return this.options.bootstrapAppConfig?.enableCompanyFeature ?? false;
|
|
46
48
|
}
|
|
47
|
-
|
|
48
|
-
* Get database mode
|
|
49
|
-
*/ getDatabaseMode() {
|
|
49
|
+
getDatabaseMode() {
|
|
50
50
|
return this.options.bootstrapAppConfig?.databaseMode ?? 'single';
|
|
51
51
|
}
|
|
52
|
-
|
|
53
|
-
* Check if running in multi-tenant mode
|
|
54
|
-
*/ isMultiTenant() {
|
|
52
|
+
isMultiTenant() {
|
|
55
53
|
return this.getDatabaseMode() === 'multi-tenant';
|
|
56
54
|
}
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
*/ getMaxFileSize() {
|
|
55
|
+
// ─── Config Getters ─────────────────────────────────────────────────────────
|
|
56
|
+
getMaxFileSize() {
|
|
60
57
|
return this.options.config?.maxFileSize ?? _storageconstants.DEFAULT_MAX_FILE_SIZE;
|
|
61
58
|
}
|
|
62
|
-
|
|
63
|
-
* Get allowed file types (MIME types or patterns)
|
|
64
|
-
*/ getAllowedFileTypes() {
|
|
59
|
+
getAllowedFileTypes() {
|
|
65
60
|
return this.options.config?.allowedFileTypes ?? _storageconstants.DEFAULT_ALLOWED_FILE_TYPES;
|
|
66
61
|
}
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
62
|
+
getOptions() {
|
|
63
|
+
return this.options;
|
|
64
|
+
}
|
|
65
|
+
getDefaultLocalStoragePath() {
|
|
66
|
+
return this.options.config?.localStoragePath ?? DEFAULT_LOCAL_PATH;
|
|
67
|
+
}
|
|
68
|
+
getAppUrl() {
|
|
69
|
+
return this.options.config?.appUrl ?? process.env.APP_URL ?? `http://localhost:${process.env.PORT ?? DEFAULT_PORT}`;
|
|
70
|
+
}
|
|
71
|
+
// ─── Validation Methods ─────────────────────────────────────────────────────
|
|
72
|
+
isFileTypeAllowed(mimeType) {
|
|
70
73
|
const allowedTypes = this.getAllowedFileTypes();
|
|
71
|
-
// If wildcard, allow all
|
|
72
74
|
if (allowedTypes.includes('*/*')) {
|
|
73
75
|
return true;
|
|
74
76
|
}
|
|
75
|
-
|
|
76
|
-
return allowedTypes.some((allowedType)=>{
|
|
77
|
-
if (allowedType.endsWith('/*')) {
|
|
78
|
-
const prefix = allowedType.slice(0, -2);
|
|
79
|
-
return mimeType.startsWith(prefix);
|
|
80
|
-
}
|
|
81
|
-
return allowedType === mimeType;
|
|
82
|
-
});
|
|
77
|
+
return allowedTypes.some((type)=>type.endsWith('/*') ? mimeType.startsWith(type.slice(0, -2)) : type === mimeType);
|
|
83
78
|
}
|
|
84
|
-
|
|
85
|
-
* Validate file size
|
|
86
|
-
*/ validateFileSize(fileSize) {
|
|
79
|
+
validateFileSize(fileSize) {
|
|
87
80
|
const maxSize = this.getMaxFileSize();
|
|
88
81
|
if (fileSize > maxSize) {
|
|
89
|
-
const maxSizeMB = (maxSize / (1024 * 1024)).toFixed(2);
|
|
90
|
-
const fileSizeMB = (fileSize / (1024 * 1024)).toFixed(2);
|
|
91
82
|
return {
|
|
92
83
|
valid: false,
|
|
93
|
-
message: `File size (${
|
|
84
|
+
message: `File size (${this.toMB(fileSize)}MB) exceeds maximum ${this.toMB(maxSize)}MB`
|
|
94
85
|
};
|
|
95
86
|
}
|
|
96
87
|
return {
|
|
97
88
|
valid: true
|
|
98
89
|
};
|
|
99
90
|
}
|
|
100
|
-
|
|
101
|
-
* Validate file type
|
|
102
|
-
*/ validateFileType(mimeType) {
|
|
91
|
+
validateFileType(mimeType) {
|
|
103
92
|
if (!this.isFileTypeAllowed(mimeType)) {
|
|
104
|
-
const allowedTypes = this.getAllowedFileTypes();
|
|
105
93
|
return {
|
|
106
94
|
valid: false,
|
|
107
|
-
message: `File type "${mimeType}"
|
|
95
|
+
message: `File type "${mimeType}" not allowed. Allowed: ${this.getAllowedFileTypes().join(', ')}`
|
|
108
96
|
};
|
|
109
97
|
}
|
|
110
98
|
return {
|
|
111
99
|
valid: true
|
|
112
100
|
};
|
|
113
101
|
}
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
return this.options.config?.defaultDatabaseConfig;
|
|
118
|
-
}
|
|
119
|
-
/**
|
|
120
|
-
* Get all options
|
|
121
|
-
*/ getOptions() {
|
|
122
|
-
return this.options;
|
|
123
|
-
}
|
|
124
|
-
/**
|
|
125
|
-
* Get default local storage base path
|
|
126
|
-
* Used for serving local files without database lookup
|
|
127
|
-
* Falls back to './uploads' if not configured
|
|
128
|
-
*/ getDefaultLocalStoragePath() {
|
|
129
|
-
// Check if localStoragePath is configured in module options
|
|
130
|
-
const configuredPath = this.options.config?.localStoragePath;
|
|
131
|
-
if (configuredPath) {
|
|
132
|
-
return configuredPath;
|
|
133
|
-
}
|
|
134
|
-
// Default to ./uploads in the project root
|
|
135
|
-
return './uploads';
|
|
136
|
-
}
|
|
137
|
-
/**
|
|
138
|
-
* Get application base URL for generating file URLs
|
|
139
|
-
* - First tries module config
|
|
140
|
-
* - Falls back to APP_URL env var
|
|
141
|
-
* - Finally constructs from PORT env var
|
|
142
|
-
*/ getAppUrl() {
|
|
143
|
-
// Try from module config first
|
|
144
|
-
if (this.options.config?.appUrl) {
|
|
145
|
-
return this.options.config.appUrl;
|
|
146
|
-
}
|
|
147
|
-
// Fallback: read directly from environment
|
|
148
|
-
if (process.env.APP_URL) {
|
|
149
|
-
return process.env.APP_URL;
|
|
150
|
-
}
|
|
151
|
-
// Last resort: construct from PORT
|
|
152
|
-
const port = process.env.PORT || '3000';
|
|
153
|
-
return `http://localhost:${port}`;
|
|
102
|
+
// ─── Private Helpers ────────────────────────────────────────────────────────
|
|
103
|
+
toMB(bytes) {
|
|
104
|
+
return (bytes / BYTES_PER_MB).toFixed(2);
|
|
154
105
|
}
|
|
155
106
|
constructor(options){
|
|
156
107
|
_define_property(this, "options", void 0);
|
|
@@ -12,8 +12,7 @@ const _modules = require("@flusys/nestjs-shared/modules");
|
|
|
12
12
|
const _common = require("@nestjs/common");
|
|
13
13
|
const _core = require("@nestjs/core");
|
|
14
14
|
const _express = require("express");
|
|
15
|
-
const
|
|
16
|
-
const _storageconstants = require("../config/storage.constants");
|
|
15
|
+
const _storageconfigservice = require("./storage-config.service");
|
|
17
16
|
function _define_property(obj, key, value) {
|
|
18
17
|
if (key in obj) {
|
|
19
18
|
Object.defineProperty(obj, key, {
|
|
@@ -93,14 +92,11 @@ let StorageDataSourceProvider = class StorageDataSourceProvider extends _modules
|
|
|
93
92
|
};
|
|
94
93
|
}
|
|
95
94
|
// Feature Flags
|
|
96
|
-
/** Get global enable company feature flag */ getEnableCompanyFeature() {
|
|
97
|
-
return this.storageOptions.bootstrapAppConfig?.enableCompanyFeature ?? false;
|
|
98
|
-
}
|
|
99
95
|
/**
|
|
100
96
|
* Get enable company feature for specific tenant
|
|
101
97
|
* Falls back to global setting if not specified per-tenant
|
|
102
98
|
*/ getEnableCompanyFeatureForTenant(tenant) {
|
|
103
|
-
return tenant?.enableCompanyFeature ?? this.
|
|
99
|
+
return tenant?.enableCompanyFeature ?? this.configService.isCompanyFeatureEnabled();
|
|
104
100
|
}
|
|
105
101
|
/**
|
|
106
102
|
* Get enable company feature for current request context
|
|
@@ -113,22 +109,16 @@ let StorageDataSourceProvider = class StorageDataSourceProvider extends _modules
|
|
|
113
109
|
* For TypeORM repositories, we always use the base entities (FileManager, etc.)
|
|
114
110
|
* but for migrations, we need the correct entity based on the feature flag.
|
|
115
111
|
*/ async getStorageEntities(enableCompanyFeature) {
|
|
116
|
-
const enable = enableCompanyFeature ?? this.
|
|
117
|
-
const
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
];
|
|
127
|
-
}
|
|
128
|
-
return [
|
|
129
|
-
FileManager,
|
|
130
|
-
Folder,
|
|
131
|
-
StorageConfig
|
|
112
|
+
const enable = enableCompanyFeature ?? this.configService.isCompanyFeatureEnabled();
|
|
113
|
+
const entities = await Promise.resolve().then(()=>/*#__PURE__*/ _interop_require_wildcard(require("../entities")));
|
|
114
|
+
return enable ? [
|
|
115
|
+
entities.FileManagerWithCompany,
|
|
116
|
+
entities.FolderWithCompany,
|
|
117
|
+
entities.StorageConfigWithCompany
|
|
118
|
+
] : [
|
|
119
|
+
entities.FileManager,
|
|
120
|
+
entities.Folder,
|
|
121
|
+
entities.StorageConfig
|
|
132
122
|
];
|
|
133
123
|
}
|
|
134
124
|
// Overrides
|
|
@@ -189,8 +179,8 @@ let StorageDataSourceProvider = class StorageDataSourceProvider extends _modules
|
|
|
189
179
|
StorageDataSourceProvider.connectionLocks.delete(tenant.id);
|
|
190
180
|
}
|
|
191
181
|
}
|
|
192
|
-
constructor(
|
|
193
|
-
super(StorageDataSourceProvider.buildParentOptions(
|
|
182
|
+
constructor(configService, request){
|
|
183
|
+
super(StorageDataSourceProvider.buildParentOptions(configService.getOptions()), request), _define_property(this, "configService", void 0), _define_property(this, "logger", void 0), this.configService = configService, this.logger = new _common.Logger(StorageDataSourceProvider.name);
|
|
194
184
|
}
|
|
195
185
|
};
|
|
196
186
|
// Override parent's static properties to have Storage-specific cache
|
|
@@ -204,12 +194,12 @@ StorageDataSourceProvider = _ts_decorate([
|
|
|
204
194
|
(0, _common.Injectable)({
|
|
205
195
|
scope: _common.Scope.REQUEST
|
|
206
196
|
}),
|
|
207
|
-
_ts_param(0, (0, _common.Inject)(
|
|
197
|
+
_ts_param(0, (0, _common.Inject)(_storageconfigservice.StorageConfigService)),
|
|
208
198
|
_ts_param(1, (0, _common.Optional)()),
|
|
209
199
|
_ts_param(1, (0, _common.Inject)(_core.REQUEST)),
|
|
210
200
|
_ts_metadata("design:type", Function),
|
|
211
201
|
_ts_metadata("design:paramtypes", [
|
|
212
|
-
typeof
|
|
202
|
+
typeof _storageconfigservice.StorageConfigService === "undefined" ? Object : _storageconfigservice.StorageConfigService,
|
|
213
203
|
typeof _express.Request === "undefined" ? Object : _express.Request
|
|
214
204
|
])
|
|
215
205
|
], StorageDataSourceProvider);
|
|
@@ -12,7 +12,7 @@ const _classes = require("@flusys/nestjs-shared/classes");
|
|
|
12
12
|
const _modules = require("@flusys/nestjs-shared/modules");
|
|
13
13
|
const _utils = require("@flusys/nestjs-shared/utils");
|
|
14
14
|
const _common = require("@nestjs/common");
|
|
15
|
-
const
|
|
15
|
+
const _storageconfigservice = require("./storage-config.service");
|
|
16
16
|
const _entities = require("../entities");
|
|
17
17
|
const _storagedatasourceprovider = require("./storage-datasource.provider");
|
|
18
18
|
function _define_property(obj, key, value) {
|
|
@@ -139,13 +139,13 @@ StorageProviderConfigService = _ts_decorate([
|
|
|
139
139
|
}),
|
|
140
140
|
_ts_param(0, (0, _common.Inject)('CACHE_INSTANCE')),
|
|
141
141
|
_ts_param(1, (0, _common.Inject)(_modules.UtilsService)),
|
|
142
|
-
_ts_param(2, (0, _common.Inject)(
|
|
142
|
+
_ts_param(2, (0, _common.Inject)(_storageconfigservice.StorageConfigService)),
|
|
143
143
|
_ts_param(3, (0, _common.Inject)(_storagedatasourceprovider.StorageDataSourceProvider)),
|
|
144
144
|
_ts_metadata("design:type", Function),
|
|
145
145
|
_ts_metadata("design:paramtypes", [
|
|
146
146
|
typeof _classes.HybridCache === "undefined" ? Object : _classes.HybridCache,
|
|
147
147
|
typeof _modules.UtilsService === "undefined" ? Object : _modules.UtilsService,
|
|
148
|
-
typeof
|
|
148
|
+
typeof _storageconfigservice.StorageConfigService === "undefined" ? Object : _storageconfigservice.StorageConfigService,
|
|
149
149
|
typeof _storagedatasourceprovider.StorageDataSourceProvider === "undefined" ? Object : _storagedatasourceprovider.StorageDataSourceProvider
|
|
150
150
|
])
|
|
151
151
|
], StorageProviderConfigService);
|
|
@@ -10,7 +10,7 @@ Object.defineProperty(exports, "UploadService", {
|
|
|
10
10
|
});
|
|
11
11
|
const _utils = require("@flusys/nestjs-shared/utils");
|
|
12
12
|
const _common = require("@nestjs/common");
|
|
13
|
-
const
|
|
13
|
+
const _storageconfigservice = require("./storage-config.service");
|
|
14
14
|
const _filelocationenum = require("../enums/file-location.enum");
|
|
15
15
|
const _storagefactoryservice = require("../providers/storage-factory.service");
|
|
16
16
|
const _storageproviderconfigservice = require("./storage-provider-config.service");
|
|
@@ -72,6 +72,25 @@ let UploadService = class UploadService {
|
|
|
72
72
|
file.originalname = _filevalidatorutil.FileValidator.sanitizeFilename(file.originalname);
|
|
73
73
|
}
|
|
74
74
|
/**
|
|
75
|
+
* Create provider from storage config entity
|
|
76
|
+
*/ async createProviderFromConfig(config) {
|
|
77
|
+
return this.storageFactory.createProvider({
|
|
78
|
+
provider: config.storage,
|
|
79
|
+
config: config.config
|
|
80
|
+
});
|
|
81
|
+
}
|
|
82
|
+
/**
|
|
83
|
+
* Create fallback local provider
|
|
84
|
+
*/ async createFallbackLocalProvider() {
|
|
85
|
+
this.logger.warn('No storage config found, using fallback local provider');
|
|
86
|
+
return this.storageFactory.createProvider({
|
|
87
|
+
provider: _filelocationenum.FileLocationEnum.LOCAL,
|
|
88
|
+
config: {
|
|
89
|
+
basePath: this.storageConfigService.getDefaultLocalStoragePath()
|
|
90
|
+
}
|
|
91
|
+
});
|
|
92
|
+
}
|
|
93
|
+
/**
|
|
75
94
|
* Get storage provider and config info based on storage config ID
|
|
76
95
|
* Validates company ownership when company feature is enabled
|
|
77
96
|
*/ async getStorageProviderWithConfig(storageConfigId, user) {
|
|
@@ -94,13 +113,7 @@ let UploadService = class UploadService {
|
|
|
94
113
|
}
|
|
95
114
|
storageConfig = defaultConfig;
|
|
96
115
|
}
|
|
97
|
-
|
|
98
|
-
const providerConfig = {
|
|
99
|
-
provider: storageConfig.storage,
|
|
100
|
-
config: storageConfig.config
|
|
101
|
-
};
|
|
102
|
-
// Get or create provider instance
|
|
103
|
-
const provider = await this.storageFactory.createProvider(providerConfig);
|
|
116
|
+
const provider = await this.createProviderFromConfig(storageConfig);
|
|
104
117
|
return {
|
|
105
118
|
provider,
|
|
106
119
|
location: storageConfig.storage,
|
|
@@ -108,74 +121,33 @@ let UploadService = class UploadService {
|
|
|
108
121
|
};
|
|
109
122
|
}
|
|
110
123
|
/**
|
|
111
|
-
* Get storage provider based on storage config ID (convenience method)
|
|
112
|
-
*/ async getStorageProvider(storageConfigId, user) {
|
|
113
|
-
const { provider } = await this.getStorageProviderWithConfig(storageConfigId, user);
|
|
114
|
-
return provider;
|
|
115
|
-
}
|
|
116
|
-
/**
|
|
117
124
|
* Get storage provider for delete operations with fallback
|
|
118
125
|
* If the original storage config doesn't exist, falls back based on locationHint
|
|
119
|
-
* This ensures files can still be deleted even if storage config was removed
|
|
120
|
-
* @param storageConfigId - The storage config ID to look up
|
|
121
|
-
* @param user - User context for company filtering
|
|
122
|
-
* @param locationHint - The file's original location type (used for fallback)
|
|
123
126
|
*/ async getStorageProviderForDelete(storageConfigId, user, locationHint) {
|
|
124
|
-
//
|
|
127
|
+
// Try to find by storageConfigId
|
|
125
128
|
if (storageConfigId) {
|
|
126
129
|
const config = await this.storageProviderConfigService.findByIdDirect(storageConfigId);
|
|
127
130
|
if (config) {
|
|
128
|
-
|
|
129
|
-
provider: config.storage,
|
|
130
|
-
config: config.config
|
|
131
|
-
};
|
|
132
|
-
return await this.storageFactory.createProvider(providerConfig);
|
|
131
|
+
return this.createProviderFromConfig(config);
|
|
133
132
|
}
|
|
134
|
-
|
|
135
|
-
this.logger.warn(`Storage config ${storageConfigId} not found, trying fallback for delete`);
|
|
133
|
+
this.logger.warn(`Storage config ${storageConfigId} not found, trying fallback`);
|
|
136
134
|
}
|
|
137
|
-
// Fallback: Use locationHint to find a matching config
|
|
135
|
+
// Fallback: Use locationHint to find a matching config
|
|
138
136
|
if (locationHint) {
|
|
139
|
-
// Try to find a config matching the file's original location type
|
|
140
137
|
const matchingConfigs = await this.storageProviderConfigService.getConfigByType(locationHint, user);
|
|
141
138
|
if (matchingConfigs.length > 0) {
|
|
142
|
-
|
|
143
|
-
provider: matchingConfigs[0].storage,
|
|
144
|
-
config: matchingConfigs[0].config
|
|
145
|
-
};
|
|
146
|
-
this.logger.debug(`Using matching ${locationHint} config for delete fallback`);
|
|
147
|
-
return await this.storageFactory.createProvider(providerConfig);
|
|
139
|
+
return this.createProviderFromConfig(matchingConfigs[0]);
|
|
148
140
|
}
|
|
149
|
-
// No matching config found, create a basic provider based on locationHint
|
|
150
141
|
if (locationHint === _filelocationenum.FileLocationEnum.LOCAL) {
|
|
151
|
-
this.
|
|
152
|
-
const localProviderConfig = {
|
|
153
|
-
provider: _filelocationenum.FileLocationEnum.LOCAL,
|
|
154
|
-
config: {
|
|
155
|
-
basePath: this.storageConfigService.getDefaultLocalStoragePath()
|
|
156
|
-
}
|
|
157
|
-
};
|
|
158
|
-
return await this.storageFactory.createProvider(localProviderConfig);
|
|
142
|
+
return this.createFallbackLocalProvider();
|
|
159
143
|
}
|
|
160
144
|
}
|
|
161
|
-
// Last resort: Try default config
|
|
145
|
+
// Last resort: Try default config
|
|
162
146
|
const defaultConfig = await this.storageProviderConfigService.getDefaultConfig(user);
|
|
163
147
|
if (defaultConfig) {
|
|
164
|
-
|
|
165
|
-
provider: defaultConfig.storage,
|
|
166
|
-
config: defaultConfig.config
|
|
167
|
-
};
|
|
168
|
-
return await this.storageFactory.createProvider(providerConfig);
|
|
148
|
+
return this.createProviderFromConfig(defaultConfig);
|
|
169
149
|
}
|
|
170
|
-
|
|
171
|
-
this.logger.warn('No storage config found, using fallback local provider for delete');
|
|
172
|
-
const localProviderConfig = {
|
|
173
|
-
provider: _filelocationenum.FileLocationEnum.LOCAL,
|
|
174
|
-
config: {
|
|
175
|
-
basePath: this.storageConfigService.getDefaultLocalStoragePath()
|
|
176
|
-
}
|
|
177
|
-
};
|
|
178
|
-
return await this.storageFactory.createProvider(localProviderConfig);
|
|
150
|
+
return this.createFallbackLocalProvider();
|
|
179
151
|
}
|
|
180
152
|
async uploadSingleFile(file, options, user) {
|
|
181
153
|
try {
|
|
@@ -273,7 +245,7 @@ let UploadService = class UploadService {
|
|
|
273
245
|
* For Local/SFTP: returns direct path
|
|
274
246
|
*/ async makeFileUrl(key, storageConfigId, expiresIn = 3600, user) {
|
|
275
247
|
try {
|
|
276
|
-
const provider = await this.
|
|
248
|
+
const { provider } = await this.getStorageProviderWithConfig(storageConfigId, user);
|
|
277
249
|
// Check if provider supports presigned URLs
|
|
278
250
|
if (provider.generatePresignedUrl) {
|
|
279
251
|
return await provider.generatePresignedUrl(key, expiresIn);
|
|
@@ -302,12 +274,12 @@ UploadService = _ts_decorate([
|
|
|
302
274
|
scope: _common.Scope.REQUEST
|
|
303
275
|
}),
|
|
304
276
|
_ts_param(0, (0, _common.Inject)(_storagefactoryservice.StorageFactoryService)),
|
|
305
|
-
_ts_param(1, (0, _common.Inject)(
|
|
277
|
+
_ts_param(1, (0, _common.Inject)(_storageconfigservice.StorageConfigService)),
|
|
306
278
|
_ts_param(2, (0, _common.Inject)(_storageproviderconfigservice.StorageProviderConfigService)),
|
|
307
279
|
_ts_metadata("design:type", Function),
|
|
308
280
|
_ts_metadata("design:paramtypes", [
|
|
309
281
|
typeof _storagefactoryservice.StorageFactoryService === "undefined" ? Object : _storagefactoryservice.StorageFactoryService,
|
|
310
|
-
typeof
|
|
282
|
+
typeof _storageconfigservice.StorageConfigService === "undefined" ? Object : _storageconfigservice.StorageConfigService,
|
|
311
283
|
typeof _storageproviderconfigservice.StorageProviderConfigService === "undefined" ? Object : _storageproviderconfigservice.StorageProviderConfigService
|
|
312
284
|
])
|
|
313
285
|
], UploadService);
|
|
@@ -303,7 +303,30 @@ function _define_property(obj, key, value) {
|
|
|
303
303
|
'image/svg+xml',
|
|
304
304
|
'application/xhtml+xml'
|
|
305
305
|
];
|
|
306
|
+
/**
|
|
307
|
+
* ZIP-based format prefixes that are valid when detected as application/zip.
|
|
308
|
+
*/ const ZIP_VARIANT_PREFIXES = [
|
|
309
|
+
'application/vnd.openxmlformats-officedocument',
|
|
310
|
+
'application/x-zip',
|
|
311
|
+
'application/x-compressed'
|
|
312
|
+
];
|
|
306
313
|
let FileValidator = class FileValidator {
|
|
314
|
+
// Result Helpers
|
|
315
|
+
static failureResult(message, detectedType, declaredType) {
|
|
316
|
+
return {
|
|
317
|
+
valid: false,
|
|
318
|
+
detectedType,
|
|
319
|
+
declaredType,
|
|
320
|
+
message
|
|
321
|
+
};
|
|
322
|
+
}
|
|
323
|
+
static successResult(detectedType, declaredType) {
|
|
324
|
+
return {
|
|
325
|
+
valid: true,
|
|
326
|
+
detectedType,
|
|
327
|
+
declaredType
|
|
328
|
+
};
|
|
329
|
+
}
|
|
307
330
|
/**
|
|
308
331
|
* Detect file type from buffer using magic bytes.
|
|
309
332
|
* @param buffer - File buffer to analyze
|
|
@@ -349,15 +372,8 @@ let FileValidator = class FileValidator {
|
|
|
349
372
|
const detectedCategory = detected.split('/')[0];
|
|
350
373
|
const declaredCategory = declared.split('/')[0];
|
|
351
374
|
// For ZIP-based formats, allow any ZIP-detected file if declared is a ZIP variant
|
|
352
|
-
if (detected === 'application/zip') {
|
|
353
|
-
|
|
354
|
-
'application/vnd.openxmlformats-officedocument',
|
|
355
|
-
'application/x-zip',
|
|
356
|
-
'application/x-compressed'
|
|
357
|
-
];
|
|
358
|
-
if (zipVariants.some((v)=>declared.startsWith(v))) {
|
|
359
|
-
return true;
|
|
360
|
-
}
|
|
375
|
+
if (detected === 'application/zip' && ZIP_VARIANT_PREFIXES.some((v)=>declared.startsWith(v))) {
|
|
376
|
+
return true;
|
|
361
377
|
}
|
|
362
378
|
return detectedCategory === declaredCategory;
|
|
363
379
|
}
|
|
@@ -385,74 +401,46 @@ let FileValidator = class FileValidator {
|
|
|
385
401
|
'*/*'
|
|
386
402
|
]) {
|
|
387
403
|
try {
|
|
388
|
-
// Detect actual file type from magic bytes
|
|
389
404
|
const detectedType = this.detectFileType(buffer);
|
|
390
|
-
//
|
|
405
|
+
// No magic bytes detected - handle text-based types
|
|
391
406
|
if (!detectedType) {
|
|
392
|
-
|
|
393
|
-
if (this.isDangerousTextType(declaredMimeType)) {
|
|
394
|
-
// Only allow dangerous types if explicitly in allowedTypes (not via wildcard)
|
|
395
|
-
const explicitlyAllowed = allowedTypes.some((t)=>t === declaredMimeType && t !== '*/*' && !t.endsWith('/*'));
|
|
396
|
-
if (!explicitlyAllowed) {
|
|
397
|
-
this.logger.warn(`Blocked dangerous file type: ${declaredMimeType} - requires explicit allowlisting`);
|
|
398
|
-
return {
|
|
399
|
-
valid: false,
|
|
400
|
-
detectedType: declaredMimeType,
|
|
401
|
-
declaredType: declaredMimeType,
|
|
402
|
-
message: `File type "${declaredMimeType}" is potentially dangerous and not explicitly allowed`
|
|
403
|
-
};
|
|
404
|
-
}
|
|
405
|
-
this.logger.warn(`Allowing explicitly permitted dangerous file type: ${declaredMimeType}`);
|
|
406
|
-
}
|
|
407
|
-
if (this.isTextBasedType(declaredMimeType)) {
|
|
408
|
-
// Safe text-based files don't have magic bytes, trust the declared type
|
|
409
|
-
const isAllowed = this.isTypeAllowed(declaredMimeType, allowedTypes);
|
|
410
|
-
return {
|
|
411
|
-
valid: isAllowed,
|
|
412
|
-
detectedType: declaredMimeType,
|
|
413
|
-
declaredType: declaredMimeType,
|
|
414
|
-
message: isAllowed ? undefined : `File type "${declaredMimeType}" is not allowed`
|
|
415
|
-
};
|
|
416
|
-
}
|
|
417
|
-
// For binary files without recognized signatures, be cautious
|
|
418
|
-
this.logger.warn(`Unable to detect file type for declared type: ${declaredMimeType}`);
|
|
419
|
-
return {
|
|
420
|
-
valid: false,
|
|
421
|
-
declaredType: declaredMimeType,
|
|
422
|
-
message: 'Unable to verify file type. File may be corrupted or unsupported.'
|
|
423
|
-
};
|
|
407
|
+
return this.validateUndetectedType(declaredMimeType, allowedTypes);
|
|
424
408
|
}
|
|
425
|
-
//
|
|
409
|
+
// Verify detected type matches declared type
|
|
426
410
|
if (!this.mimeTypesMatch(detectedType, declaredMimeType)) {
|
|
427
411
|
this.logger.warn(`MIME type mismatch: declared=${declaredMimeType}, detected=${detectedType}`);
|
|
428
|
-
return {
|
|
429
|
-
valid: false,
|
|
430
|
-
detectedType,
|
|
431
|
-
declaredType: declaredMimeType,
|
|
432
|
-
message: `File content does not match declared type. Detected: ${detectedType}, Declared: ${declaredMimeType}`
|
|
433
|
-
};
|
|
412
|
+
return this.failureResult(`File content does not match declared type. Detected: ${detectedType}, Declared: ${declaredMimeType}`, detectedType, declaredMimeType);
|
|
434
413
|
}
|
|
435
|
-
//
|
|
414
|
+
// Verify type is in allowed list
|
|
436
415
|
if (!this.isTypeAllowed(detectedType, allowedTypes)) {
|
|
437
|
-
return {
|
|
438
|
-
valid: false,
|
|
439
|
-
detectedType,
|
|
440
|
-
declaredType: declaredMimeType,
|
|
441
|
-
message: `File type "${detectedType}" is not allowed`
|
|
442
|
-
};
|
|
416
|
+
return this.failureResult(`File type "${detectedType}" is not allowed`, detectedType, declaredMimeType);
|
|
443
417
|
}
|
|
444
|
-
return
|
|
445
|
-
valid: true,
|
|
446
|
-
detectedType,
|
|
447
|
-
declaredType: declaredMimeType
|
|
448
|
-
};
|
|
418
|
+
return this.successResult(detectedType, declaredMimeType);
|
|
449
419
|
} catch (error) {
|
|
450
420
|
this.logger.error('File validation error:', error);
|
|
451
|
-
return
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
421
|
+
return this.failureResult('File validation failed');
|
|
422
|
+
}
|
|
423
|
+
}
|
|
424
|
+
/**
|
|
425
|
+
* Handle validation for files without detectable magic bytes.
|
|
426
|
+
*/ static validateUndetectedType(declaredMimeType, allowedTypes) {
|
|
427
|
+
// Check for dangerous text types first (HTML, JS, SVG)
|
|
428
|
+
if (this.isDangerousTextType(declaredMimeType)) {
|
|
429
|
+
const explicitlyAllowed = allowedTypes.some((t)=>t === declaredMimeType && t !== '*/*' && !t.endsWith('/*'));
|
|
430
|
+
if (!explicitlyAllowed) {
|
|
431
|
+
this.logger.warn(`Blocked dangerous file type: ${declaredMimeType} - requires explicit allowlisting`);
|
|
432
|
+
return this.failureResult(`File type "${declaredMimeType}" is potentially dangerous and not explicitly allowed`, declaredMimeType, declaredMimeType);
|
|
433
|
+
}
|
|
434
|
+
this.logger.warn(`Allowing explicitly permitted dangerous file type: ${declaredMimeType}`);
|
|
435
|
+
}
|
|
436
|
+
// Safe text-based files don't have magic bytes, trust declared type
|
|
437
|
+
if (this.isTextBasedType(declaredMimeType)) {
|
|
438
|
+
const isAllowed = this.isTypeAllowed(declaredMimeType, allowedTypes);
|
|
439
|
+
return isAllowed ? this.successResult(declaredMimeType, declaredMimeType) : this.failureResult(`File type "${declaredMimeType}" is not allowed`, declaredMimeType, declaredMimeType);
|
|
455
440
|
}
|
|
441
|
+
// Binary files without recognized signatures - be cautious
|
|
442
|
+
this.logger.warn(`Unable to detect file type for declared type: ${declaredMimeType}`);
|
|
443
|
+
return this.failureResult('Unable to verify file type. File may be corrupted or unsupported.', undefined, declaredMimeType);
|
|
456
444
|
}
|
|
457
445
|
/**
|
|
458
446
|
* Sanitize filename to prevent path traversal and special character issues.
|
|
@@ -147,12 +147,9 @@ let ImageCompressor = class ImageCompressor {
|
|
|
147
147
|
break;
|
|
148
148
|
}
|
|
149
149
|
try {
|
|
150
|
-
|
|
151
|
-
const { data, info } = await image.toBuffer({
|
|
152
|
-
resolveWithObject: true
|
|
153
|
-
});
|
|
150
|
+
const compressedBuffer = await image.toBuffer();
|
|
154
151
|
return {
|
|
155
|
-
buffer:
|
|
152
|
+
buffer: compressedBuffer,
|
|
156
153
|
format: `image/${targetFormat}`
|
|
157
154
|
};
|
|
158
155
|
} catch {
|
package/config/index.d.ts
CHANGED
|
@@ -1,9 +1,3 @@
|
|
|
1
1
|
export declare const STORAGE_MODULE_OPTIONS = "STORAGE_MODULE_OPTIONS";
|
|
2
2
|
export declare const DEFAULT_MAX_FILE_SIZE: number;
|
|
3
3
|
export declare const DEFAULT_ALLOWED_FILE_TYPES: string[];
|
|
4
|
-
export declare const FILE_VALIDATION_MESSAGES: {
|
|
5
|
-
readonly FILE_TOO_LARGE: "File size exceeds the maximum allowed size";
|
|
6
|
-
readonly INVALID_FILE_TYPE: "File type is not allowed";
|
|
7
|
-
readonly NO_FILE_PROVIDED: "No file was provided";
|
|
8
|
-
readonly UPLOAD_FAILED: "File upload failed";
|
|
9
|
-
};
|
|
@@ -5,6 +5,7 @@ import { UploadService } from '../services/upload.service';
|
|
|
5
5
|
export declare class UploadController {
|
|
6
6
|
private readonly uploadService;
|
|
7
7
|
constructor(uploadService: UploadService);
|
|
8
|
+
private toFileUploadResponse;
|
|
8
9
|
uploadSingleFile(file: Express.Multer.File, options: UploadOptionsDto, user: ILoggedUserInfo): Promise<SingleResponseDto<FileUploadResponsePayloadDto>>;
|
|
9
10
|
uploadMultipleFiles(files: Express.Multer.File[], options: UploadOptionsDto, user: ILoggedUserInfo): Promise<SingleResponseDto<FileUploadResponsePayloadDto[]>>;
|
|
10
11
|
deleteSingleFile(dto: DeleteSingleFileDto, user: ILoggedUserInfo): Promise<SingleResponseDto<boolean>>;
|