@flusys/nestjs-storage 1.1.0-beta → 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.
- package/README.md +148 -6
- package/cjs/config/index.js +0 -1
- package/cjs/config/storage.constants.js +0 -17
- package/cjs/controllers/file-manager.controller.js +44 -1
- package/cjs/controllers/folder.controller.js +44 -1
- package/cjs/controllers/storage-config.controller.js +44 -1
- package/cjs/controllers/upload.controller.js +18 -29
- package/cjs/docs/storage-swagger.config.js +24 -136
- package/cjs/dtos/file-manager.dto.js +70 -34
- package/cjs/dtos/folder.dto.js +15 -9
- package/cjs/dtos/storage-config.dto.js +4 -85
- package/cjs/dtos/upload.dto.js +24 -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 +74 -3
- package/cjs/interfaces/index.js +0 -1
- package/cjs/middlewares/file-serve.middleware.js +113 -100
- package/cjs/modules/storage.module.js +82 -136
- package/cjs/providers/azure-provider.optional.js +10 -38
- package/cjs/providers/local-provider.js +38 -31
- 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 +238 -323
- package/cjs/services/folder.service.js +8 -11
- package/cjs/services/index.js +1 -0
- package/cjs/{config → services}/storage-config.service.js +32 -76
- package/cjs/services/storage-datasource.provider.js +16 -26
- package/cjs/services/storage-provider-config.service.js +15 -37
- package/cjs/services/upload.service.js +72 -88
- package/cjs/utils/file-validator.util.js +458 -0
- package/cjs/utils/image-compressor.util.js +3 -8
- package/config/index.d.ts +0 -1
- package/config/storage.constants.d.ts +0 -8
- package/controllers/upload.controller.d.ts +3 -6
- package/dtos/file-manager.dto.d.ts +12 -5
- package/dtos/folder.dto.d.ts +5 -5
- package/dtos/storage-config.dto.d.ts +7 -13
- package/entities/file-manager-with-company.entity.d.ts +2 -2
- package/entities/file-manager.entity.d.ts +12 -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 +8 -2
- package/fesm/config/index.js +0 -1
- package/fesm/config/storage.constants.js +0 -8
- package/fesm/controllers/file-manager.controller.js +45 -2
- package/fesm/controllers/folder.controller.js +45 -2
- package/fesm/controllers/storage-config.controller.js +45 -2
- package/fesm/controllers/upload.controller.js +19 -30
- package/fesm/docs/storage-swagger.config.js +27 -142
- package/fesm/dtos/file-manager.dto.js +71 -35
- package/fesm/dtos/folder.dto.js +16 -10
- package/fesm/dtos/storage-config.dto.js +8 -95
- package/fesm/dtos/upload.dto.js +25 -19
- 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 +5 -13
- package/fesm/entities/storage-config-with-company.entity.js +3 -4
- package/fesm/entities/storage-config.entity.js +75 -4
- package/fesm/interfaces/index.js +0 -1
- package/fesm/interfaces/storage-config.interface.js +1 -3
- package/fesm/middlewares/file-serve.middleware.js +114 -101
- package/fesm/modules/storage.module.js +83 -136
- package/fesm/providers/azure-provider.optional.js +11 -42
- package/fesm/providers/local-provider.js +38 -31
- 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 -322
- package/fesm/services/folder.service.js +6 -9
- package/fesm/services/index.js +1 -0
- package/fesm/{config → services}/storage-config.service.js +32 -76
- package/fesm/services/storage-datasource.provider.js +16 -26
- package/fesm/services/storage-provider-config.service.js +13 -35
- package/fesm/services/upload.service.js +71 -87
- package/fesm/utils/file-validator.util.js +451 -0
- package/fesm/utils/image-compressor.util.js +3 -8
- package/interfaces/file-manager.interface.d.ts +7 -4
- package/interfaces/index.d.ts +0 -1
- package/interfaces/storage-config.interface.d.ts +0 -20
- 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 +6 -6
- package/providers/azure-provider.optional.d.ts +8 -6
- package/providers/local-provider.d.ts +2 -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 +23 -16
- package/services/folder.service.d.ts +4 -4
- package/services/index.d.ts +1 -0
- package/services/storage-config.service.d.ts +24 -0
- package/services/storage-datasource.provider.d.ts +3 -4
- package/services/storage-provider-config.service.d.ts +4 -4
- package/services/upload.service.d.ts +3 -2
- package/utils/file-validator.util.d.ts +19 -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/cjs/interfaces/file-upload-response.interface.js +0 -4
- package/config/storage-config.service.d.ts +0 -22
- 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
- package/fesm/interfaces/file-upload-response.interface.js +0 -1
- 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
|
|
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
|
-
|
|
89
|
-
const
|
|
90
|
-
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
|
+
];
|
|
91
105
|
let FileManagerService = class FileManagerService extends _classes.RequestScopedApiService {
|
|
92
|
-
|
|
93
|
-
|
|
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
|
|
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
|
|
116
|
+
const existing = await this.repository.findOne({
|
|
117
117
|
where: {
|
|
118
118
|
id: dto.id
|
|
119
119
|
}
|
|
120
120
|
});
|
|
121
|
-
if (!
|
|
122
|
-
|
|
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
|
-
|
|
176
|
-
|
|
177
|
-
|
|
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
|
-
|
|
183
|
-
|
|
184
|
-
fileManager.companyId = user?.companyId ?? null;
|
|
132
|
+
if (this.storageConfig.isCompanyFeatureEnabled()) {
|
|
133
|
+
merged.companyId = user?.companyId ?? null;
|
|
185
134
|
}
|
|
186
|
-
return
|
|
135
|
+
return merged;
|
|
187
136
|
}
|
|
188
137
|
async getSelectQuery(query, _user, select) {
|
|
189
|
-
|
|
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
|
-
|
|
140
|
+
fields.push('file_manager.companyId');
|
|
208
141
|
}
|
|
209
|
-
|
|
210
|
-
|
|
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
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
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((
|
|
196
|
+
...new Set(items.map((i)=>i.storageConfigId).filter(Boolean))
|
|
226
197
|
];
|
|
227
|
-
|
|
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
|
|
238
|
-
const
|
|
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
|
-
|
|
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
|
-
|
|
256
|
-
const enrichedItems = items.map((item)=>({
|
|
219
|
+
return items.map((item)=>({
|
|
257
220
|
...item,
|
|
258
|
-
providerName: item.storageConfigId ?
|
|
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
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
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
|
-
|
|
289
|
-
|
|
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
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
292
|
+
if (!folder) {
|
|
293
|
+
throw new _common.BadRequestException(`Folder ${folderId} not found or access denied`);
|
|
294
|
+
}
|
|
295
|
+
return folder;
|
|
296
296
|
}
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
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
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
const
|
|
314
|
-
|
|
315
|
-
|
|
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
|
-
}
|
|
325
|
-
|
|
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
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
if (
|
|
334
|
-
|
|
335
|
-
|
|
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
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
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
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
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.
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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)(
|
|
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
|
|
413
|
+
typeof _storageconfigservice.StorageConfigService === "undefined" ? Object : _storageconfigservice.StorageConfigService,
|
|
499
414
|
typeof _storagedatasourceprovider.StorageDataSourceProvider === "undefined" ? Object : _storagedatasourceprovider.StorageDataSourceProvider
|
|
500
415
|
])
|
|
501
416
|
], FileManagerService);
|