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