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