@flusys/nestjs-storage 1.0.0-beta → 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.
- package/README.md +174 -13
- package/cjs/config/index.js +0 -1
- package/cjs/config/storage.constants.js +0 -17
- package/cjs/controllers/file-manager.controller.js +44 -1
- package/cjs/controllers/folder.controller.js +44 -1
- package/cjs/controllers/storage-config.controller.js +44 -1
- package/cjs/controllers/upload.controller.js +18 -29
- package/cjs/docs/storage-swagger.config.js +24 -136
- package/cjs/dtos/file-manager.dto.js +71 -35
- package/cjs/dtos/folder.dto.js +15 -9
- package/cjs/dtos/storage-config.dto.js +25 -66
- package/cjs/dtos/upload.dto.js +24 -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/interfaces/index.js +0 -1
- package/cjs/middlewares/file-serve.middleware.js +113 -100
- package/cjs/modules/storage.module.js +82 -136
- package/cjs/providers/azure-provider.optional.js +10 -38
- package/cjs/providers/local-provider.js +38 -31
- 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 +238 -323
- package/cjs/services/folder.service.js +8 -11
- package/cjs/services/index.js +1 -0
- package/cjs/{config → services}/storage-config.service.js +32 -76
- package/cjs/services/storage-datasource.provider.js +16 -26
- package/cjs/services/storage-provider-config.service.js +21 -38
- package/cjs/services/upload.service.js +72 -88
- package/cjs/utils/file-validator.util.js +458 -0
- package/cjs/utils/image-compressor.util.js +3 -8
- package/config/index.d.ts +0 -1
- package/config/storage.constants.d.ts +0 -8
- package/controllers/upload.controller.d.ts +3 -6
- package/dtos/file-manager.dto.d.ts +13 -7
- package/dtos/folder.dto.d.ts +5 -5
- package/dtos/storage-config.dto.d.ts +13 -14
- 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 -8
- package/fesm/controllers/file-manager.controller.js +45 -2
- package/fesm/controllers/folder.controller.js +45 -2
- package/fesm/controllers/storage-config.controller.js +45 -2
- package/fesm/controllers/upload.controller.js +19 -30
- package/fesm/docs/storage-swagger.config.js +27 -142
- package/fesm/dtos/file-manager.dto.js +72 -36
- package/fesm/dtos/folder.dto.js +16 -10
- package/fesm/dtos/storage-config.dto.js +29 -76
- package/fesm/dtos/upload.dto.js +25 -19
- 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 +5 -13
- package/fesm/entities/storage-config-with-company.entity.js +3 -4
- package/fesm/entities/storage-config.entity.js +74 -4
- package/fesm/interfaces/index.js +0 -1
- package/fesm/interfaces/storage-config.interface.js +1 -3
- package/fesm/middlewares/file-serve.middleware.js +114 -101
- package/fesm/modules/storage.module.js +83 -136
- package/fesm/providers/azure-provider.optional.js +11 -42
- package/fesm/providers/local-provider.js +38 -31
- package/fesm/providers/s3-provider.optional.js +20 -44
- 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 -322
- package/fesm/services/folder.service.js +6 -9
- package/fesm/services/index.js +1 -0
- package/fesm/{config → services}/storage-config.service.js +32 -76
- package/fesm/services/storage-datasource.provider.js +16 -26
- package/fesm/services/storage-provider-config.service.js +19 -36
- package/fesm/services/upload.service.js +71 -87
- package/fesm/utils/file-validator.util.js +451 -0
- package/fesm/utils/image-compressor.util.js +3 -8
- package/interfaces/file-manager.interface.d.ts +7 -4
- package/interfaces/index.d.ts +0 -1
- package/interfaces/storage-config.interface.d.ts +3 -22
- 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 +6 -6
- package/providers/azure-provider.optional.d.ts +8 -6
- package/providers/local-provider.d.ts +2 -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 +23 -16
- package/services/folder.service.d.ts +4 -4
- package/services/index.d.ts +1 -0
- package/services/storage-config.service.d.ts +24 -0
- 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 +19 -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 -64
- package/cjs/interfaces/file-upload-response.interface.js +0 -4
- package/config/storage-config.service.d.ts +0 -22
- 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 -7
- 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 -57
- package/fesm/interfaces/file-upload-response.interface.js +0 -1
- package/interfaces/file-upload-response.interface.d.ts +0 -6
|
@@ -25,25 +25,60 @@ function _ts_param(paramIndex, decorator) {
|
|
|
25
25
|
decorator(target, key, paramIndex);
|
|
26
26
|
};
|
|
27
27
|
}
|
|
28
|
-
import {
|
|
29
|
-
import {
|
|
28
|
+
import { ErrorHandler, validateCompanyOwnership } from '@flusys/nestjs-shared/utils';
|
|
29
|
+
import { BadRequestException, Inject, Injectable, Logger, NotFoundException, Scope } from '@nestjs/common';
|
|
30
|
+
import { StorageConfigService } from './storage-config.service';
|
|
30
31
|
import { FileLocationEnum } from '../enums/file-location.enum';
|
|
31
32
|
import { StorageFactoryService } from '../providers/storage-factory.service';
|
|
32
33
|
import { StorageProviderConfigService } from './storage-provider-config.service';
|
|
34
|
+
import { FileValidator } from '../utils/file-validator.util';
|
|
33
35
|
export class UploadService {
|
|
34
36
|
/**
|
|
35
|
-
* Validate file before upload
|
|
37
|
+
* Validate file before upload - includes size, type, and content validation.
|
|
38
|
+
* Uses magic bytes to verify file content matches declared MIME type.
|
|
36
39
|
*/ validateFile(file) {
|
|
37
40
|
// Validate file size
|
|
38
41
|
const sizeValidation = this.storageConfigService.validateFileSize(file.size);
|
|
39
42
|
if (!sizeValidation.valid) {
|
|
40
43
|
throw new BadRequestException(sizeValidation.message);
|
|
41
44
|
}
|
|
42
|
-
// Validate file type
|
|
45
|
+
// Validate declared file type (MIME)
|
|
43
46
|
const typeValidation = this.storageConfigService.validateFileType(file.mimetype);
|
|
44
47
|
if (!typeValidation.valid) {
|
|
45
48
|
throw new BadRequestException(typeValidation.message);
|
|
46
49
|
}
|
|
50
|
+
// Validate file content matches declared type (magic bytes check)
|
|
51
|
+
// This prevents MIME type spoofing attacks
|
|
52
|
+
const allowedTypes = this.storageConfigService.getAllowedFileTypes();
|
|
53
|
+
const contentValidation = FileValidator.validateFileContent(file.buffer, file.mimetype, allowedTypes);
|
|
54
|
+
if (!contentValidation.valid) {
|
|
55
|
+
this.logger.warn(`File content validation failed: ${contentValidation.message}`, {
|
|
56
|
+
declaredType: file.mimetype,
|
|
57
|
+
detectedType: contentValidation.detectedType
|
|
58
|
+
});
|
|
59
|
+
throw new BadRequestException(contentValidation.message || 'File content validation failed');
|
|
60
|
+
}
|
|
61
|
+
// Sanitize filename to prevent path traversal attacks
|
|
62
|
+
file.originalname = FileValidator.sanitizeFilename(file.originalname);
|
|
63
|
+
}
|
|
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
|
+
});
|
|
47
82
|
}
|
|
48
83
|
/**
|
|
49
84
|
* Get storage provider and config info based on storage config ID
|
|
@@ -57,13 +92,8 @@ export class UploadService {
|
|
|
57
92
|
if (!config) {
|
|
58
93
|
throw new NotFoundException('Storage configuration not found');
|
|
59
94
|
}
|
|
60
|
-
// Validate company ownership
|
|
61
|
-
|
|
62
|
-
const configWithCompany = config;
|
|
63
|
-
if (configWithCompany.companyId && configWithCompany.companyId !== user.companyId) {
|
|
64
|
-
throw new BadRequestException('Storage configuration belongs to another company');
|
|
65
|
-
}
|
|
66
|
-
}
|
|
95
|
+
// Validate company ownership using shared utility
|
|
96
|
+
validateCompanyOwnership(config, user, this.storageConfigService.isCompanyFeatureEnabled(), 'Storage configuration');
|
|
67
97
|
storageConfig = config;
|
|
68
98
|
} else {
|
|
69
99
|
// Use default config (scoped to user's company/branch)
|
|
@@ -73,13 +103,7 @@ export class UploadService {
|
|
|
73
103
|
}
|
|
74
104
|
storageConfig = defaultConfig;
|
|
75
105
|
}
|
|
76
|
-
|
|
77
|
-
const providerConfig = {
|
|
78
|
-
provider: storageConfig.storage,
|
|
79
|
-
config: storageConfig.config
|
|
80
|
-
};
|
|
81
|
-
// Get or create provider instance
|
|
82
|
-
const provider = await this.storageFactory.createProvider(providerConfig);
|
|
106
|
+
const provider = await this.createProviderFromConfig(storageConfig);
|
|
83
107
|
return {
|
|
84
108
|
provider,
|
|
85
109
|
location: storageConfig.storage,
|
|
@@ -87,74 +111,33 @@ export class UploadService {
|
|
|
87
111
|
};
|
|
88
112
|
}
|
|
89
113
|
/**
|
|
90
|
-
* Get storage provider based on storage config ID (convenience method)
|
|
91
|
-
*/ async getStorageProvider(storageConfigId, user) {
|
|
92
|
-
const { provider } = await this.getStorageProviderWithConfig(storageConfigId, user);
|
|
93
|
-
return provider;
|
|
94
|
-
}
|
|
95
|
-
/**
|
|
96
114
|
* Get storage provider for delete operations with fallback
|
|
97
115
|
* If the original storage config doesn't exist, falls back based on locationHint
|
|
98
|
-
* This ensures files can still be deleted even if storage config was removed
|
|
99
|
-
* @param storageConfigId - The storage config ID to look up
|
|
100
|
-
* @param user - User context for company filtering
|
|
101
|
-
* @param locationHint - The file's original location type (used for fallback)
|
|
102
116
|
*/ async getStorageProviderForDelete(storageConfigId, user, locationHint) {
|
|
103
|
-
//
|
|
117
|
+
// Try to find by storageConfigId
|
|
104
118
|
if (storageConfigId) {
|
|
105
119
|
const config = await this.storageProviderConfigService.findByIdDirect(storageConfigId);
|
|
106
120
|
if (config) {
|
|
107
|
-
|
|
108
|
-
provider: config.storage,
|
|
109
|
-
config: config.config
|
|
110
|
-
};
|
|
111
|
-
return await this.storageFactory.createProvider(providerConfig);
|
|
121
|
+
return this.createProviderFromConfig(config);
|
|
112
122
|
}
|
|
113
|
-
|
|
114
|
-
this.logger.warn(`Storage config ${storageConfigId} not found, trying fallback for delete`);
|
|
123
|
+
this.logger.warn(`Storage config ${storageConfigId} not found, trying fallback`);
|
|
115
124
|
}
|
|
116
|
-
// Fallback: Use locationHint to find a matching config
|
|
125
|
+
// Fallback: Use locationHint to find a matching config
|
|
117
126
|
if (locationHint) {
|
|
118
|
-
// Try to find a config matching the file's original location type
|
|
119
127
|
const matchingConfigs = await this.storageProviderConfigService.getConfigByType(locationHint, user);
|
|
120
128
|
if (matchingConfigs.length > 0) {
|
|
121
|
-
|
|
122
|
-
provider: matchingConfigs[0].storage,
|
|
123
|
-
config: matchingConfigs[0].config
|
|
124
|
-
};
|
|
125
|
-
this.logger.debug(`Using matching ${locationHint} config for delete fallback`);
|
|
126
|
-
return await this.storageFactory.createProvider(providerConfig);
|
|
129
|
+
return this.createProviderFromConfig(matchingConfigs[0]);
|
|
127
130
|
}
|
|
128
|
-
// No matching config found, create a basic provider based on locationHint
|
|
129
131
|
if (locationHint === FileLocationEnum.LOCAL) {
|
|
130
|
-
this.
|
|
131
|
-
const localProviderConfig = {
|
|
132
|
-
provider: FileLocationEnum.LOCAL,
|
|
133
|
-
config: {
|
|
134
|
-
basePath: this.storageConfigService.getDefaultLocalStoragePath()
|
|
135
|
-
}
|
|
136
|
-
};
|
|
137
|
-
return await this.storageFactory.createProvider(localProviderConfig);
|
|
132
|
+
return this.createFallbackLocalProvider();
|
|
138
133
|
}
|
|
139
134
|
}
|
|
140
|
-
// Last resort: Try default config
|
|
135
|
+
// Last resort: Try default config
|
|
141
136
|
const defaultConfig = await this.storageProviderConfigService.getDefaultConfig(user);
|
|
142
137
|
if (defaultConfig) {
|
|
143
|
-
|
|
144
|
-
provider: defaultConfig.storage,
|
|
145
|
-
config: defaultConfig.config
|
|
146
|
-
};
|
|
147
|
-
return await this.storageFactory.createProvider(providerConfig);
|
|
138
|
+
return this.createProviderFromConfig(defaultConfig);
|
|
148
139
|
}
|
|
149
|
-
|
|
150
|
-
this.logger.warn('No storage config found, using fallback local provider for delete');
|
|
151
|
-
const localProviderConfig = {
|
|
152
|
-
provider: FileLocationEnum.LOCAL,
|
|
153
|
-
config: {
|
|
154
|
-
basePath: this.storageConfigService.getDefaultLocalStoragePath()
|
|
155
|
-
}
|
|
156
|
-
};
|
|
157
|
-
return await this.storageFactory.createProvider(localProviderConfig);
|
|
140
|
+
return this.createFallbackLocalProvider();
|
|
158
141
|
}
|
|
159
142
|
async uploadSingleFile(file, options, user) {
|
|
160
143
|
try {
|
|
@@ -168,9 +151,9 @@ export class UploadService {
|
|
|
168
151
|
location,
|
|
169
152
|
storageConfigId: configId
|
|
170
153
|
};
|
|
171
|
-
} catch (
|
|
172
|
-
this.logger
|
|
173
|
-
|
|
154
|
+
} catch (error) {
|
|
155
|
+
ErrorHandler.logError(this.logger, error, 'uploadSingleFile');
|
|
156
|
+
ErrorHandler.rethrowError(error);
|
|
174
157
|
}
|
|
175
158
|
}
|
|
176
159
|
async uploadMultipleFiles(files, options, user) {
|
|
@@ -187,31 +170,31 @@ export class UploadService {
|
|
|
187
170
|
location,
|
|
188
171
|
storageConfigId: configId
|
|
189
172
|
}));
|
|
190
|
-
} catch (
|
|
191
|
-
this.logger
|
|
192
|
-
|
|
173
|
+
} catch (error) {
|
|
174
|
+
ErrorHandler.logError(this.logger, error, 'uploadMultipleFiles');
|
|
175
|
+
ErrorHandler.rethrowError(error);
|
|
193
176
|
}
|
|
194
177
|
}
|
|
195
178
|
async deleteSingleFile(key, storageConfigId, user, locationHint) {
|
|
196
179
|
try {
|
|
197
|
-
if (!key) throw new
|
|
180
|
+
if (!key) throw new BadRequestException('No file path provided');
|
|
198
181
|
const provider = await this.getStorageProviderForDelete(storageConfigId, user, locationHint);
|
|
199
182
|
await provider.deleteFile(key);
|
|
200
183
|
return true;
|
|
201
|
-
} catch (
|
|
202
|
-
this.logger
|
|
203
|
-
|
|
184
|
+
} catch (error) {
|
|
185
|
+
ErrorHandler.logError(this.logger, error, 'deleteSingleFile');
|
|
186
|
+
ErrorHandler.rethrowError(error);
|
|
204
187
|
}
|
|
205
188
|
}
|
|
206
189
|
async deleteMultipleFile(keys, storageConfigId, user, locationHint) {
|
|
207
190
|
try {
|
|
208
|
-
if (!keys || !keys.length) throw new
|
|
191
|
+
if (!keys || !keys.length) throw new BadRequestException('No file paths provided');
|
|
209
192
|
const provider = await this.getStorageProviderForDelete(storageConfigId, user, locationHint);
|
|
210
193
|
await provider.deleteMultipleFiles(keys);
|
|
211
194
|
return true;
|
|
212
|
-
} catch (
|
|
213
|
-
this.logger
|
|
214
|
-
|
|
195
|
+
} catch (error) {
|
|
196
|
+
ErrorHandler.logError(this.logger, error, 'deleteMultipleFiles');
|
|
197
|
+
ErrorHandler.rethrowError(error);
|
|
215
198
|
}
|
|
216
199
|
}
|
|
217
200
|
bytesToKb(bytes) {
|
|
@@ -241,7 +224,8 @@ export class UploadService {
|
|
|
241
224
|
}
|
|
242
225
|
return null;
|
|
243
226
|
} catch (error) {
|
|
244
|
-
|
|
227
|
+
const errorMessage = error instanceof Error ? error.message : 'Unknown error';
|
|
228
|
+
this.logger.warn(`Failed to get local storage basePath: ${errorMessage}`);
|
|
245
229
|
return null;
|
|
246
230
|
}
|
|
247
231
|
}
|
|
@@ -251,16 +235,16 @@ export class UploadService {
|
|
|
251
235
|
* For Local/SFTP: returns direct path
|
|
252
236
|
*/ async makeFileUrl(key, storageConfigId, expiresIn = 3600, user) {
|
|
253
237
|
try {
|
|
254
|
-
const provider = await this.
|
|
238
|
+
const { provider } = await this.getStorageProviderWithConfig(storageConfigId, user);
|
|
255
239
|
// Check if provider supports presigned URLs
|
|
256
240
|
if (provider.generatePresignedUrl) {
|
|
257
241
|
return await provider.generatePresignedUrl(key, expiresIn);
|
|
258
242
|
}
|
|
259
243
|
// For SFTP or other providers without presigned URLs
|
|
260
244
|
return key;
|
|
261
|
-
} catch (
|
|
262
|
-
this.logger
|
|
263
|
-
|
|
245
|
+
} catch (error) {
|
|
246
|
+
ErrorHandler.logError(this.logger, error, 'makeFileUrl');
|
|
247
|
+
ErrorHandler.rethrowError(error);
|
|
264
248
|
}
|
|
265
249
|
}
|
|
266
250
|
// NOTE: @Inject() required for bundled code - type metadata may be lost during esbuild
|