@flusys/nestjs-storage 1.1.0-beta → 1.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (118) hide show
  1. package/README.md +153 -8
  2. package/cjs/config/index.js +0 -1
  3. package/cjs/config/storage.constants.js +0 -17
  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 +18 -29
  8. package/cjs/docs/storage-swagger.config.js +24 -136
  9. package/cjs/dtos/file-manager.dto.js +71 -35
  10. package/cjs/dtos/folder.dto.js +15 -9
  11. package/cjs/dtos/storage-config.dto.js +5 -86
  12. package/cjs/dtos/upload.dto.js +24 -17
  13. package/cjs/entities/file-manager-with-company.entity.js +3 -4
  14. package/cjs/entities/file-manager.entity.js +71 -3
  15. package/cjs/entities/folder-with-company.entity.js +3 -4
  16. package/cjs/entities/folder.entity.js +19 -3
  17. package/cjs/entities/index.js +9 -10
  18. package/cjs/entities/storage-config-with-company.entity.js +3 -4
  19. package/cjs/entities/storage-config.entity.js +73 -3
  20. package/cjs/interfaces/index.js +0 -1
  21. package/cjs/middlewares/file-serve.middleware.js +113 -100
  22. package/cjs/modules/storage.module.js +82 -136
  23. package/cjs/providers/azure-provider.optional.js +10 -38
  24. package/cjs/providers/local-provider.js +38 -31
  25. package/cjs/providers/s3-provider.optional.js +19 -40
  26. package/cjs/providers/storage-factory.service.js +54 -99
  27. package/cjs/providers/storage-provider.registry.js +8 -18
  28. package/cjs/services/file-manager.service.js +238 -323
  29. package/cjs/services/folder.service.js +8 -11
  30. package/cjs/services/index.js +1 -0
  31. package/cjs/{config → services}/storage-config.service.js +32 -76
  32. package/cjs/services/storage-datasource.provider.js +16 -26
  33. package/cjs/services/storage-provider-config.service.js +15 -37
  34. package/cjs/services/upload.service.js +72 -88
  35. package/cjs/utils/file-validator.util.js +458 -0
  36. package/cjs/utils/image-compressor.util.js +3 -8
  37. package/config/index.d.ts +0 -1
  38. package/config/storage.constants.d.ts +0 -8
  39. package/controllers/upload.controller.d.ts +3 -6
  40. package/dtos/file-manager.dto.d.ts +13 -7
  41. package/dtos/folder.dto.d.ts +5 -5
  42. package/dtos/storage-config.dto.d.ts +9 -16
  43. package/entities/file-manager-with-company.entity.d.ts +2 -2
  44. package/entities/file-manager.entity.d.ts +11 -2
  45. package/entities/folder-with-company.entity.d.ts +2 -2
  46. package/entities/folder.entity.d.ts +4 -2
  47. package/entities/index.d.ts +3 -4
  48. package/entities/storage-config-with-company.entity.d.ts +2 -2
  49. package/entities/storage-config.entity.d.ts +7 -2
  50. package/fesm/config/index.js +0 -1
  51. package/fesm/config/storage.constants.js +0 -8
  52. package/fesm/controllers/file-manager.controller.js +45 -2
  53. package/fesm/controllers/folder.controller.js +45 -2
  54. package/fesm/controllers/storage-config.controller.js +45 -2
  55. package/fesm/controllers/upload.controller.js +19 -30
  56. package/fesm/docs/storage-swagger.config.js +27 -142
  57. package/fesm/dtos/file-manager.dto.js +72 -36
  58. package/fesm/dtos/folder.dto.js +16 -10
  59. package/fesm/dtos/storage-config.dto.js +9 -96
  60. package/fesm/dtos/upload.dto.js +25 -19
  61. package/fesm/entities/file-manager-with-company.entity.js +3 -4
  62. package/fesm/entities/file-manager.entity.js +72 -4
  63. package/fesm/entities/folder-with-company.entity.js +3 -4
  64. package/fesm/entities/folder.entity.js +20 -4
  65. package/fesm/entities/index.js +5 -13
  66. package/fesm/entities/storage-config-with-company.entity.js +3 -4
  67. package/fesm/entities/storage-config.entity.js +74 -4
  68. package/fesm/interfaces/index.js +0 -1
  69. package/fesm/interfaces/storage-config.interface.js +1 -3
  70. package/fesm/middlewares/file-serve.middleware.js +114 -101
  71. package/fesm/modules/storage.module.js +83 -136
  72. package/fesm/providers/azure-provider.optional.js +14 -45
  73. package/fesm/providers/local-provider.js +38 -31
  74. package/fesm/providers/s3-provider.optional.js +23 -47
  75. package/fesm/providers/storage-factory.service.js +52 -97
  76. package/fesm/providers/storage-provider.registry.js +10 -20
  77. package/fesm/services/file-manager.service.js +237 -322
  78. package/fesm/services/folder.service.js +6 -9
  79. package/fesm/services/index.js +1 -0
  80. package/fesm/{config → services}/storage-config.service.js +32 -76
  81. package/fesm/services/storage-datasource.provider.js +16 -26
  82. package/fesm/services/storage-provider-config.service.js +13 -35
  83. package/fesm/services/upload.service.js +71 -87
  84. package/fesm/utils/file-validator.util.js +451 -0
  85. package/fesm/utils/image-compressor.util.js +3 -8
  86. package/interfaces/file-manager.interface.d.ts +7 -4
  87. package/interfaces/index.d.ts +0 -1
  88. package/interfaces/storage-config.interface.d.ts +1 -22
  89. package/interfaces/storage-module-options.interface.d.ts +0 -5
  90. package/middlewares/file-serve.middleware.d.ts +9 -1
  91. package/modules/storage.module.d.ts +1 -2
  92. package/package.json +6 -6
  93. package/providers/azure-provider.optional.d.ts +8 -6
  94. package/providers/local-provider.d.ts +2 -7
  95. package/providers/s3-provider.optional.d.ts +9 -7
  96. package/providers/storage-factory.service.d.ts +8 -9
  97. package/providers/storage-provider.registry.d.ts +4 -4
  98. package/services/file-manager.service.d.ts +23 -16
  99. package/services/folder.service.d.ts +4 -4
  100. package/services/index.d.ts +1 -0
  101. package/services/storage-config.service.d.ts +24 -0
  102. package/services/storage-datasource.provider.d.ts +3 -4
  103. package/services/storage-provider-config.service.d.ts +5 -6
  104. package/services/upload.service.d.ts +5 -5
  105. package/utils/file-validator.util.d.ts +19 -0
  106. package/cjs/entities/file-manager-base.entity.js +0 -115
  107. package/cjs/entities/folder-base.entity.js +0 -55
  108. package/cjs/entities/storage-config-base.entity.js +0 -93
  109. package/cjs/interfaces/file-upload-response.interface.js +0 -4
  110. package/config/storage-config.service.d.ts +0 -22
  111. package/entities/file-manager-base.entity.d.ts +0 -13
  112. package/entities/folder-base.entity.d.ts +0 -5
  113. package/entities/storage-config-base.entity.d.ts +0 -9
  114. package/fesm/entities/file-manager-base.entity.js +0 -108
  115. package/fesm/entities/folder-base.entity.js +0 -48
  116. package/fesm/entities/storage-config-base.entity.js +0 -83
  117. package/fesm/interfaces/file-upload-response.interface.js +0 -1
  118. package/interfaces/file-upload-response.interface.d.ts +0 -6
