@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.
Files changed (60) hide show
  1. package/README.md +131 -13
  2. package/cjs/config/storage-config.service.js +5 -0
  3. package/cjs/config/storage.constants.js +0 -8
  4. package/cjs/controllers/file-manager.controller.js +44 -1
  5. package/cjs/controllers/folder.controller.js +44 -1
  6. package/cjs/controllers/storage-config.controller.js +44 -1
  7. package/cjs/controllers/upload.controller.js +6 -12
  8. package/cjs/dtos/file-manager.dto.js +8 -5
  9. package/cjs/dtos/storage-config.dto.js +41 -1
  10. package/cjs/dtos/upload.dto.js +7 -0
  11. package/cjs/entities/storage-config-base.entity.js +31 -2
  12. package/cjs/interfaces/index.js +0 -1
  13. package/cjs/middlewares/file-serve.middleware.js +6 -0
  14. package/cjs/providers/local-provider.js +52 -2
  15. package/cjs/providers/storage-factory.service.js +2 -2
  16. package/cjs/services/file-manager.service.js +37 -24
  17. package/cjs/services/folder.service.js +5 -8
  18. package/cjs/services/storage-provider-config.service.js +18 -35
  19. package/cjs/services/upload.service.js +39 -27
  20. package/cjs/utils/file-validator.util.js +470 -0
  21. package/cjs/utils/image-compressor.util.js +1 -3
  22. package/config/storage-config.service.d.ts +5 -2
  23. package/config/storage.constants.d.ts +0 -2
  24. package/controllers/upload.controller.d.ts +2 -6
  25. package/dtos/file-manager.dto.d.ts +2 -4
  26. package/dtos/folder.dto.d.ts +2 -4
  27. package/dtos/storage-config.dto.d.ts +9 -6
  28. package/entities/storage-config-base.entity.d.ts +2 -0
  29. package/fesm/config/storage-config.service.js +5 -0
  30. package/fesm/config/storage.constants.js +0 -2
  31. package/fesm/controllers/file-manager.controller.js +45 -2
  32. package/fesm/controllers/folder.controller.js +45 -2
  33. package/fesm/controllers/storage-config.controller.js +45 -2
  34. package/fesm/controllers/upload.controller.js +7 -13
  35. package/fesm/dtos/file-manager.dto.js +8 -5
  36. package/fesm/dtos/storage-config.dto.js +45 -11
  37. package/fesm/dtos/upload.dto.js +8 -1
  38. package/fesm/entities/index.js +1 -5
  39. package/fesm/entities/storage-config-base.entity.js +33 -7
  40. package/fesm/interfaces/index.js +0 -1
  41. package/fesm/interfaces/storage-config.interface.js +1 -3
  42. package/fesm/middlewares/file-serve.middleware.js +7 -1
  43. package/fesm/providers/local-provider.js +52 -2
  44. package/fesm/providers/storage-factory.service.js +2 -2
  45. package/fesm/services/file-manager.service.js +38 -25
  46. package/fesm/services/folder.service.js +5 -8
  47. package/fesm/services/storage-provider-config.service.js +18 -35
  48. package/fesm/services/upload.service.js +40 -28
  49. package/fesm/utils/file-validator.util.js +463 -0
  50. package/fesm/utils/image-compressor.util.js +1 -3
  51. package/interfaces/file-manager.interface.d.ts +7 -4
  52. package/interfaces/index.d.ts +0 -1
  53. package/interfaces/storage-config.interface.d.ts +2 -20
  54. package/package.json +6 -6
  55. package/providers/local-provider.d.ts +2 -0
  56. package/services/file-manager.service.d.ts +2 -2
  57. package/utils/file-validator.util.d.ts +16 -0
  58. package/cjs/interfaces/file-upload-response.interface.js +0 -4
  59. package/fesm/interfaces/file-upload-response.interface.js +0 -1
  60. 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 { RequestScopedApiService, HybridCache } from '@flusys/nestjs-shared/classes';
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: dto.storageConfigId
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
- storageLocation = storageConfig.storage;
119
+ const typedConfig = storageConfig;
120
+ if (typedConfig.storage) {
121
+ storageLocation = typedConfig.storage;
122
+ }
118
123
  }
119
124
  } catch (error) {
120
- this.logger.warn(`Failed to get storage location from config: ${error}`);
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
- fileManager = {
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 they exist on the entity (when company feature is enabled)
132
- if ('companyId' in fileManager) {
133
- fileManager.companyId = user?.companyId ?? null;
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 fileManager;
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
- this.logger.warn(`Failed to fetch provider names: ${error}`);
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
- // If company feature enabled and user has companyId, filter by user's company
258
+ // Apply company filter using shared utility
251
259
  const enableCompanyFeature = this.storageConfig.isCompanyFeatureEnabled();
252
- if (enableCompanyFeature && user?.companyId) {
253
- query.andWhere('file_manager.companyId = :companyId', {
254
- companyId: user.companyId
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
- if (config && config.config?.basePath) {
320
- const basePath = config.config.basePath.replace(/^\.\//, '');
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
- this.logger.warn(`Failed to get basePath for delete: ${error}`);
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
- this.logger.error(`Failed to generate URL for file ${file.id}: ${error?.message || 'Unknown error'}`);
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
- if (config && config.config?.basePath) {
392
- const basePath = config.config.basePath.replace(/^\.\//, ''); // Remove leading ./
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
- this.logger.warn(`Failed to get basePath for file ${file.id}: ${error}`);
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
- this.logger.error(`Failed to save updated file URLs: ${error?.message || 'Unknown error'}`);
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
- if (this.storageConfig.isCompanyFeatureEnabled() && user?.companyId) {
73
- query.andWhere('folder.companyId = :companyId', {
74
- companyId: user.companyId
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
- if (this.storageConfig.isCompanyFeatureEnabled() && user?.companyId) {
74
- query.andWhere('storageConfig.companyId = :companyId', {
75
- companyId: user.companyId
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
- // Filter by company only if company feature is enabled and user is provided
100
- if (this.storageConfig.isCompanyFeatureEnabled() && user?.companyId) {
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
- name: 'default'
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
- // Filter by company only if company feature is enabled and user is provided
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 { BadRequestException, Inject, Injectable, InternalServerErrorException, Logger, NotFoundException, Scope } from '@nestjs/common';
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 if company feature enabled
61
- if (this.storageConfigService.isCompanyFeatureEnabled() && user?.companyId) {
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 (err) {
172
- this.logger.error('Single file upload failed', err);
173
- throw new InternalServerErrorException(err?.message || 'Single file upload failed');
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 (err) {
191
- this.logger.error('Multiple files upload failed', err);
192
- throw new InternalServerErrorException(err?.message || 'Multiple files upload failed');
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 Error('No file path provided');
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 (err) {
202
- this.logger.error('Delete single file failed', err);
203
- throw new InternalServerErrorException(err?.message || 'Delete single file failed');
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 Error('No file paths provided');
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 (err) {
213
- this.logger.error('Delete multiple files failed', err);
214
- throw new InternalServerErrorException(err?.message || 'Delete multiple files failed');
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
- this.logger.warn(`Failed to get local storage basePath: ${error.message}`);
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 (err) {
262
- this.logger.error('Generate file URL failed', err);
263
- throw new InternalServerErrorException(err?.message || 'Generate file URL failed');
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