@flusys/nestjs-storage 1.0.0-beta → 1.0.0-rc
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 +131 -13
- package/cjs/config/storage-config.service.js +5 -0
- package/cjs/config/storage.constants.js +0 -8
- 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 +6 -12
- package/cjs/dtos/file-manager.dto.js +8 -5
- package/cjs/dtos/storage-config.dto.js +41 -1
- package/cjs/dtos/upload.dto.js +7 -0
- package/cjs/entities/storage-config-base.entity.js +31 -2
- package/cjs/interfaces/index.js +0 -1
- package/cjs/middlewares/file-serve.middleware.js +6 -0
- package/cjs/providers/local-provider.js +52 -2
- package/cjs/providers/storage-factory.service.js +2 -2
- package/cjs/services/file-manager.service.js +37 -24
- package/cjs/services/folder.service.js +5 -8
- package/cjs/services/storage-provider-config.service.js +18 -35
- package/cjs/services/upload.service.js +39 -27
- package/cjs/utils/file-validator.util.js +470 -0
- package/cjs/utils/image-compressor.util.js +1 -3
- package/config/storage-config.service.d.ts +5 -2
- package/config/storage.constants.d.ts +0 -2
- package/controllers/upload.controller.d.ts +2 -6
- package/dtos/file-manager.dto.d.ts +2 -4
- package/dtos/folder.dto.d.ts +2 -4
- package/dtos/storage-config.dto.d.ts +9 -6
- package/entities/storage-config-base.entity.d.ts +2 -0
- package/fesm/config/storage-config.service.js +5 -0
- package/fesm/config/storage.constants.js +0 -2
- 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 +7 -13
- package/fesm/dtos/file-manager.dto.js +8 -5
- package/fesm/dtos/storage-config.dto.js +45 -11
- package/fesm/dtos/upload.dto.js +8 -1
- package/fesm/entities/index.js +1 -5
- package/fesm/entities/storage-config-base.entity.js +33 -7
- package/fesm/interfaces/index.js +0 -1
- package/fesm/interfaces/storage-config.interface.js +1 -3
- package/fesm/middlewares/file-serve.middleware.js +7 -1
- package/fesm/providers/local-provider.js +52 -2
- package/fesm/providers/storage-factory.service.js +2 -2
- package/fesm/services/file-manager.service.js +38 -25
- package/fesm/services/folder.service.js +5 -8
- package/fesm/services/storage-provider-config.service.js +18 -35
- package/fesm/services/upload.service.js +40 -28
- package/fesm/utils/file-validator.util.js +463 -0
- package/fesm/utils/image-compressor.util.js +1 -3
- package/interfaces/file-manager.interface.d.ts +7 -4
- package/interfaces/index.d.ts +0 -1
- package/interfaces/storage-config.interface.d.ts +2 -20
- package/package.json +6 -6
- package/providers/local-provider.d.ts +2 -0
- package/services/file-manager.service.d.ts +2 -2
- package/utils/file-validator.util.d.ts +16 -0
- package/cjs/interfaces/file-upload-response.interface.js +0 -4
- package/fesm/interfaces/file-upload-response.interface.js +0 -1
- package/interfaces/file-upload-response.interface.d.ts +0 -6
|
@@ -25,8 +25,9 @@ function _ts_param(paramIndex, decorator) {
|
|
|
25
25
|
decorator(target, key, paramIndex);
|
|
26
26
|
};
|
|
27
27
|
}
|
|
28
|
-
import {
|
|
28
|
+
import { HybridCache, RequestScopedApiService } from '@flusys/nestjs-shared/classes';
|
|
29
29
|
import { UtilsService } from '@flusys/nestjs-shared/modules';
|
|
30
|
+
import { applyCompanyFilter, ErrorHandler } from '@flusys/nestjs-shared/utils';
|
|
30
31
|
import { BadRequestException, Inject, Injectable, NotFoundException, Scope } from '@nestjs/common';
|
|
31
32
|
import { Brackets, In } from 'typeorm';
|
|
32
33
|
import { StorageConfigService } from '../config';
|
|
@@ -103,8 +104,9 @@ export class FileManagerService extends RequestScopedApiService {
|
|
|
103
104
|
const enableCompanyFeature = this.storageConfig.isCompanyFeatureEnabled();
|
|
104
105
|
const storageConfigEntity = enableCompanyFeature ? (await import('../entities')).StorageConfigWithCompany : (await import('../entities')).StorageConfig;
|
|
105
106
|
const storageConfigRepository = await this.dataSourceProvider.getRepository(storageConfigEntity);
|
|
107
|
+
const storageConfigId = dto.storageConfigId;
|
|
106
108
|
const whereCondition = {
|
|
107
|
-
id:
|
|
109
|
+
id: storageConfigId
|
|
108
110
|
};
|
|
109
111
|
// Filter by company if company feature is enabled
|
|
110
112
|
if (enableCompanyFeature && user?.companyId) {
|
|
@@ -114,25 +116,30 @@ export class FileManagerService extends RequestScopedApiService {
|
|
|
114
116
|
where: whereCondition
|
|
115
117
|
});
|
|
116
118
|
if (storageConfig) {
|
|
117
|
-
|
|
119
|
+
const typedConfig = storageConfig;
|
|
120
|
+
if (typedConfig.storage) {
|
|
121
|
+
storageLocation = typedConfig.storage;
|
|
122
|
+
}
|
|
118
123
|
}
|
|
119
124
|
} catch (error) {
|
|
120
|
-
|
|
125
|
+
const errorMessage = ErrorHandler.getErrorMessage(error);
|
|
126
|
+
this.logger.warn(`Failed to get storage location from config ${dto.storageConfigId}: ${errorMessage}`);
|
|
121
127
|
// Fall back to DTO location or default
|
|
122
128
|
}
|
|
123
129
|
}
|
|
124
|
-
// Set basic fields
|
|
125
|
-
|
|
130
|
+
// Set basic fields - merge existing data with DTO
|
|
131
|
+
const mergedFileManager = {
|
|
126
132
|
...fileManager,
|
|
127
133
|
...dto,
|
|
128
134
|
location: storageLocation,
|
|
129
135
|
folder: validatedFolder
|
|
130
136
|
};
|
|
131
|
-
// Only set company fields if
|
|
132
|
-
|
|
133
|
-
|
|
137
|
+
// Only set company fields if company feature is enabled
|
|
138
|
+
const enableCompanyFeatureForEntity = this.storageConfig.isCompanyFeatureEnabled();
|
|
139
|
+
if (enableCompanyFeatureForEntity) {
|
|
140
|
+
mergedFileManager.companyId = user?.companyId ?? null;
|
|
134
141
|
}
|
|
135
|
-
return
|
|
142
|
+
return mergedFileManager;
|
|
136
143
|
}
|
|
137
144
|
async getSelectQuery(query, _user, select) {
|
|
138
145
|
if (!select || !select.length) {
|
|
@@ -209,7 +216,8 @@ export class FileManagerService extends RequestScopedApiService {
|
|
|
209
216
|
this.logger.debug(`enrichWithProviderNames: First enriched item: ${JSON.stringify(enrichedItems[0])}`);
|
|
210
217
|
return enrichedItems;
|
|
211
218
|
} catch (error) {
|
|
212
|
-
|
|
219
|
+
const errorMessage = ErrorHandler.getErrorMessage(error);
|
|
220
|
+
this.logger.warn(`Failed to fetch provider names: ${errorMessage}`);
|
|
213
221
|
return items.map((item)=>({
|
|
214
222
|
...item,
|
|
215
223
|
providerName: undefined
|
|
@@ -247,13 +255,12 @@ export class FileManagerService extends RequestScopedApiService {
|
|
|
247
255
|
* Override: Extra query manipulation - Auto-filter by user's company and private file permissions
|
|
248
256
|
*/ async getExtraManipulateQuery(query, filterDto, user) {
|
|
249
257
|
const result = await super.getExtraManipulateQuery(query, filterDto, user);
|
|
250
|
-
//
|
|
258
|
+
// Apply company filter using shared utility
|
|
251
259
|
const enableCompanyFeature = this.storageConfig.isCompanyFeatureEnabled();
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
}
|
|
260
|
+
applyCompanyFilter(query, {
|
|
261
|
+
isCompanyFeatureEnabled: enableCompanyFeature,
|
|
262
|
+
entityAlias: 'file_manager'
|
|
263
|
+
}, user);
|
|
257
264
|
// Check if user has permission to see private files
|
|
258
265
|
if (user) {
|
|
259
266
|
const cacheKey = enableCompanyFeature ? `${USER_ACTION_PERMISSION_CACHE_KEY}_${user.id}_${user.companyId}` : `${USER_ACTION_PERMISSION_CACHE_KEY}_${user.id}`;
|
|
@@ -316,8 +323,9 @@ export class FileManagerService extends RequestScopedApiService {
|
|
|
316
323
|
'config'
|
|
317
324
|
]
|
|
318
325
|
});
|
|
319
|
-
|
|
320
|
-
|
|
326
|
+
const typedConfig = config;
|
|
327
|
+
if (typedConfig?.config?.basePath) {
|
|
328
|
+
const basePath = typedConfig.config.basePath.replace(/^\.\//, '');
|
|
321
329
|
// Convert old keys to new format
|
|
322
330
|
deleteKeys = keys.map((key)=>{
|
|
323
331
|
if (!key.includes('/')) {
|
|
@@ -327,7 +335,8 @@ export class FileManagerService extends RequestScopedApiService {
|
|
|
327
335
|
});
|
|
328
336
|
}
|
|
329
337
|
} catch (error) {
|
|
330
|
-
|
|
338
|
+
const errorMessage = ErrorHandler.getErrorMessage(error);
|
|
339
|
+
this.logger.warn(`Failed to get basePath for delete: ${errorMessage}`);
|
|
331
340
|
}
|
|
332
341
|
}
|
|
333
342
|
await this.uploadService.deleteMultipleFile(deleteKeys, configId === 'default' ? undefined : configId, user ?? undefined, location);
|
|
@@ -361,7 +370,8 @@ export class FileManagerService extends RequestScopedApiService {
|
|
|
361
370
|
file.expiresAt = now + expiresIn * 1000;
|
|
362
371
|
shouldUpdate = true;
|
|
363
372
|
} catch (error) {
|
|
364
|
-
|
|
373
|
+
const errorMessage = error instanceof Error ? error.message : 'Unknown error';
|
|
374
|
+
this.logger.error(`Failed to generate URL for file ${file.id}: ${errorMessage}`);
|
|
365
375
|
// Use fallback URL with appUrl from config
|
|
366
376
|
const baseUrl = this.getFileBaseUrl(protocol, host);
|
|
367
377
|
file.url = `${baseUrl}/storage/upload/file/${file.key}`;
|
|
@@ -388,13 +398,15 @@ export class FileManagerService extends RequestScopedApiService {
|
|
|
388
398
|
'config'
|
|
389
399
|
]
|
|
390
400
|
});
|
|
391
|
-
|
|
392
|
-
|
|
401
|
+
const typedConfig = config;
|
|
402
|
+
if (typedConfig?.config?.basePath) {
|
|
403
|
+
const basePath = typedConfig.config.basePath.replace(/^\.\//, ''); // Remove leading ./
|
|
393
404
|
fileKey = `${basePath}/${file.key}`;
|
|
394
405
|
this.logger.debug(`Prefixed old key with basePath: ${fileKey}`);
|
|
395
406
|
}
|
|
396
407
|
} catch (error) {
|
|
397
|
-
|
|
408
|
+
const errorMessage = ErrorHandler.getErrorMessage(error);
|
|
409
|
+
this.logger.warn(`Failed to get basePath for file ${file.id}: ${errorMessage}`);
|
|
398
410
|
}
|
|
399
411
|
}
|
|
400
412
|
const baseUrl = this.getFileBaseUrl(protocol, host);
|
|
@@ -419,7 +431,8 @@ export class FileManagerService extends RequestScopedApiService {
|
|
|
419
431
|
try {
|
|
420
432
|
await this.repository.save(updatedFiles);
|
|
421
433
|
} catch (error) {
|
|
422
|
-
|
|
434
|
+
const errorMessage = error instanceof Error ? error.message : 'Unknown error';
|
|
435
|
+
this.logger.error(`Failed to save updated file URLs: ${errorMessage}`);
|
|
423
436
|
}
|
|
424
437
|
}
|
|
425
438
|
return responses;
|
|
@@ -27,6 +27,7 @@ function _ts_param(paramIndex, decorator) {
|
|
|
27
27
|
}
|
|
28
28
|
import { RequestScopedApiService, HybridCache } from '@flusys/nestjs-shared/classes';
|
|
29
29
|
import { UtilsService } from '@flusys/nestjs-shared/modules';
|
|
30
|
+
import { applyCompanyFilter } from '@flusys/nestjs-shared/utils';
|
|
30
31
|
import { Inject, Injectable, Scope } from '@nestjs/common';
|
|
31
32
|
import { StorageConfigService } from '../config';
|
|
32
33
|
import { Folder, FolderWithCompany } from '../entities';
|
|
@@ -38,16 +39,13 @@ export class FolderService extends RequestScopedApiService {
|
|
|
38
39
|
getDataSourceProvider() {
|
|
39
40
|
return this.dataSourceProvider;
|
|
40
41
|
}
|
|
41
|
-
// Entity Conversion
|
|
42
42
|
async convertSingleDtoToEntity(dto, user) {
|
|
43
43
|
const entity = await super.convertSingleDtoToEntity(dto, user);
|
|
44
|
-
// Set companyId from user context if company feature enabled
|
|
45
44
|
if (this.storageConfig.isCompanyFeatureEnabled()) {
|
|
46
45
|
entity.companyId = user?.companyId ?? null;
|
|
47
46
|
}
|
|
48
47
|
return entity;
|
|
49
48
|
}
|
|
50
|
-
// Query Customization
|
|
51
49
|
async getSelectQuery(query, _user, select) {
|
|
52
50
|
if (!select?.length) {
|
|
53
51
|
select = [
|
|
@@ -69,11 +67,10 @@ export class FolderService extends RequestScopedApiService {
|
|
|
69
67
|
}
|
|
70
68
|
async getExtraManipulateQuery(query, filterDto, user) {
|
|
71
69
|
const result = await super.getExtraManipulateQuery(query, filterDto, user);
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
}
|
|
70
|
+
applyCompanyFilter(query, {
|
|
71
|
+
isCompanyFeatureEnabled: this.storageConfig.isCompanyFeatureEnabled(),
|
|
72
|
+
entityAlias: 'folder'
|
|
73
|
+
}, user);
|
|
77
74
|
return result;
|
|
78
75
|
}
|
|
79
76
|
constructor(cacheManager, utilsService, storageConfig, dataSourceProvider){
|
|
@@ -27,6 +27,7 @@ function _ts_param(paramIndex, decorator) {
|
|
|
27
27
|
}
|
|
28
28
|
import { RequestScopedApiService, HybridCache } from '@flusys/nestjs-shared/classes';
|
|
29
29
|
import { UtilsService } from '@flusys/nestjs-shared/modules';
|
|
30
|
+
import { applyCompanyFilter, buildCompanyWhereCondition } from '@flusys/nestjs-shared/utils';
|
|
30
31
|
import { Inject, Injectable, Scope } from '@nestjs/common';
|
|
31
32
|
import { StorageConfigService } from '../config';
|
|
32
33
|
import { StorageConfig, StorageConfigWithCompany } from '../entities';
|
|
@@ -38,16 +39,13 @@ export class StorageProviderConfigService extends RequestScopedApiService {
|
|
|
38
39
|
getDataSourceProvider() {
|
|
39
40
|
return this.dataSourceProvider;
|
|
40
41
|
}
|
|
41
|
-
// Entity Conversion
|
|
42
42
|
async convertSingleDtoToEntity(dto, user) {
|
|
43
43
|
const entity = await super.convertSingleDtoToEntity(dto, user);
|
|
44
|
-
// Set companyId from user context if company feature enabled
|
|
45
44
|
if (this.storageConfig.isCompanyFeatureEnabled()) {
|
|
46
45
|
entity.companyId = user?.companyId ?? null;
|
|
47
46
|
}
|
|
48
47
|
return entity;
|
|
49
48
|
}
|
|
50
|
-
// Query Customization
|
|
51
49
|
async getSelectQuery(query, _user, select) {
|
|
52
50
|
if (!select?.length) {
|
|
53
51
|
select = [
|
|
@@ -55,6 +53,8 @@ export class StorageProviderConfigService extends RequestScopedApiService {
|
|
|
55
53
|
'name',
|
|
56
54
|
'storage',
|
|
57
55
|
'config',
|
|
56
|
+
'isActive',
|
|
57
|
+
'isDefault',
|
|
58
58
|
'createdAt',
|
|
59
59
|
'updatedAt'
|
|
60
60
|
];
|
|
@@ -70,19 +70,14 @@ export class StorageProviderConfigService extends RequestScopedApiService {
|
|
|
70
70
|
}
|
|
71
71
|
async getExtraManipulateQuery(query, filterDto, user) {
|
|
72
72
|
const result = await super.getExtraManipulateQuery(query, filterDto, user);
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
}
|
|
73
|
+
applyCompanyFilter(query, {
|
|
74
|
+
isCompanyFeatureEnabled: this.storageConfig.isCompanyFeatureEnabled(),
|
|
75
|
+
entityAlias: 'storageConfig'
|
|
76
|
+
}, user);
|
|
78
77
|
query.orderBy(`${this.entityName}.createdAt`, 'DESC');
|
|
79
78
|
return result;
|
|
80
79
|
}
|
|
81
|
-
|
|
82
|
-
* Find storage config by ID without throwing (returns null if not found)
|
|
83
|
-
* Uses direct repository query - bypasses company filtering
|
|
84
|
-
* Use for internal operations like file deletion where config ID is already known
|
|
85
|
-
*/ async findByIdDirect(id) {
|
|
80
|
+
async findByIdDirect(id) {
|
|
86
81
|
await this.ensureRepositoryInitialized();
|
|
87
82
|
return await this.repository.findOne({
|
|
88
83
|
where: {
|
|
@@ -90,21 +85,15 @@ export class StorageProviderConfigService extends RequestScopedApiService {
|
|
|
90
85
|
}
|
|
91
86
|
});
|
|
92
87
|
}
|
|
93
|
-
|
|
94
|
-
* Get default storage configuration (scoped to user's company if enabled)
|
|
95
|
-
* Falls back to any available config if 'default' not found
|
|
96
|
-
*/ async getDefaultConfig(user) {
|
|
88
|
+
async getDefaultConfig(user) {
|
|
97
89
|
await this.ensureRepositoryInitialized();
|
|
98
|
-
const baseWhere = {
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
baseWhere.companyId = user.companyId;
|
|
102
|
-
}
|
|
103
|
-
// First try to find config named 'default'
|
|
90
|
+
const baseWhere = buildCompanyWhereCondition({
|
|
91
|
+
isActive: true
|
|
92
|
+
}, this.storageConfig.isCompanyFeatureEnabled(), user);
|
|
104
93
|
const defaultConfig = await this.repository.findOne({
|
|
105
94
|
where: {
|
|
106
95
|
...baseWhere,
|
|
107
|
-
|
|
96
|
+
isDefault: true
|
|
108
97
|
},
|
|
109
98
|
order: {
|
|
110
99
|
createdAt: 'ASC'
|
|
@@ -113,7 +102,6 @@ export class StorageProviderConfigService extends RequestScopedApiService {
|
|
|
113
102
|
if (defaultConfig) {
|
|
114
103
|
return defaultConfig;
|
|
115
104
|
}
|
|
116
|
-
// Fall back to any available config for this company/user
|
|
117
105
|
return await this.repository.findOne({
|
|
118
106
|
where: baseWhere,
|
|
119
107
|
order: {
|
|
@@ -121,17 +109,12 @@ export class StorageProviderConfigService extends RequestScopedApiService {
|
|
|
121
109
|
}
|
|
122
110
|
});
|
|
123
111
|
}
|
|
124
|
-
|
|
125
|
-
* Get storage configuration by type (scoped to user's company if enabled)
|
|
126
|
-
*/ async getConfigByType(storage, user) {
|
|
112
|
+
async getConfigByType(storage, user) {
|
|
127
113
|
await this.ensureRepositoryInitialized();
|
|
128
|
-
const where = {
|
|
129
|
-
storage
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
if (this.storageConfig.isCompanyFeatureEnabled() && user?.companyId) {
|
|
133
|
-
where.companyId = user.companyId;
|
|
134
|
-
}
|
|
114
|
+
const where = buildCompanyWhereCondition({
|
|
115
|
+
storage,
|
|
116
|
+
isActive: true
|
|
117
|
+
}, this.storageConfig.isCompanyFeatureEnabled(), user);
|
|
135
118
|
return await this.repository.find({
|
|
136
119
|
where
|
|
137
120
|
});
|
|
@@ -25,25 +25,41 @@ function _ts_param(paramIndex, decorator) {
|
|
|
25
25
|
decorator(target, key, paramIndex);
|
|
26
26
|
};
|
|
27
27
|
}
|
|
28
|
-
import {
|
|
28
|
+
import { ErrorHandler, validateCompanyOwnership } from '@flusys/nestjs-shared/utils';
|
|
29
|
+
import { BadRequestException, Inject, Injectable, Logger, NotFoundException, Scope } from '@nestjs/common';
|
|
29
30
|
import { StorageConfigService } from '../config';
|
|
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);
|
|
47
63
|
}
|
|
48
64
|
/**
|
|
49
65
|
* Get storage provider and config info based on storage config ID
|
|
@@ -57,13 +73,8 @@ export class UploadService {
|
|
|
57
73
|
if (!config) {
|
|
58
74
|
throw new NotFoundException('Storage configuration not found');
|
|
59
75
|
}
|
|
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
|
-
}
|
|
76
|
+
// Validate company ownership using shared utility
|
|
77
|
+
validateCompanyOwnership(config, user, this.storageConfigService.isCompanyFeatureEnabled(), 'Storage configuration');
|
|
67
78
|
storageConfig = config;
|
|
68
79
|
} else {
|
|
69
80
|
// Use default config (scoped to user's company/branch)
|
|
@@ -168,9 +179,9 @@ export class UploadService {
|
|
|
168
179
|
location,
|
|
169
180
|
storageConfigId: configId
|
|
170
181
|
};
|
|
171
|
-
} catch (
|
|
172
|
-
this.logger
|
|
173
|
-
|
|
182
|
+
} catch (error) {
|
|
183
|
+
ErrorHandler.logError(this.logger, error, 'uploadSingleFile');
|
|
184
|
+
ErrorHandler.rethrowError(error);
|
|
174
185
|
}
|
|
175
186
|
}
|
|
176
187
|
async uploadMultipleFiles(files, options, user) {
|
|
@@ -187,31 +198,31 @@ export class UploadService {
|
|
|
187
198
|
location,
|
|
188
199
|
storageConfigId: configId
|
|
189
200
|
}));
|
|
190
|
-
} catch (
|
|
191
|
-
this.logger
|
|
192
|
-
|
|
201
|
+
} catch (error) {
|
|
202
|
+
ErrorHandler.logError(this.logger, error, 'uploadMultipleFiles');
|
|
203
|
+
ErrorHandler.rethrowError(error);
|
|
193
204
|
}
|
|
194
205
|
}
|
|
195
206
|
async deleteSingleFile(key, storageConfigId, user, locationHint) {
|
|
196
207
|
try {
|
|
197
|
-
if (!key) throw new
|
|
208
|
+
if (!key) throw new BadRequestException('No file path provided');
|
|
198
209
|
const provider = await this.getStorageProviderForDelete(storageConfigId, user, locationHint);
|
|
199
210
|
await provider.deleteFile(key);
|
|
200
211
|
return true;
|
|
201
|
-
} catch (
|
|
202
|
-
this.logger
|
|
203
|
-
|
|
212
|
+
} catch (error) {
|
|
213
|
+
ErrorHandler.logError(this.logger, error, 'deleteSingleFile');
|
|
214
|
+
ErrorHandler.rethrowError(error);
|
|
204
215
|
}
|
|
205
216
|
}
|
|
206
217
|
async deleteMultipleFile(keys, storageConfigId, user, locationHint) {
|
|
207
218
|
try {
|
|
208
|
-
if (!keys || !keys.length) throw new
|
|
219
|
+
if (!keys || !keys.length) throw new BadRequestException('No file paths provided');
|
|
209
220
|
const provider = await this.getStorageProviderForDelete(storageConfigId, user, locationHint);
|
|
210
221
|
await provider.deleteMultipleFiles(keys);
|
|
211
222
|
return true;
|
|
212
|
-
} catch (
|
|
213
|
-
this.logger
|
|
214
|
-
|
|
223
|
+
} catch (error) {
|
|
224
|
+
ErrorHandler.logError(this.logger, error, 'deleteMultipleFiles');
|
|
225
|
+
ErrorHandler.rethrowError(error);
|
|
215
226
|
}
|
|
216
227
|
}
|
|
217
228
|
bytesToKb(bytes) {
|
|
@@ -241,7 +252,8 @@ export class UploadService {
|
|
|
241
252
|
}
|
|
242
253
|
return null;
|
|
243
254
|
} catch (error) {
|
|
244
|
-
|
|
255
|
+
const errorMessage = error instanceof Error ? error.message : 'Unknown error';
|
|
256
|
+
this.logger.warn(`Failed to get local storage basePath: ${errorMessage}`);
|
|
245
257
|
return null;
|
|
246
258
|
}
|
|
247
259
|
}
|
|
@@ -258,9 +270,9 @@ export class UploadService {
|
|
|
258
270
|
}
|
|
259
271
|
// For SFTP or other providers without presigned URLs
|
|
260
272
|
return key;
|
|
261
|
-
} catch (
|
|
262
|
-
this.logger
|
|
263
|
-
|
|
273
|
+
} catch (error) {
|
|
274
|
+
ErrorHandler.logError(this.logger, error, 'makeFileUrl');
|
|
275
|
+
ErrorHandler.rethrowError(error);
|
|
264
276
|
}
|
|
265
277
|
}
|
|
266
278
|
// NOTE: @Inject() required for bundled code - type metadata may be lost during esbuild
|