@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.
- package/README.md +44 -1
- package/cjs/config/index.js +0 -1
- package/cjs/config/storage.constants.js +0 -9
- package/cjs/controllers/upload.controller.js +12 -17
- package/cjs/docs/storage-swagger.config.js +24 -136
- package/cjs/dtos/file-manager.dto.js +65 -32
- package/cjs/dtos/folder.dto.js +15 -9
- package/cjs/dtos/storage-config.dto.js +5 -86
- package/cjs/dtos/upload.dto.js +17 -17
- package/cjs/entities/file-manager-with-company.entity.js +3 -4
- package/cjs/entities/file-manager.entity.js +71 -3
- package/cjs/entities/folder-with-company.entity.js +3 -4
- package/cjs/entities/folder.entity.js +19 -3
- package/cjs/entities/index.js +9 -10
- package/cjs/entities/storage-config-with-company.entity.js +3 -4
- package/cjs/entities/storage-config.entity.js +73 -3
- package/cjs/middlewares/file-serve.middleware.js +107 -100
- package/cjs/modules/storage.module.js +82 -136
- package/cjs/providers/azure-provider.optional.js +10 -38
- package/cjs/providers/local-provider.js +0 -43
- package/cjs/providers/s3-provider.optional.js +19 -40
- package/cjs/providers/storage-factory.service.js +54 -99
- package/cjs/providers/storage-provider.registry.js +8 -18
- package/cjs/services/file-manager.service.js +239 -337
- package/cjs/services/folder.service.js +3 -3
- package/cjs/services/index.js +1 -0
- package/cjs/{config → services}/storage-config.service.js +30 -79
- package/cjs/services/storage-datasource.provider.js +16 -26
- package/cjs/services/storage-provider-config.service.js +3 -3
- package/cjs/services/upload.service.js +33 -61
- package/cjs/utils/file-validator.util.js +54 -66
- package/cjs/utils/image-compressor.util.js +2 -5
- package/config/index.d.ts +0 -1
- package/config/storage.constants.d.ts +0 -6
- package/controllers/upload.controller.d.ts +1 -0
- package/dtos/file-manager.dto.d.ts +11 -3
- package/dtos/folder.dto.d.ts +3 -1
- package/dtos/storage-config.dto.d.ts +7 -11
- package/entities/file-manager-with-company.entity.d.ts +2 -2
- package/entities/file-manager.entity.d.ts +11 -2
- package/entities/folder-with-company.entity.d.ts +2 -2
- package/entities/folder.entity.d.ts +4 -2
- package/entities/index.d.ts +3 -4
- package/entities/storage-config-with-company.entity.d.ts +2 -2
- package/entities/storage-config.entity.d.ts +7 -2
- package/fesm/config/index.js +0 -1
- package/fesm/config/storage.constants.js +0 -6
- package/fesm/controllers/upload.controller.js +12 -17
- package/fesm/docs/storage-swagger.config.js +27 -142
- package/fesm/dtos/file-manager.dto.js +66 -33
- package/fesm/dtos/folder.dto.js +16 -10
- package/fesm/dtos/storage-config.dto.js +7 -88
- package/fesm/dtos/upload.dto.js +17 -18
- package/fesm/entities/file-manager-with-company.entity.js +3 -4
- package/fesm/entities/file-manager.entity.js +72 -4
- package/fesm/entities/folder-with-company.entity.js +3 -4
- package/fesm/entities/folder.entity.js +20 -4
- package/fesm/entities/index.js +4 -8
- package/fesm/entities/storage-config-with-company.entity.js +3 -4
- package/fesm/entities/storage-config.entity.js +74 -4
- package/fesm/middlewares/file-serve.middleware.js +107 -100
- package/fesm/modules/storage.module.js +83 -136
- package/fesm/providers/azure-provider.optional.js +11 -42
- package/fesm/providers/local-provider.js +0 -43
- package/fesm/providers/s3-provider.optional.js +20 -44
- package/fesm/providers/storage-factory.service.js +52 -97
- package/fesm/providers/storage-provider.registry.js +10 -20
- package/fesm/services/file-manager.service.js +237 -335
- package/fesm/services/folder.service.js +1 -1
- package/fesm/services/index.js +1 -0
- package/fesm/{config → services}/storage-config.service.js +30 -79
- package/fesm/services/storage-datasource.provider.js +16 -26
- package/fesm/services/storage-provider-config.service.js +1 -1
- package/fesm/services/upload.service.js +31 -59
- package/fesm/utils/file-validator.util.js +54 -66
- package/fesm/utils/image-compressor.util.js +2 -5
- package/interfaces/storage-config.interface.d.ts +1 -2
- package/interfaces/storage-module-options.interface.d.ts +0 -5
- package/middlewares/file-serve.middleware.d.ts +9 -1
- package/modules/storage.module.d.ts +1 -2
- package/package.json +3 -3
- package/providers/azure-provider.optional.d.ts +8 -6
- package/providers/local-provider.d.ts +0 -7
- package/providers/s3-provider.optional.d.ts +9 -7
- package/providers/storage-factory.service.d.ts +8 -9
- package/providers/storage-provider.registry.d.ts +4 -4
- package/services/file-manager.service.d.ts +21 -14
- package/services/folder.service.d.ts +4 -4
- package/services/index.d.ts +1 -0
- package/{config → services}/storage-config.service.d.ts +9 -10
- package/services/storage-datasource.provider.d.ts +3 -4
- package/services/storage-provider-config.service.d.ts +5 -6
- package/services/upload.service.d.ts +5 -5
- package/utils/file-validator.util.d.ts +3 -0
- package/cjs/entities/file-manager-base.entity.js +0 -115
- package/cjs/entities/folder-base.entity.js +0 -55
- package/cjs/entities/storage-config-base.entity.js +0 -93
- package/entities/file-manager-base.entity.d.ts +0 -13
- package/entities/folder-base.entity.d.ts +0 -5
- package/entities/storage-config-base.entity.d.ts +0 -9
- package/fesm/entities/file-manager-base.entity.js +0 -108
- package/fesm/entities/folder-base.entity.js +0 -48
- 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
|
|
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
|
-
|
|
90
|
-
const
|
|
91
|
-
const
|
|
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
|
-
|
|
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
|
|
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
|
|
116
|
+
const existing = await this.repository.findOne({
|
|
118
117
|
where: {
|
|
119
118
|
id: dto.id
|
|
120
119
|
}
|
|
121
120
|
});
|
|
122
|
-
if (!
|
|
123
|
-
|
|
124
|
-
}
|
|
125
|
-
fileManager = dbData;
|
|
121
|
+
if (!existing) throw new _common.NotFoundException('Entity not found for update');
|
|
122
|
+
entity = existing;
|
|
126
123
|
}
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
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
|
-
|
|
189
|
-
|
|
190
|
-
if (enableCompanyFeatureForEntity) {
|
|
191
|
-
mergedFileManager.companyId = user?.companyId ?? null;
|
|
132
|
+
if (this.storageConfig.isCompanyFeatureEnabled()) {
|
|
133
|
+
merged.companyId = user?.companyId ?? null;
|
|
192
134
|
}
|
|
193
|
-
return
|
|
135
|
+
return merged;
|
|
194
136
|
}
|
|
195
137
|
async getSelectQuery(query, _user, select) {
|
|
196
|
-
|
|
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
|
-
|
|
140
|
+
fields.push('file_manager.companyId');
|
|
215
141
|
}
|
|
216
|
-
|
|
217
|
-
|
|
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
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
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((
|
|
196
|
+
...new Set(items.map((i)=>i.storageConfigId).filter(Boolean))
|
|
233
197
|
];
|
|
234
|
-
|
|
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
|
|
245
|
-
const
|
|
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
|
-
|
|
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
|
-
|
|
263
|
-
const enrichedItems = items.map((item)=>({
|
|
219
|
+
return items.map((item)=>({
|
|
264
220
|
...item,
|
|
265
|
-
providerName: item.storageConfigId ?
|
|
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
|
-
|
|
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
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
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
|
-
|
|
297
|
-
|
|
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
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
292
|
+
if (!folder) {
|
|
293
|
+
throw new _common.BadRequestException(`Folder ${folderId} not found or access denied`);
|
|
294
|
+
}
|
|
295
|
+
return folder;
|
|
304
296
|
}
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
}
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
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
|
-
}
|
|
332
|
-
|
|
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
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
if (
|
|
341
|
-
|
|
342
|
-
|
|
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
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
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
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
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.
|
|
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
|
-
|
|
486
|
-
this.
|
|
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
|
-
|
|
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
|
-
|
|
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)(
|
|
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
|
|
413
|
+
typeof _storageconfigservice.StorageConfigService === "undefined" ? Object : _storageconfigservice.StorageConfigService,
|
|
512
414
|
typeof _storagedatasourceprovider.StorageDataSourceProvider === "undefined" ? Object : _storagedatasourceprovider.StorageDataSourceProvider
|
|
513
415
|
])
|
|
514
416
|
], FileManagerService);
|