@flusys/nestjs-storage 0.1.0-beta.2 → 1.0.0-beta
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 +3 -7
- package/cjs/config/storage-config.service.js +31 -0
- package/cjs/controllers/file-manager.controller.js +8 -6
- package/cjs/controllers/folder.controller.js +3 -4
- package/cjs/controllers/storage-config.controller.js +3 -4
- package/cjs/controllers/upload.controller.js +22 -169
- package/cjs/dtos/file-manager.dto.js +36 -2
- package/cjs/dtos/upload.dto.js +16 -0
- package/cjs/middlewares/file-serve.middleware.js +204 -0
- package/cjs/middlewares/index.js +18 -0
- package/cjs/modules/storage.module.js +58 -14
- package/cjs/providers/azure-provider.optional.js +1 -2
- package/cjs/providers/local-provider.js +43 -11
- package/cjs/providers/storage-factory.service.js +49 -5
- package/cjs/services/file-manager.service.js +134 -9
- package/cjs/services/folder.service.js +17 -48
- package/cjs/services/storage-datasource.provider.js +10 -16
- package/cjs/services/storage-provider-config.service.js +26 -32
- package/cjs/services/upload.service.js +135 -24
- package/cjs/utils/image-compressor.util.js +43 -5
- package/config/storage-config.service.d.ts +2 -0
- package/controllers/file-manager.controller.d.ts +1 -1
- package/controllers/upload.controller.d.ts +5 -4
- package/dtos/file-manager.dto.d.ts +4 -0
- package/dtos/upload.dto.d.ts +2 -0
- package/fesm/config/storage-config.service.js +31 -0
- package/fesm/controllers/file-manager.controller.js +8 -6
- package/fesm/controllers/folder.controller.js +5 -6
- package/fesm/controllers/storage-config.controller.js +5 -6
- package/fesm/controllers/upload.controller.js +25 -131
- package/fesm/dtos/file-manager.dto.js +36 -2
- package/fesm/dtos/upload.dto.js +16 -0
- package/fesm/middlewares/file-serve.middleware.js +153 -0
- package/fesm/middlewares/index.js +1 -0
- package/fesm/modules/storage.module.js +60 -16
- package/fesm/providers/azure-provider.optional.js +1 -2
- package/fesm/providers/local-provider.js +43 -11
- package/fesm/providers/storage-factory.service.js +50 -6
- package/fesm/services/file-manager.service.js +134 -9
- package/fesm/services/folder.service.js +18 -49
- package/fesm/services/storage-datasource.provider.js +10 -16
- package/fesm/services/storage-provider-config.service.js +26 -32
- package/fesm/services/upload.service.js +135 -24
- package/fesm/utils/image-compressor.util.js +3 -1
- package/interfaces/file-manager.interface.d.ts +2 -0
- package/interfaces/storage-module-options.interface.d.ts +2 -0
- package/interfaces/storage-provider.interface.d.ts +2 -0
- package/middlewares/file-serve.middleware.d.ts +9 -0
- package/middlewares/index.d.ts +1 -0
- package/modules/storage.module.d.ts +3 -2
- package/package.json +26 -11
- package/providers/local-provider.d.ts +2 -1
- package/providers/storage-factory.service.d.ts +4 -0
- package/services/file-manager.service.d.ts +7 -1
- package/services/folder.service.d.ts +1 -2
- package/services/storage-provider-config.service.d.ts +2 -2
- package/services/upload.service.d.ts +6 -2
|
@@ -33,14 +33,19 @@ import { v4 as uuidv4 } from 'uuid';
|
|
|
33
33
|
*/ export class LocalProvider {
|
|
34
34
|
/**
|
|
35
35
|
* Initialize Local File System provider with configuration
|
|
36
|
+
* @param config.basePath - Base path for file storage (default: './uploads')
|
|
37
|
+
* @param config.baseUrl - Optional base URL for generating file URLs
|
|
36
38
|
*/ async initialize(config) {
|
|
37
|
-
|
|
38
|
-
this.
|
|
39
|
+
// Store original relative path for key generation
|
|
40
|
+
this.relativeBasePath = config?.basePath || './uploads';
|
|
41
|
+
// Resolve to absolute path for file operations
|
|
42
|
+
this.basePath = path.resolve(this.relativeBasePath);
|
|
43
|
+
this.baseUrl = config?.baseUrl || '';
|
|
39
44
|
// Ensure base directory exists
|
|
40
45
|
await fs.mkdir(this.basePath, {
|
|
41
46
|
recursive: true
|
|
42
47
|
});
|
|
43
|
-
this.logger.log(`Local Provider initialized: ${this.basePath}`);
|
|
48
|
+
this.logger.log(`Local Provider initialized: ${this.basePath} (relative: ${this.relativeBasePath})`);
|
|
44
49
|
}
|
|
45
50
|
async uploadFile(file, options) {
|
|
46
51
|
let processedBuffer = file.buffer;
|
|
@@ -66,8 +71,9 @@ import { v4 as uuidv4 } from 'uuid';
|
|
|
66
71
|
});
|
|
67
72
|
// Write file to disk
|
|
68
73
|
await fs.writeFile(filePath, processedBuffer);
|
|
69
|
-
// Generate
|
|
70
|
-
|
|
74
|
+
// Generate key that includes the base path (relative to cwd)
|
|
75
|
+
// This makes the key self-contained for serving files
|
|
76
|
+
const relativeKey = path.join(this.relativeBasePath, options.folderPath || '', fileName).replace(/\\/g, '/'); // Normalize path separators
|
|
71
77
|
this.logger.log(`Uploaded file to local storage: ${relativeKey}`);
|
|
72
78
|
return {
|
|
73
79
|
name: fileName,
|
|
@@ -81,7 +87,24 @@ import { v4 as uuidv4 } from 'uuid';
|
|
|
81
87
|
}
|
|
82
88
|
async deleteFile(key) {
|
|
83
89
|
try {
|
|
84
|
-
|
|
90
|
+
// Key now includes the basePath, resolve from cwd
|
|
91
|
+
let filePath = path.resolve(key);
|
|
92
|
+
// Check if file exists at the resolved path
|
|
93
|
+
try {
|
|
94
|
+
await fs.access(filePath);
|
|
95
|
+
} catch {
|
|
96
|
+
// Fallback: try with basePath prefix (for old keys without basePath)
|
|
97
|
+
const fallbackPath = path.join(this.basePath, key);
|
|
98
|
+
try {
|
|
99
|
+
await fs.access(fallbackPath);
|
|
100
|
+
filePath = fallbackPath;
|
|
101
|
+
this.logger.debug(`Using fallback path for delete: ${fallbackPath}`);
|
|
102
|
+
} catch {
|
|
103
|
+
// File doesn't exist at either location
|
|
104
|
+
this.logger.warn(`File not found for deletion: ${key}`);
|
|
105
|
+
return;
|
|
106
|
+
}
|
|
107
|
+
}
|
|
85
108
|
await fs.unlink(filePath);
|
|
86
109
|
this.logger.log(`Deleted file from local storage: ${key}`);
|
|
87
110
|
} catch (_error) {
|
|
@@ -95,8 +118,13 @@ import { v4 as uuidv4 } from 'uuid';
|
|
|
95
118
|
async generatePresignedUrl(key, _expiresInSeconds) {
|
|
96
119
|
// Return public URL or relative path
|
|
97
120
|
// Note: Local storage doesn't support true presigned URLs
|
|
98
|
-
// If baseUrl is provided, return full URL, otherwise return relative path
|
|
99
|
-
|
|
121
|
+
// If baseUrl is provided, return full URL with file serving endpoint, otherwise return relative path
|
|
122
|
+
if (this.baseUrl) {
|
|
123
|
+
// Construct URL with the file serving endpoint
|
|
124
|
+
const baseUrlWithoutTrailingSlash = this.baseUrl.replace(/\/$/, '');
|
|
125
|
+
return `${baseUrlWithoutTrailingSlash}/storage/upload/file/${key}`;
|
|
126
|
+
}
|
|
127
|
+
return `/storage/upload/file/${key}`;
|
|
100
128
|
}
|
|
101
129
|
async healthCheck() {
|
|
102
130
|
try {
|
|
@@ -108,14 +136,16 @@ import { v4 as uuidv4 } from 'uuid';
|
|
|
108
136
|
}
|
|
109
137
|
/**
|
|
110
138
|
* Get the absolute path for a file key
|
|
139
|
+
* Key now includes basePath, so resolve from cwd
|
|
111
140
|
*/ getAbsolutePath(key) {
|
|
112
|
-
return path.
|
|
141
|
+
return path.resolve(key);
|
|
113
142
|
}
|
|
114
143
|
/**
|
|
115
144
|
* Check if a file exists
|
|
116
145
|
*/ async fileExists(key) {
|
|
117
146
|
try {
|
|
118
|
-
|
|
147
|
+
// Key now includes basePath, resolve from cwd
|
|
148
|
+
const filePath = path.resolve(key);
|
|
119
149
|
await fs.access(filePath);
|
|
120
150
|
return true;
|
|
121
151
|
} catch {
|
|
@@ -125,7 +155,8 @@ import { v4 as uuidv4 } from 'uuid';
|
|
|
125
155
|
/**
|
|
126
156
|
* Get file stats
|
|
127
157
|
*/ async getFileStats(key) {
|
|
128
|
-
|
|
158
|
+
// Key now includes basePath, resolve from cwd
|
|
159
|
+
const filePath = path.resolve(key);
|
|
129
160
|
const stats = await fs.stat(filePath);
|
|
130
161
|
return {
|
|
131
162
|
size: stats.size,
|
|
@@ -137,5 +168,6 @@ import { v4 as uuidv4 } from 'uuid';
|
|
|
137
168
|
_define_property(this, "logger", new Logger(LocalProvider.name));
|
|
138
169
|
_define_property(this, "basePath", '');
|
|
139
170
|
_define_property(this, "baseUrl", '');
|
|
171
|
+
_define_property(this, "relativeBasePath", ''); // Path relative to cwd for key generation
|
|
140
172
|
}
|
|
141
173
|
}
|
|
@@ -17,9 +17,18 @@ function _ts_decorate(decorators, target, key, desc) {
|
|
|
17
17
|
else for(var i = decorators.length - 1; i >= 0; i--)if (d = decorators[i]) r = (c < 3 ? d(r) : c > 3 ? d(target, key, r) : d(target, key)) || r;
|
|
18
18
|
return c > 3 && r && Object.defineProperty(target, key, r), r;
|
|
19
19
|
}
|
|
20
|
+
function _ts_metadata(k, v) {
|
|
21
|
+
if (typeof Reflect === "object" && typeof Reflect.metadata === "function") return Reflect.metadata(k, v);
|
|
22
|
+
}
|
|
23
|
+
function _ts_param(paramIndex, decorator) {
|
|
24
|
+
return function(target, key) {
|
|
25
|
+
decorator(target, key, paramIndex);
|
|
26
|
+
};
|
|
27
|
+
}
|
|
20
28
|
import { StorageProviderRegistry } from './storage-provider.registry';
|
|
21
|
-
import { Injectable, Logger, NotFoundException } from '@nestjs/common';
|
|
29
|
+
import { Inject, Injectable, Logger, NotFoundException } from '@nestjs/common';
|
|
22
30
|
import * as crypto from 'crypto';
|
|
31
|
+
import { StorageConfigService } from '../config';
|
|
23
32
|
export class StorageFactoryService {
|
|
24
33
|
/**
|
|
25
34
|
* Generate a stable cache key for provider configuration
|
|
@@ -49,7 +58,19 @@ export class StorageFactoryService {
|
|
|
49
58
|
const instance = new ProviderClass();
|
|
50
59
|
// Initialize if method exists
|
|
51
60
|
if ('initialize' in instance && typeof instance.initialize === 'function') {
|
|
52
|
-
|
|
61
|
+
// For local provider, inject appUrl as fallback for baseUrl
|
|
62
|
+
let initConfig = config.config;
|
|
63
|
+
if (config.provider === 'local' && !config.config?.baseUrl) {
|
|
64
|
+
const appUrl = this.storageConfigService.getAppUrl();
|
|
65
|
+
if (appUrl) {
|
|
66
|
+
initConfig = {
|
|
67
|
+
...config.config,
|
|
68
|
+
baseUrl: appUrl
|
|
69
|
+
};
|
|
70
|
+
this.logger.debug(`Using appUrl from config as baseUrl: ${appUrl}`);
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
await instance.initialize(initConfig);
|
|
53
74
|
}
|
|
54
75
|
// Cache the instance
|
|
55
76
|
this.providerCache.set(providerKey, instance);
|
|
@@ -91,6 +112,20 @@ export class StorageFactoryService {
|
|
|
91
112
|
return StorageProviderRegistry.getAll();
|
|
92
113
|
}
|
|
93
114
|
/**
|
|
115
|
+
* Get the basePath from cached local provider
|
|
116
|
+
* Returns null if no local provider is cached
|
|
117
|
+
*/ getLocalProviderBasePath() {
|
|
118
|
+
// Find cached local provider
|
|
119
|
+
const localKey = Array.from(this.providerCache.keys()).find((key)=>key.startsWith('local'));
|
|
120
|
+
if (localKey) {
|
|
121
|
+
const provider = this.providerCache.get(localKey);
|
|
122
|
+
if (provider && 'basePath' in provider) {
|
|
123
|
+
return provider.basePath;
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
return null;
|
|
127
|
+
}
|
|
128
|
+
/**
|
|
94
129
|
* Cleanup all cached provider connections on module destroy
|
|
95
130
|
* Ensures SFTP connections and other resources are properly released
|
|
96
131
|
*/ async onModuleDestroy() {
|
|
@@ -106,11 +141,20 @@ export class StorageFactoryService {
|
|
|
106
141
|
this.providerCache.clear();
|
|
107
142
|
this.logger.log('Storage provider cleanup complete');
|
|
108
143
|
}
|
|
109
|
-
constructor(){
|
|
110
|
-
_define_property(this, "
|
|
111
|
-
_define_property(this, "
|
|
144
|
+
constructor(storageConfigService){
|
|
145
|
+
_define_property(this, "storageConfigService", void 0);
|
|
146
|
+
_define_property(this, "logger", void 0);
|
|
147
|
+
_define_property(this, "providerCache", void 0);
|
|
148
|
+
this.storageConfigService = storageConfigService;
|
|
149
|
+
this.logger = new Logger(StorageFactoryService.name);
|
|
150
|
+
this.providerCache = new Map();
|
|
112
151
|
}
|
|
113
152
|
}
|
|
114
153
|
StorageFactoryService = _ts_decorate([
|
|
115
|
-
Injectable()
|
|
154
|
+
Injectable(),
|
|
155
|
+
_ts_param(0, Inject(StorageConfigService)),
|
|
156
|
+
_ts_metadata("design:type", Function),
|
|
157
|
+
_ts_metadata("design:paramtypes", [
|
|
158
|
+
typeof StorageConfigService === "undefined" ? Object : StorageConfigService
|
|
159
|
+
])
|
|
116
160
|
], StorageFactoryService);
|
|
@@ -39,6 +39,13 @@ const USER_ACTION_PERMISSION_CACHE_KEY = 'user_action_permission';
|
|
|
39
39
|
const SHOW_PRIVATE_FILE_ACTION = 'storage.file.viewPrivate';
|
|
40
40
|
export class FileManagerService extends RequestScopedApiService {
|
|
41
41
|
/**
|
|
42
|
+
* Get base URL for file serving
|
|
43
|
+
* Priority: appUrl config > request headers
|
|
44
|
+
*/ getFileBaseUrl(protocol, host) {
|
|
45
|
+
const appUrl = this.storageConfig.getAppUrl();
|
|
46
|
+
return appUrl?.replace(/\/$/, '') ?? `${protocol}://${host}`;
|
|
47
|
+
}
|
|
48
|
+
/**
|
|
42
49
|
* Resolve entity class for this service
|
|
43
50
|
* @returns FileManager or FileManagerWithCompany based on configuration
|
|
44
51
|
*/ resolveEntity() {
|
|
@@ -137,6 +144,7 @@ export class FileManagerService extends RequestScopedApiService {
|
|
|
137
144
|
'key',
|
|
138
145
|
'url',
|
|
139
146
|
'location',
|
|
147
|
+
'storageConfigId',
|
|
140
148
|
'isPrivate',
|
|
141
149
|
'createdAt',
|
|
142
150
|
'deletedAt'
|
|
@@ -147,6 +155,7 @@ export class FileManagerService extends RequestScopedApiService {
|
|
|
147
155
|
if (this.storageConfig.isCompanyFeatureEnabled()) {
|
|
148
156
|
selectFields.push('file_manager.companyId');
|
|
149
157
|
}
|
|
158
|
+
// Join folder
|
|
150
159
|
selectFields.push('folder.id');
|
|
151
160
|
selectFields.push('folder.name');
|
|
152
161
|
query.leftJoinAndSelect('file_manager.folder', 'folder');
|
|
@@ -156,6 +165,57 @@ export class FileManagerService extends RequestScopedApiService {
|
|
|
156
165
|
isRaw: false
|
|
157
166
|
};
|
|
158
167
|
}
|
|
168
|
+
/**
|
|
169
|
+
* Enrich file list with provider names from storage_config table
|
|
170
|
+
* Call this method to add providerName to the list results
|
|
171
|
+
*/ async enrichWithProviderNames(items) {
|
|
172
|
+
// Get unique storage config IDs
|
|
173
|
+
const configIds = [
|
|
174
|
+
...new Set(items.map((item)=>item.storageConfigId).filter(Boolean))
|
|
175
|
+
];
|
|
176
|
+
this.logger.debug(`enrichWithProviderNames: items count=${items.length}, configIds=${JSON.stringify(configIds)}`);
|
|
177
|
+
if (configIds.length === 0) {
|
|
178
|
+
this.logger.debug('enrichWithProviderNames: No storage config IDs found in items');
|
|
179
|
+
return items.map((item)=>({
|
|
180
|
+
...item,
|
|
181
|
+
providerName: undefined
|
|
182
|
+
}));
|
|
183
|
+
}
|
|
184
|
+
// Fetch storage config names
|
|
185
|
+
try {
|
|
186
|
+
const enableCompanyFeature = this.storageConfig.isCompanyFeatureEnabled();
|
|
187
|
+
const storageConfigEntity = enableCompanyFeature ? (await import('../entities')).StorageConfigWithCompany : (await import('../entities')).StorageConfig;
|
|
188
|
+
const storageConfigRepository = await this.dataSourceProvider.getRepository(storageConfigEntity);
|
|
189
|
+
const configs = await storageConfigRepository.find({
|
|
190
|
+
where: {
|
|
191
|
+
id: In(configIds)
|
|
192
|
+
},
|
|
193
|
+
select: [
|
|
194
|
+
'id',
|
|
195
|
+
'name'
|
|
196
|
+
]
|
|
197
|
+
});
|
|
198
|
+
this.logger.debug(`enrichWithProviderNames: Found ${configs.length} configs: ${JSON.stringify(configs)}`);
|
|
199
|
+
// Create a map for quick lookup
|
|
200
|
+
const configNameMap = new Map(configs.map((c)=>[
|
|
201
|
+
c.id,
|
|
202
|
+
c.name
|
|
203
|
+
]));
|
|
204
|
+
// Enrich items with provider names
|
|
205
|
+
const enrichedItems = items.map((item)=>({
|
|
206
|
+
...item,
|
|
207
|
+
providerName: item.storageConfigId ? configNameMap.get(item.storageConfigId) : undefined
|
|
208
|
+
}));
|
|
209
|
+
this.logger.debug(`enrichWithProviderNames: First enriched item: ${JSON.stringify(enrichedItems[0])}`);
|
|
210
|
+
return enrichedItems;
|
|
211
|
+
} catch (error) {
|
|
212
|
+
this.logger.warn(`Failed to fetch provider names: ${error}`);
|
|
213
|
+
return items.map((item)=>({
|
|
214
|
+
...item,
|
|
215
|
+
providerName: undefined
|
|
216
|
+
}));
|
|
217
|
+
}
|
|
218
|
+
}
|
|
159
219
|
async getFilterQuery(query, filter, _user) {
|
|
160
220
|
Object.entries(filter).forEach(([key, value])=>{
|
|
161
221
|
if (key === 'contentType') {
|
|
@@ -218,7 +278,7 @@ export class FileManagerService extends RequestScopedApiService {
|
|
|
218
278
|
}
|
|
219
279
|
return result;
|
|
220
280
|
}
|
|
221
|
-
async beforeDeleteOperation(dto,
|
|
281
|
+
async beforeDeleteOperation(dto, user, _queryRunner) {
|
|
222
282
|
if (dto.type === 'permanent') {
|
|
223
283
|
const ids = Array.isArray(dto.id) ? dto.id : [
|
|
224
284
|
dto.id
|
|
@@ -231,13 +291,46 @@ export class FileManagerService extends RequestScopedApiService {
|
|
|
231
291
|
fileManager.forEach((file)=>{
|
|
232
292
|
const configId = file.storageConfigId || 'default';
|
|
233
293
|
if (!filesByConfig.has(configId)) {
|
|
234
|
-
filesByConfig.set(configId,
|
|
294
|
+
filesByConfig.set(configId, {
|
|
295
|
+
keys: [],
|
|
296
|
+
location: file.location
|
|
297
|
+
});
|
|
235
298
|
}
|
|
236
|
-
filesByConfig.get(configId).push(file.key);
|
|
299
|
+
filesByConfig.get(configId).keys.push(file.key);
|
|
237
300
|
});
|
|
238
301
|
// Delete files from each storage config
|
|
239
|
-
for (const [configId, keys] of filesByConfig){
|
|
240
|
-
|
|
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);
|
|
241
334
|
}
|
|
242
335
|
}
|
|
243
336
|
}
|
|
@@ -269,13 +362,43 @@ export class FileManagerService extends RequestScopedApiService {
|
|
|
269
362
|
shouldUpdate = true;
|
|
270
363
|
} catch (error) {
|
|
271
364
|
this.logger.error(`Failed to generate URL for file ${file.id}: ${error?.message || 'Unknown error'}`);
|
|
272
|
-
// Use fallback URL
|
|
273
|
-
|
|
365
|
+
// Use fallback URL with appUrl from config
|
|
366
|
+
const baseUrl = this.getFileBaseUrl(protocol, host);
|
|
367
|
+
file.url = `${baseUrl}/storage/upload/file/${file.key}`;
|
|
274
368
|
}
|
|
275
369
|
}
|
|
276
370
|
// SFTP/Local files - always construct full URL (no expiry, but need full path)
|
|
277
371
|
if (file.location === FileLocationEnum.SFTP || file.location === FileLocationEnum.LOCAL) {
|
|
278
|
-
|
|
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}`;
|
|
279
402
|
if (file.url !== expectedUrl) {
|
|
280
403
|
file.url = expectedUrl;
|
|
281
404
|
shouldUpdate = true;
|
|
@@ -286,7 +409,9 @@ export class FileManagerService extends RequestScopedApiService {
|
|
|
286
409
|
id: file.id,
|
|
287
410
|
name: file.name,
|
|
288
411
|
contentType: file.contentType,
|
|
289
|
-
url: file.url || ''
|
|
412
|
+
url: file.url || '',
|
|
413
|
+
location: file.location,
|
|
414
|
+
storageConfigId: file.storageConfigId || undefined
|
|
290
415
|
};
|
|
291
416
|
}));
|
|
292
417
|
// Save only changed records
|
|
@@ -27,51 +27,29 @@ function _ts_param(paramIndex, decorator) {
|
|
|
27
27
|
}
|
|
28
28
|
import { RequestScopedApiService, HybridCache } from '@flusys/nestjs-shared/classes';
|
|
29
29
|
import { UtilsService } from '@flusys/nestjs-shared/modules';
|
|
30
|
-
import { Inject, Injectable,
|
|
30
|
+
import { Inject, Injectable, Scope } from '@nestjs/common';
|
|
31
31
|
import { StorageConfigService } from '../config';
|
|
32
32
|
import { Folder, FolderWithCompany } from '../entities';
|
|
33
33
|
import { StorageDataSourceProvider } from './storage-datasource.provider';
|
|
34
34
|
export class FolderService extends RequestScopedApiService {
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
* @returns Folder or FolderWithCompany based on configuration
|
|
38
|
-
*/ resolveEntity() {
|
|
39
|
-
const enableCompanyFeature = this.storageConfig.isCompanyFeatureEnabled();
|
|
40
|
-
return enableCompanyFeature ? FolderWithCompany : Folder;
|
|
35
|
+
resolveEntity() {
|
|
36
|
+
return this.storageConfig.isCompanyFeatureEnabled() ? FolderWithCompany : Folder;
|
|
41
37
|
}
|
|
42
|
-
|
|
43
|
-
* Get DataSource provider for this service
|
|
44
|
-
* @returns StorageDataSourceProvider instance
|
|
45
|
-
*/ getDataSourceProvider() {
|
|
38
|
+
getDataSourceProvider() {
|
|
46
39
|
return this.dataSourceProvider;
|
|
47
40
|
}
|
|
41
|
+
// Entity Conversion
|
|
48
42
|
async convertSingleDtoToEntity(dto, user) {
|
|
49
|
-
|
|
50
|
-
//
|
|
51
|
-
if (
|
|
52
|
-
|
|
53
|
-
where: {
|
|
54
|
-
id: dto.id
|
|
55
|
-
}
|
|
56
|
-
});
|
|
57
|
-
if (!dbData) {
|
|
58
|
-
throw new NotFoundException('No such entity data found for update! Please, Try Again.');
|
|
59
|
-
}
|
|
60
|
-
folder = dbData;
|
|
61
|
-
}
|
|
62
|
-
// Set company/branch IDs if company feature is enabled
|
|
63
|
-
folder = {
|
|
64
|
-
...folder,
|
|
65
|
-
...dto
|
|
66
|
-
};
|
|
67
|
-
// Only set company fields if they exist on the entity (when company feature is enabled)
|
|
68
|
-
if ('companyId' in folder) {
|
|
69
|
-
folder.companyId = user?.companyId ?? null;
|
|
43
|
+
const entity = await super.convertSingleDtoToEntity(dto, user);
|
|
44
|
+
// Set companyId from user context if company feature enabled
|
|
45
|
+
if (this.storageConfig.isCompanyFeatureEnabled()) {
|
|
46
|
+
entity.companyId = user?.companyId ?? null;
|
|
70
47
|
}
|
|
71
|
-
return
|
|
48
|
+
return entity;
|
|
72
49
|
}
|
|
50
|
+
// Query Customization
|
|
73
51
|
async getSelectQuery(query, _user, select) {
|
|
74
|
-
if (!select
|
|
52
|
+
if (!select?.length) {
|
|
75
53
|
select = [
|
|
76
54
|
'id',
|
|
77
55
|
'name',
|
|
@@ -79,35 +57,26 @@ export class FolderService extends RequestScopedApiService {
|
|
|
79
57
|
'createdAt',
|
|
80
58
|
'deletedAt'
|
|
81
59
|
];
|
|
60
|
+
if (this.storageConfig.isCompanyFeatureEnabled()) {
|
|
61
|
+
select.push('companyId');
|
|
62
|
+
}
|
|
82
63
|
}
|
|
83
|
-
|
|
84
|
-
// Add company context fields if company feature is enabled
|
|
85
|
-
// The entity will have these fields only if company feature is enabled
|
|
86
|
-
if (this.storageConfig.isCompanyFeatureEnabled()) {
|
|
87
|
-
selectFields.push('folder.companyId');
|
|
88
|
-
}
|
|
89
|
-
query.select(selectFields);
|
|
64
|
+
query.select(select.map((f)=>`${this.entityName}.${f}`));
|
|
90
65
|
return {
|
|
91
66
|
query,
|
|
92
67
|
isRaw: false
|
|
93
68
|
};
|
|
94
69
|
}
|
|
95
|
-
|
|
96
|
-
* Override: Extra query manipulation - Auto-filter by user's company
|
|
97
|
-
*/ async getExtraManipulateQuery(query, filterDto, user) {
|
|
70
|
+
async getExtraManipulateQuery(query, filterDto, user) {
|
|
98
71
|
const result = await super.getExtraManipulateQuery(query, filterDto, user);
|
|
99
|
-
|
|
100
|
-
const enableCompanyFeature = this.storageConfig.isCompanyFeatureEnabled();
|
|
101
|
-
if (enableCompanyFeature && user?.companyId) {
|
|
72
|
+
if (this.storageConfig.isCompanyFeatureEnabled() && user?.companyId) {
|
|
102
73
|
query.andWhere('folder.companyId = :companyId', {
|
|
103
74
|
companyId: user.companyId
|
|
104
75
|
});
|
|
105
76
|
}
|
|
106
77
|
return result;
|
|
107
78
|
}
|
|
108
|
-
// NOTE: @Inject() required for bundled code - type metadata may be lost during esbuild
|
|
109
79
|
constructor(cacheManager, utilsService, storageConfig, dataSourceProvider){
|
|
110
|
-
// Repository will be set asynchronously by RequestScopedApiService
|
|
111
80
|
super('folder', null, cacheManager, utilsService, FolderService.name, true), _define_property(this, "cacheManager", void 0), _define_property(this, "utilsService", void 0), _define_property(this, "storageConfig", void 0), _define_property(this, "dataSourceProvider", void 0), this.cacheManager = cacheManager, this.utilsService = utilsService, this.storageConfig = storageConfig, this.dataSourceProvider = dataSourceProvider;
|
|
112
81
|
}
|
|
113
82
|
}
|
|
@@ -32,10 +32,8 @@ import { Request } from 'express';
|
|
|
32
32
|
import { StorageModuleOptions } from '../interfaces';
|
|
33
33
|
import { STORAGE_MODULE_OPTIONS } from '../config/storage.constants';
|
|
34
34
|
export class StorageDataSourceProvider extends MultiTenantDataSourceService {
|
|
35
|
-
//
|
|
36
|
-
/**
|
|
37
|
-
* Build parent options from StorageModuleOptions
|
|
38
|
-
*/ static buildParentOptions(options) {
|
|
35
|
+
// Factory Methods
|
|
36
|
+
/** Build parent options from StorageModuleOptions */ static buildParentOptions(options) {
|
|
39
37
|
return {
|
|
40
38
|
bootstrapAppConfig: options.bootstrapAppConfig,
|
|
41
39
|
defaultDatabaseConfig: options.config?.defaultDatabaseConfig,
|
|
@@ -43,10 +41,8 @@ export class StorageDataSourceProvider extends MultiTenantDataSourceService {
|
|
|
43
41
|
tenants: options.config?.tenants
|
|
44
42
|
};
|
|
45
43
|
}
|
|
46
|
-
//
|
|
47
|
-
/**
|
|
48
|
-
* Get global enable company feature flag
|
|
49
|
-
*/ getEnableCompanyFeature() {
|
|
44
|
+
// Feature Flags
|
|
45
|
+
/** Get global enable company feature flag */ getEnableCompanyFeature() {
|
|
50
46
|
return this.storageOptions.bootstrapAppConfig?.enableCompanyFeature ?? false;
|
|
51
47
|
}
|
|
52
48
|
/**
|
|
@@ -60,11 +56,11 @@ export class StorageDataSourceProvider extends MultiTenantDataSourceService {
|
|
|
60
56
|
*/ getEnableCompanyFeatureForCurrentTenant() {
|
|
61
57
|
return this.getEnableCompanyFeatureForTenant(this.getCurrentTenant() ?? undefined);
|
|
62
58
|
}
|
|
63
|
-
//
|
|
59
|
+
// Entity Management
|
|
64
60
|
/**
|
|
65
|
-
* Get storage entities for migrations based on company feature flag
|
|
66
|
-
*
|
|
67
|
-
*
|
|
61
|
+
* Get storage entities for migrations based on company feature flag.
|
|
62
|
+
* For TypeORM repositories, we always use the base entities (FileManager, etc.)
|
|
63
|
+
* but for migrations, we need the correct entity based on the feature flag.
|
|
68
64
|
*/ async getStorageEntities(enableCompanyFeature) {
|
|
69
65
|
const enable = enableCompanyFeature ?? this.getEnableCompanyFeature();
|
|
70
66
|
const { FileManager, Folder, StorageConfig } = await import('../entities');
|
|
@@ -84,10 +80,8 @@ export class StorageDataSourceProvider extends MultiTenantDataSourceService {
|
|
|
84
80
|
StorageConfig
|
|
85
81
|
];
|
|
86
82
|
}
|
|
87
|
-
//
|
|
88
|
-
/**
|
|
89
|
-
* Override to dynamically set entities based on tenant config
|
|
90
|
-
*/ async createDataSourceFromConfig(config) {
|
|
83
|
+
// Overrides
|
|
84
|
+
/** Override to dynamically set entities based on tenant config */ async createDataSourceFromConfig(config) {
|
|
91
85
|
const currentTenant = this.getCurrentTenant();
|
|
92
86
|
const enableCompanyFeature = this.getEnableCompanyFeatureForTenant(currentTenant ?? undefined);
|
|
93
87
|
const entities = await this.getStorageEntities(enableCompanyFeature);
|