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