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