@@ -25,168 +25,134 @@ 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
- import { StorageConfigService } from '../config';
33
+ import { StorageConfigService } from './storage-config.service';
33
34
  import { FileManager, FileManagerWithCompany, Folder, FolderWithCompany } from '../entities';
34
35
  import { FileLocationEnum } from '../enums/file-location.enum';
35
36
  import { StorageDataSourceProvider } from './storage-datasource.provider';
36
37
  import { UploadService } from './upload.service';
37
- // Storage-specific constants
38
- const USER_ACTION_PERMISSION_CACHE_KEY = 'user_action_permission';
39
- const SHOW_PRIVATE_FILE_ACTION = 'storage.file.viewPrivate';
38
+ const PERMISSION_CACHE_KEY = 'user_action_permission';
39
+ const VIEW_PRIVATE_ACTION = 'storage.file.viewPrivate';
40
+ const URL_EXPIRY_SECONDS = 3600;
41
+ const DEFAULT_SELECT_FIELDS = [
42
+ 'id',
43
+ 'name',
44
+ 'contentType',
45
+ 'size',
46
+ 'key',
47
+ 'url',
48
+ 'location',
49
+ 'storageConfigId',
50
+ 'isPrivate',
51
+ 'createdAt',
52
+ 'deletedAt'
53
+ ];
40
54
  export class FileManagerService extends RequestScopedApiService {
41
- /**
42
- * Get base URL for file serving
43
- * Priority: appUrl config > request headers
44
- */ getFileBaseUrl(protocol, host) {
45
- const appUrl = this.storageConfig.getAppUrl();
46
- return appUrl?.replace(/\/$/, '') ?? `${protocol}://${host}`;
55
+ resolveEntity() {
56
+ return this.storageConfig.isCompanyFeatureEnabled() ? FileManagerWithCompany : FileManager;
47
57
  }
48
- /**
49
- * Resolve entity class for this service
50
- * @returns FileManager or FileManagerWithCompany based on configuration
51
- */ resolveEntity() {
52
- const enableCompanyFeature = this.storageConfig.isCompanyFeatureEnabled();
53
- return enableCompanyFeature ? FileManagerWithCompany : FileManager;
54
- }
55
- /**
56
- * Get DataSource provider for this service
57
- * @returns StorageDataSourceProvider instance
58
- */ getDataSourceProvider() {
58
+ getDataSourceProvider() {
59
59
  return this.dataSourceProvider;
60
60
  }
61
+ // ─── Override Methods ───────────────────────────────────────────────────────
61
62
  async convertSingleDtoToEntity(dto, user) {
62
- let fileManager = {};
63
- // NOTE: Using 'id' in dto check instead of instanceof - instanceof may not work after esbuild bundling
63
+ let entity = {};
64
64
  if ('id' in dto && dto.id && typeof dto.id === 'string') {
65
- const dbData = await this.repository.findOne({
65
+ const existing = await this.repository.findOne({
66
66
  where: {
67
67
  id: dto.id
68
68
  }
69
69
  });
70
- if (!dbData) {
71
- throw new NotFoundException('No such entity data found for update! Please, Try Again.');
72
- }
73
- fileManager = dbData;
74
- }
75
- // Validate folder exists if folderId is provided
76
- let validatedFolder = null;
77
- if (dto.folderId) {
78
- const enableCompanyFeature = this.storageConfig.isCompanyFeatureEnabled();
79
- const folderEntity = enableCompanyFeature ? FolderWithCompany : Folder;
80
- const folderRepository = await this.dataSourceProvider.getRepository(folderEntity);
81
- const whereCondition = {
82
- id: dto.folderId
83
- };
84
- // Filter by company if company feature is enabled
85
- if (enableCompanyFeature && user?.companyId) {
86
- whereCondition.companyId = user.companyId;
87
- }
88
- const folder = await folderRepository.findOne({
89
- where: whereCondition
90
- });
91
- if (!folder) {
92
- throw new BadRequestException(`Folder with ID ${dto.folderId} does not exist or you don't have access to it.`);
93
- }
94
- validatedFolder = {
95
- id: dto.folderId
96
- };
97
- }
98
- // Get storage location from storage config
99
- let storageLocation = dto.location || FileLocationEnum.LOCAL; // Default to LOCAL if not provided
100
- // If storageConfigId is provided, get the location from storage config
101
- if (dto.storageConfigId) {
102
- try {
103
- const enableCompanyFeature = this.storageConfig.isCompanyFeatureEnabled();
104
- const storageConfigEntity = enableCompanyFeature ? (await import('../entities')).StorageConfigWithCompany : (await import('../entities')).StorageConfig;
105
- const storageConfigRepository = await this.dataSourceProvider.getRepository(storageConfigEntity);
106
- const whereCondition = {
107
- id: dto.storageConfigId
108
- };
109
- // Filter by company if company feature is enabled
110
- if (enableCompanyFeature && user?.companyId) {
111
- whereCondition.companyId = user.companyId;
112
- }
113
- const storageConfig = await storageConfigRepository.findOne({
114
- where: whereCondition
115
- });
116
- if (storageConfig) {
117
- storageLocation = storageConfig.storage;
118
- }
119
- } catch (error) {
120
- this.logger.warn(`Failed to get storage location from config: ${error}`);
121
- // Fall back to DTO location or default
122
- }
70
+ if (!existing) throw new NotFoundException('Entity not found for update');
71
+ entity = existing;
123
72
  }
124
- // Set basic fields
125
- fileManager = {
126
- ...fileManager,
73
+ const validatedFolder = dto.folderId ? await this.validateFolder(dto.folderId, user) : null;
74
+ const storageLocation = await this.resolveStorageLocation(dto, user);
75
+ const merged = {
76
+ ...entity,
127
77
  ...dto,
128
78
  location: storageLocation,
129
79
  folder: validatedFolder
130
80
  };
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;
81
+ if (this.storageConfig.isCompanyFeatureEnabled()) {
82
+ merged.companyId = user?.companyId ?? null;
134
83
  }
135
- return fileManager;
84
+ return merged;
136
85
  }
137
86
  async getSelectQuery(query, _user, select) {
138
- if (!select || !select.length) {
139
- select = [
140
- 'id',
141
- 'name',
142
- 'contentType',
143
- 'size',
144
- 'key',
145
- 'url',
146
- 'location',
147
- 'storageConfigId',
148
- 'isPrivate',
149
- 'createdAt',
150
- 'deletedAt'
151
- ];
152
- }
153
- const selectFields = select.map((field)=>`${this.entityName}.${field}`);
154
- // Add company context fields if company feature is enabled
87
+ const fields = (select?.length ? select : DEFAULT_SELECT_FIELDS).map((f)=>`${this.entityName}.${f}`);
155
88
  if (this.storageConfig.isCompanyFeatureEnabled()) {
156
- selectFields.push('file_manager.companyId');
89
+ fields.push('file_manager.companyId');
157
90
  }
158
- // Join folder
159
- selectFields.push('folder.id');
160
- selectFields.push('folder.name');
161
- query.leftJoinAndSelect('file_manager.folder', 'folder');
162
- query.select(selectFields);
91
+ fields.push('folder.id', 'folder.name');
92
+ query.leftJoinAndSelect('file_manager.folder', 'folder').select(fields);
163
93
  return {
164
94
  query,
165
95
  isRaw: false
166
96
  };
167
97
  }
168
- /**
169
- * Enrich file list with provider names from storage_config table
170
- * Call this method to add providerName to the list results
171
- */ async enrichWithProviderNames(items) {
172
- // Get unique storage config IDs
98
+ async getFilterQuery(query, filter, _user) {
99
+ for (const [key, value] of Object.entries(filter)){
100
+ if (key === 'contentType') {
101
+ this.applyContentTypeFilter(query, value);
102
+ } else {
103
+ query.andWhere(`${this.entityName}.${key} = :value`, {
104
+ value
105
+ });
106
+ }
107
+ }
108
+ return {
109
+ query,
110
+ isRaw: false
111
+ };
112
+ }
113
+ async getExtraManipulateQuery(query, filterDto, user) {
114
+ const result = await super.getExtraManipulateQuery(query, filterDto, user);
115
+ applyCompanyFilter(query, {
116
+ isCompanyFeatureEnabled: this.storageConfig.isCompanyFeatureEnabled(),
117
+ entityAlias: 'file_manager'
118
+ }, user);
119
+ await this.applyPrivateFileFilter(query, user);
120
+ return result;
121
+ }
122
+ async beforeDeleteOperation(dto, user, _qr) {
123
+ if (dto.type !== 'permanent') return;
124
+ const ids = Array.isArray(dto.id) ? dto.id : [
125
+ dto.id
126
+ ];
127
+ const files = await this.repository.findBy({
128
+ id: In(ids)
129
+ });
130
+ const grouped = this.groupFilesByConfig(files);
131
+ for (const [configId, { keys, location }] of grouped){
132
+ let deleteKeys = keys;
133
+ if (location === FileLocationEnum.LOCAL && configId !== 'default') {
134
+ const basePath = await this.getStorageConfigBasePath(configId);
135
+ if (basePath) {
136
+ deleteKeys = keys.map((k)=>k.includes('/') ? k : `${basePath}/${k}`);
137
+ }
138
+ }
139
+ await this.uploadService.deleteMultipleFile(deleteKeys, configId === 'default' ? undefined : configId, user ?? undefined, location);
140
+ }
141
+ }
142
+ // ─── Public Methods ─────────────────────────────────────────────────────────
143
+ async enrichWithProviderNames(items) {
173
144
  const configIds = [
174
- ...new Set(items.map((item)=>item.storageConfigId).filter(Boolean))
145
+ ...new Set(items.map((i)=>i.storageConfigId).filter(Boolean))
175
146
  ];
176
- this.logger.debug(`enrichWithProviderNames: items count=${items.length}, configIds=${JSON.stringify(configIds)}`);
177
- if (configIds.length === 0) {
178
- this.logger.debug('enrichWithProviderNames: No storage config IDs found in items');
147
+ if (!configIds.length) {
179
148
  return items.map((item)=>({
180
149
  ...item,
181
150
  providerName: undefined
182
151
  }));
183
152
  }
184
- // Fetch storage config names
185
153
  try {
186
- const enableCompanyFeature = this.storageConfig.isCompanyFeatureEnabled();
187
- const storageConfigEntity = enableCompanyFeature ? (await import('../entities')).StorageConfigWithCompany : (await import('../entities')).StorageConfig;
188
- const storageConfigRepository = await this.dataSourceProvider.getRepository(storageConfigEntity);
189
- const configs = await storageConfigRepository.find({
154
+ const repo = await this.getStorageConfigRepository();
155
+ const configs = await repo.find({
190
156
  where: {
191
157
  id: In(configIds)
192
158
  },
@@ -195,239 +161,188 @@ export class FileManagerService extends RequestScopedApiService {
195
161
  'name'
196
162
  ]
197
163
  });
198
- this.logger.debug(`enrichWithProviderNames: Found ${configs.length} configs: ${JSON.stringify(configs)}`);
199
- // Create a map for quick lookup
200
- const configNameMap = new Map(configs.map((c)=>[
164
+ const nameMap = new Map(configs.map((c)=>[
201
165
  c.id,
202
166
  c.name
203
167
  ]));
204
- // Enrich items with provider names
205
- const enrichedItems = items.map((item)=>({
168
+ return items.map((item)=>({
206
169
  ...item,
207
- providerName: item.storageConfigId ? configNameMap.get(item.storageConfigId) : undefined
170
+ providerName: item.storageConfigId ? nameMap.get(item.storageConfigId) : undefined
208
171
  }));
209
- this.logger.debug(`enrichWithProviderNames: First enriched item: ${JSON.stringify(enrichedItems[0])}`);
210
- return enrichedItems;
211
172
  } catch (error) {
212
- this.logger.warn(`Failed to fetch provider names: ${error}`);
173
+ this.logger.warn(`Failed to fetch provider names: ${ErrorHandler.getErrorMessage(error)}`);
213
174
  return items.map((item)=>({
214
175
  ...item,
215
176
  providerName: undefined
216
177
  }));
217
178
  }
218
179
  }
219
- async getFilterQuery(query, filter, _user) {
220
- Object.entries(filter).forEach(([key, value])=>{
221
- if (key === 'contentType') {
222
- const types = value.split(',').map((t)=>t.trim());
223
- const patterns = types.map((t)=>t.replace('*', '%'));
224
- query.where(new Brackets((qb1)=>{
225
- patterns.forEach((p, i)=>{
226
- const param = `p${i}`;
227
- if (i === 0) qb1.where(`${this.entityName}.contentType LIKE :${param}`, {
228
- [param]: p
229
- });
230
- else qb1.orWhere(`${this.entityName}.contentType LIKE :${param}`, {
231
- [param]: p
232
- });
233
- });
234
- }));
235
- return;
180
+ async getFiles(dtos, protocol, host, user) {
181
+ await this.ensureRepositoryInitialized();
182
+ const ids = dtos.map((d)=>d.id).filter(Boolean);
183
+ if (!ids.length) throw new BadRequestException('No valid file IDs provided');
184
+ const files = await this.repository.findBy({
185
+ id: In(ids)
186
+ });
187
+ const now = Date.now();
188
+ const updatedFiles = [];
189
+ const responses = await Promise.all(files.map(async (file)=>{
190
+ const updated = await this.refreshFileUrl(file, protocol, host, now, user);
191
+ if (updated) updatedFiles.push(file);
192
+ return this.toFileResponse(file);
193
+ }));
194
+ if (updatedFiles.length) {
195
+ try {
196
+ await this.repository.save(updatedFiles);
197
+ } catch (error) {
198
+ this.logger.error(`Failed to save updated URLs: ${ErrorHandler.getErrorMessage(error)}`);
236
199
  }
237
- query.andWhere(`${this.entityName}.${key} = :value`, {
238
- value
200
+ }
201
+ return responses;
202
+ }
203
+ // ─── Private Helpers ────────────────────────────────────────────────────────
204
+ async getStorageConfigRepository() {
205
+ const entity = this.storageConfig.isCompanyFeatureEnabled() ? (await import('../entities')).StorageConfigWithCompany : (await import('../entities')).StorageConfig;
206
+ return this.dataSourceProvider.getRepository(entity);
207
+ }
208
+ buildWhereWithCompany(id, user) {
209
+ const where = {
210
+ id
211
+ };
212
+ if (this.storageConfig.isCompanyFeatureEnabled() && user?.companyId) {
213
+ where.companyId = user.companyId;
214
+ }
215
+ return where;
216
+ }
217
+ async getStorageConfigBasePath(configId) {
218
+ try {
219
+ const repo = await this.getStorageConfigRepository();
220
+ const config = await repo.findOne({
221
+ where: {
222
+ id: configId
223
+ },
224
+ select: [
225
+ 'id',
226
+ 'config'
227
+ ]
239
228
  });
229
+ return config?.config?.basePath?.replace(/^\.\//, '') ?? null;
230
+ } catch (error) {
231
+ this.logger.warn(`Failed to get basePath for ${configId}: ${ErrorHandler.getErrorMessage(error)}`);
232
+ return null;
233
+ }
234
+ }
235
+ async validateFolder(folderId, user) {
236
+ const entity = this.storageConfig.isCompanyFeatureEnabled() ? FolderWithCompany : Folder;
237
+ const repo = await this.dataSourceProvider.getRepository(entity);
238
+ const folder = await repo.findOne({
239
+ where: this.buildWhereWithCompany(folderId, user)
240
240
  });
241
- return {
242
- query,
243
- isRaw: false
244
- };
241
+ if (!folder) {
242
+ throw new BadRequestException(`Folder ${folderId} not found or access denied`);
243
+ }
244
+ return folder;
245
245
  }
246
- /**
247
- * Override: Extra query manipulation - Auto-filter by user's company and private file permissions
248
- */ async getExtraManipulateQuery(query, filterDto, user) {
249
- const result = await super.getExtraManipulateQuery(query, filterDto, user);
250
- // If company feature enabled and user has companyId, filter by user's company
251
- const enableCompanyFeature = this.storageConfig.isCompanyFeatureEnabled();
252
- if (enableCompanyFeature && user?.companyId) {
253
- query.andWhere('file_manager.companyId = :companyId', {
254
- companyId: user.companyId
246
+ async resolveStorageLocation(dto, user) {
247
+ const configId = dto.storageConfigId;
248
+ if (!configId) return dto.location || FileLocationEnum.LOCAL;
249
+ try {
250
+ const repo = await this.getStorageConfigRepository();
251
+ const config = await repo.findOne({
252
+ where: this.buildWhereWithCompany(configId, user)
255
253
  });
254
+ return config?.storage || dto.location || FileLocationEnum.LOCAL;
255
+ } catch (error) {
256
+ this.logger.warn(`Failed to resolve storage location: ${ErrorHandler.getErrorMessage(error)}`);
257
+ return dto.location || FileLocationEnum.LOCAL;
256
258
  }
257
- // Check if user has permission to see private files
258
- if (user) {
259
- const cacheKey = enableCompanyFeature ? `${USER_ACTION_PERMISSION_CACHE_KEY}_${user.id}_${user.companyId}` : `${USER_ACTION_PERMISSION_CACHE_KEY}_${user.id}`;
260
- const userActions = await this.cacheManager.get(cacheKey);
261
- if (userActions) {
262
- const userActionUrls = userActions.map((item)=>item.url);
263
- if (!userActionUrls.includes(SHOW_PRIVATE_FILE_ACTION)) {
264
- query.andWhere('file_manager.isPrivate = :isPrivate', {
265
- isPrivate: false
266
- });
267
- }
268
- } else {
269
- query.andWhere('file_manager.isPrivate = :isPrivate', {
270
- isPrivate: false
259
+ }
260
+ applyContentTypeFilter(query, value) {
261
+ const patterns = value.split(',').map((t)=>t.trim().replace('*', '%'));
262
+ query.where(new Brackets((qb)=>{
263
+ patterns.forEach((p, i)=>{
264
+ const method = i === 0 ? 'where' : 'orWhere';
265
+ qb[method](`${this.entityName}.contentType LIKE :p${i}`, {
266
+ [`p${i}`]: p
271
267
  });
272
- }
273
- } else {
274
- // No user - only show public files
268
+ });
269
+ }));
270
+ }
271
+ async applyPrivateFileFilter(query, user) {
272
+ if (!user) {
275
273
  query.andWhere('file_manager.isPrivate = :isPrivate', {
276
274
  isPrivate: false
277
275
  });
276
+ return;
278
277
  }
279
- return result;
280
- }
281
- async beforeDeleteOperation(dto, user, _queryRunner) {
282
- if (dto.type === 'permanent') {
283
- const ids = Array.isArray(dto.id) ? dto.id : [
284
- dto.id
285
- ];
286
- const fileManager = await this.repository.findBy({
287
- id: In(ids)
288
- });
289
- // Group files by storage config for batch deletion
290
- const filesByConfig = new Map();
291
- fileManager.forEach((file)=>{
292
- const configId = file.storageConfigId || 'default';
293
- if (!filesByConfig.has(configId)) {
294
- filesByConfig.set(configId, {
295
- keys: [],
296
- location: file.location
297
- });
298
- }
299
- filesByConfig.get(configId).keys.push(file.key);
278
+ const cacheKey = this.storageConfig.isCompanyFeatureEnabled() ? `${PERMISSION_CACHE_KEY}_${user.id}_${user.companyId}` : `${PERMISSION_CACHE_KEY}_${user.id}`;
279
+ const actions = await this.cacheManager.get(cacheKey);
280
+ const canViewPrivate = actions?.some((a)=>a.url === VIEW_PRIVATE_ACTION) ?? false;
281
+ if (!canViewPrivate) {
282
+ query.andWhere('file_manager.isPrivate = :isPrivate', {
283
+ isPrivate: false
300
284
  });
301
- // Delete files from each storage config
302
- for (const [configId, { keys, location }] of filesByConfig){
303
- // For local files with old key format (no '/'), we need to prefix with basePath
304
- let deleteKeys = keys;
305
- if (location === FileLocationEnum.LOCAL && configId !== 'default') {
306
- try {
307
- const enableCompanyFeature = this.storageConfig.isCompanyFeatureEnabled();
308
- const storageConfigEntity = enableCompanyFeature ? (await import('../entities')).StorageConfigWithCompany : (await import('../entities')).StorageConfig;
309
- const storageConfigRepository = await this.dataSourceProvider.getRepository(storageConfigEntity);
310
- const config = await storageConfigRepository.findOne({
311
- where: {
312
- id: configId
313
- },
314
- select: [
315
- 'id',
316
- 'config'
317
- ]
318
- });
319
- if (config && config.config?.basePath) {
320
- const basePath = config.config.basePath.replace(/^\.\//, '');
321
- // Convert old keys to new format
322
- deleteKeys = keys.map((key)=>{
323
- if (!key.includes('/')) {
324
- return `${basePath}/${key}`;
325
- }
326
- return key;
327
- });
328
- }
329
- } catch (error) {
330
- this.logger.warn(`Failed to get basePath for delete: ${error}`);
331
- }
332
- }
333
- await this.uploadService.deleteMultipleFile(deleteKeys, configId === 'default' ? undefined : configId, user ?? undefined, location);
334
- }
335
285
  }
336
286
  }
337
- /**
338
- * Get multiple files and ensure URLs are valid
339
- * Now supports dynamic storage providers (AWS, Azure, SFTP)
340
- */ async getFiles(dtos, protocol, host, user) {
341
- await this.ensureRepositoryInitialized();
342
- const ids = dtos.map((d)=>d.id).filter(Boolean);
343
- if (!ids.length) throw new BadRequestException('No valid file IDs provided');
344
- const files = await this.repository.findBy({
345
- id: In(ids)
346
- });
347
- const now = Date.now();
348
- const expiresIn = this.FILE_URL_EXPIRY_SECONDS;
349
- const updatedFiles = [];
350
- const responses = await Promise.all(files.map(async (file)=>{
351
- let shouldUpdate = false;
352
- // Check if file has storage config ID
353
- if (!file.storageConfigId) {
354
- this.logger.warn(`File ${file.id} has no storageConfigId. Please update this file with a valid storage config.`);
355
- }
356
- // Generate URL if expired or missing
357
- const needsNewUrl = !file.url || file.location === FileLocationEnum.AWS && (typeof file.expiresAt !== 'number' || now >= file.expiresAt) || file.location === FileLocationEnum.AZURE && (typeof file.expiresAt !== 'number' || now >= file.expiresAt);
358
- if (needsNewUrl && file.storageConfigId) {
359
- try {
360
- file.url = await this.uploadService.makeFileUrl(file.key, file.storageConfigId, expiresIn, user);
361
- file.expiresAt = now + expiresIn * 1000;
362
- shouldUpdate = true;
363
- } catch (error) {
364
- this.logger.error(`Failed to generate URL for file ${file.id}: ${error?.message || 'Unknown error'}`);
365
- // Use fallback URL with appUrl from config
366
- const baseUrl = this.getFileBaseUrl(protocol, host);
367
- file.url = `${baseUrl}/storage/upload/file/${file.key}`;
368
- }
369
- }
370
- // SFTP/Local files - always construct full URL (no expiry, but need full path)
371
- if (file.location === FileLocationEnum.SFTP || file.location === FileLocationEnum.LOCAL) {
372
- // For backward compatibility: if key doesn't include basePath, look it up from config
373
- let fileKey = file.key;
374
- // Check if key looks like it's missing the basePath (old format)
375
- // Old format: "uuid-filename.png"
376
- // New format: "uploads/uuid-filename.png" or "uploads/company1/uuid-filename.png"
377
- if (!fileKey.includes('/') && file.storageConfigId) {
378
- try {
379
- const enableCompanyFeature = this.storageConfig.isCompanyFeatureEnabled();
380
- const storageConfigEntity = enableCompanyFeature ? (await import('../entities')).StorageConfigWithCompany : (await import('../entities')).StorageConfig;
381
- const storageConfigRepository = await this.dataSourceProvider.getRepository(storageConfigEntity);
382
- const config = await storageConfigRepository.findOne({
383
- where: {
384
- id: file.storageConfigId
385
- },
386
- select: [
387
- 'id',
388
- 'config'
389
- ]
390
- });
391
- if (config && config.config?.basePath) {
392
- const basePath = config.config.basePath.replace(/^\.\//, ''); // Remove leading ./
393
- fileKey = `${basePath}/${file.key}`;
394
- this.logger.debug(`Prefixed old key with basePath: ${fileKey}`);
395
- }
396
- } catch (error) {
397
- this.logger.warn(`Failed to get basePath for file ${file.id}: ${error}`);
398
- }
399
- }
400
- const baseUrl = this.getFileBaseUrl(protocol, host);
401
- const expectedUrl = `${baseUrl}/storage/upload/file/${fileKey}`;
402
- if (file.url !== expectedUrl) {
403
- file.url = expectedUrl;
404
- shouldUpdate = true;
405
- }
287
+ groupFilesByConfig(files) {
288
+ const grouped = new Map();
289
+ for (const file of files){
290
+ const configId = file.storageConfigId || 'default';
291
+ if (!grouped.has(configId)) {
292
+ grouped.set(configId, {
293
+ keys: [],
294
+ location: file.location
295
+ });
406
296
  }
407
- if (shouldUpdate) updatedFiles.push(file);
408
- return {
409
- id: file.id,
410
- name: file.name,
411
- contentType: file.contentType,
412
- url: file.url || '',
413
- location: file.location,
414
- storageConfigId: file.storageConfigId || undefined
415
- };
416
- }));
417
- // Save only changed records
418
- if (updatedFiles.length > 0) {
297
+ grouped.get(configId).keys.push(file.key);
298
+ }
299
+ return grouped;
300
+ }
301
+ getFileBaseUrl(protocol, host) {
302
+ return this.storageConfig.getAppUrl()?.replace(/\/$/, '') ?? `${protocol}://${host}`;
303
+ }
304
+ async refreshFileUrl(file, protocol, host, now, user) {
305
+ if (!file.storageConfigId) {
306
+ this.logger.warn(`File ${file.id} has no storageConfigId`);
307
+ }
308
+ const isCloudProvider = file.location === FileLocationEnum.AWS || file.location === FileLocationEnum.AZURE;
309
+ const needsNewUrl = !file.url || isCloudProvider && (typeof file.expiresAt !== 'number' || now >= file.expiresAt);
310
+ if (needsNewUrl && file.storageConfigId) {
419
311
  try {
420
- await this.repository.save(updatedFiles);
312
+ file.url = await this.uploadService.makeFileUrl(file.key, file.storageConfigId, URL_EXPIRY_SECONDS, user);
313
+ file.expiresAt = now + URL_EXPIRY_SECONDS * 1000;
314
+ return true;
421
315
  } catch (error) {
422
- this.logger.error(`Failed to save updated file URLs: ${error?.message || 'Unknown error'}`);
316
+ this.logger.error(`Failed to generate URL for ${file.id}: ${ErrorHandler.getErrorMessage(error)}`);
317
+ file.url = `${this.getFileBaseUrl(protocol, host)}/storage/upload/file/${file.key}`;
423
318
  }
424
319
  }
425
- return responses;
320
+ if (file.location === FileLocationEnum.SFTP || file.location === FileLocationEnum.LOCAL) {
321
+ let fileKey = file.key;
322
+ if (!fileKey.includes('/') && file.storageConfigId) {
323
+ const basePath = await this.getStorageConfigBasePath(file.storageConfigId);
324
+ if (basePath) fileKey = `${basePath}/${file.key}`;
325
+ }
326
+ const expectedUrl = `${this.getFileBaseUrl(protocol, host)}/storage/upload/file/${fileKey}`;
327
+ if (file.url !== expectedUrl) {
328
+ file.url = expectedUrl;
329
+ return true;
330
+ }
331
+ }
332
+ return false;
333
+ }
334
+ toFileResponse(file) {
335
+ return {
336
+ id: file.id,
337
+ name: file.name,
338
+ contentType: file.contentType,
339
+ url: file.url || '',
340
+ location: file.location,
341
+ storageConfigId: file.storageConfigId || undefined
342
+ };
426
343
  }
427
- // NOTE: @Inject() required for bundled code - type metadata may be lost during esbuild
428
344
  constructor(cacheManager, utilsService, uploadService, storageConfig, dataSourceProvider){
429
- // Repository will be set asynchronously by RequestScopedApiService
430
- super('file_manager', null, cacheManager, utilsService, FileManagerService.name, true), _define_property(this, "cacheManager", void 0), _define_property(this, "utilsService", void 0), _define_property(this, "uploadService", void 0), _define_property(this, "storageConfig", void 0), _define_property(this, "dataSourceProvider", void 0), _define_property(this, "FILE_URL_EXPIRY_SECONDS", void 0), this.cacheManager = cacheManager, this.utilsService = utilsService, this.uploadService = uploadService, this.storageConfig = storageConfig, this.dataSourceProvider = dataSourceProvider, this.FILE_URL_EXPIRY_SECONDS = 3600;
345
+ super('file_manager', null, cacheManager, utilsService, FileManagerService.name, true), _define_property(this, "cacheManager", void 0), _define_property(this, "utilsService", void 0), _define_property(this, "uploadService", void 0), _define_property(this, "storageConfig", void 0), _define_property(this, "dataSourceProvider", void 0), this.cacheManager = cacheManager, this.utilsService = utilsService, this.uploadService = uploadService, this.storageConfig = storageConfig, this.dataSourceProvider = dataSourceProvider;
431
346
  }
432
347
  }
433
348
  FileManagerService = _ts_decorate([