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