@flusys/nestjs-storage 3.0.0 → 4.0.0-rc
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 +1 -1
- package/cjs/config/index.js +1 -0
- package/cjs/config/message-keys.js +90 -0
- package/cjs/controllers/file-manager.controller.js +7 -1
- package/cjs/controllers/folder.controller.js +1 -0
- package/cjs/controllers/storage-config.controller.js +1 -0
- package/cjs/controllers/upload.controller.js +9 -4
- package/cjs/middlewares/file-serve.middleware.js +39 -15
- package/cjs/modules/storage.module.js +2 -2
- package/cjs/providers/azure-provider.optional.js +8 -5
- package/cjs/providers/local-provider.js +16 -14
- package/cjs/providers/s3-provider.optional.js +8 -5
- package/cjs/providers/sftp-provider.optional.js +13 -11
- package/cjs/providers/storage-factory.service.js +19 -10
- package/cjs/services/file-manager.service.js +30 -23
- package/cjs/services/folder.service.js +2 -1
- package/cjs/services/storage-datasource.provider.js +6 -2
- package/cjs/services/storage-provider-config.service.js +2 -1
- package/cjs/services/upload.service.js +129 -79
- package/cjs/utils/file-validator.util.js +4 -22
- package/config/index.d.ts +1 -0
- package/config/message-keys.d.ts +114 -0
- package/controllers/folder.controller.d.ts +7 -7
- package/controllers/storage-config.controller.d.ts +7 -7
- package/controllers/upload.controller.d.ts +1 -1
- package/fesm/config/index.js +1 -0
- package/fesm/config/message-keys.js +64 -0
- package/fesm/controllers/file-manager.controller.js +7 -1
- package/fesm/controllers/folder.controller.js +1 -0
- package/fesm/controllers/storage-config.controller.js +1 -0
- package/fesm/controllers/upload.controller.js +9 -4
- package/fesm/middlewares/file-serve.middleware.js +40 -16
- package/fesm/modules/storage.module.js +2 -2
- package/fesm/providers/azure-provider.optional.js +9 -6
- package/fesm/providers/local-provider.js +28 -26
- package/fesm/providers/s3-provider.optional.js +9 -6
- package/fesm/providers/sftp-provider.optional.js +26 -24
- package/fesm/providers/storage-factory.service.js +20 -11
- package/fesm/services/file-manager.service.js +31 -24
- package/fesm/services/folder.service.js +2 -1
- package/fesm/services/storage-datasource.provider.js +7 -3
- package/fesm/services/storage-provider-config.service.js +2 -1
- package/fesm/services/upload.service.js +131 -81
- package/fesm/utils/file-validator.util.js +3 -21
- package/middlewares/file-serve.middleware.d.ts +0 -1
- package/package.json +3 -3
- package/providers/azure-provider.optional.d.ts +0 -1
- package/providers/local-provider.d.ts +0 -1
- package/providers/s3-provider.optional.d.ts +0 -1
- package/providers/sftp-provider.optional.d.ts +0 -1
- package/providers/storage-factory.service.d.ts +0 -1
- package/services/file-manager.service.d.ts +2 -2
- package/services/storage-datasource.provider.d.ts +0 -2
- package/services/upload.service.d.ts +0 -1
- package/utils/file-validator.util.d.ts +0 -1
|
@@ -1,15 +1,4 @@
|
|
|
1
|
-
|
|
2
|
-
* LOCAL File System Provider
|
|
3
|
-
*
|
|
4
|
-
* This provider uses Node.js built-in 'fs' module - NO external dependencies required!
|
|
5
|
-
* Perfect for same-server storage scenarios.
|
|
6
|
-
*
|
|
7
|
-
* Usage:
|
|
8
|
-
* import { LocalProvider } from '@flusys/nestjs-storage/providers/local-provider';
|
|
9
|
-
* import { StorageProviderRegistry, FileLocationEnum } from '@flusys/nestjs-storage';
|
|
10
|
-
*
|
|
11
|
-
* StorageProviderRegistry.register(FileLocationEnum.LOCAL, LocalProvider);
|
|
12
|
-
*/ function _define_property(obj, key, value) {
|
|
1
|
+
function _define_property(obj, key, value) {
|
|
13
2
|
if (key in obj) {
|
|
14
3
|
Object.defineProperty(obj, key, {
|
|
15
4
|
value: value,
|
|
@@ -22,8 +11,20 @@
|
|
|
22
11
|
}
|
|
23
12
|
return obj;
|
|
24
13
|
}
|
|
14
|
+
/**
|
|
15
|
+
* LOCAL File System Provider
|
|
16
|
+
*
|
|
17
|
+
* This provider uses Node.js built-in 'fs' module - NO external dependencies required!
|
|
18
|
+
* Perfect for same-server storage scenarios.
|
|
19
|
+
*
|
|
20
|
+
* Usage:
|
|
21
|
+
* import { LocalProvider } from '@flusys/nestjs-storage/providers/local-provider';
|
|
22
|
+
* import { StorageProviderRegistry, FileLocationEnum } from '@flusys/nestjs-storage';
|
|
23
|
+
*
|
|
24
|
+
* StorageProviderRegistry.register(FileLocationEnum.LOCAL, LocalProvider);
|
|
25
|
+
*/ import { BadRequestException } from '@nestjs/common';
|
|
26
|
+
import { SYSTEM_MESSAGES } from '@flusys/nestjs-shared/constants';
|
|
25
27
|
import { ImageCompressor } from '../utils/image-compressor.util';
|
|
26
|
-
import { Logger } from '@nestjs/common';
|
|
27
28
|
import * as fs from 'fs/promises';
|
|
28
29
|
import * as path from 'path';
|
|
29
30
|
import { v4 as uuidv4 } from 'uuid';
|
|
@@ -39,8 +40,10 @@ import { v4 as uuidv4 } from 'uuid';
|
|
|
39
40
|
const normalizedBasePath = path.resolve(this.basePath);
|
|
40
41
|
const normalizedTargetPath = path.resolve(targetPath);
|
|
41
42
|
if (!normalizedTargetPath.startsWith(normalizedBasePath + path.sep) && normalizedTargetPath !== normalizedBasePath) {
|
|
42
|
-
|
|
43
|
-
|
|
43
|
+
throw new BadRequestException({
|
|
44
|
+
message: 'Invalid path: Path traversal attempt detected',
|
|
45
|
+
messageKey: SYSTEM_MESSAGES.PATH_TRAVERSAL_DETECTED
|
|
46
|
+
});
|
|
44
47
|
}
|
|
45
48
|
}
|
|
46
49
|
/**
|
|
@@ -48,10 +51,16 @@ import { v4 as uuidv4 } from 'uuid';
|
|
|
48
51
|
* @throws Error if key contains suspicious patterns
|
|
49
52
|
*/ validateKeyFormat(key) {
|
|
50
53
|
if (!key || typeof key !== 'string' || key.trim().length === 0) {
|
|
51
|
-
throw new
|
|
54
|
+
throw new BadRequestException({
|
|
55
|
+
message: 'Invalid file key: empty or invalid',
|
|
56
|
+
messageKey: SYSTEM_MESSAGES.INVALID_FILE_KEY
|
|
57
|
+
});
|
|
52
58
|
}
|
|
53
59
|
if (key.includes('\0')) {
|
|
54
|
-
throw new
|
|
60
|
+
throw new BadRequestException({
|
|
61
|
+
message: 'Invalid file key: contains null bytes',
|
|
62
|
+
messageKey: SYSTEM_MESSAGES.INVALID_FILE_KEY
|
|
63
|
+
});
|
|
55
64
|
}
|
|
56
65
|
}
|
|
57
66
|
/**
|
|
@@ -68,7 +77,6 @@ import { v4 as uuidv4 } from 'uuid';
|
|
|
68
77
|
await fs.mkdir(this.basePath, {
|
|
69
78
|
recursive: true
|
|
70
79
|
});
|
|
71
|
-
this.logger.log(`Local Provider initialized: ${this.basePath} (relative: ${this.relativeBasePath})`);
|
|
72
80
|
}
|
|
73
81
|
async uploadFile(file, options) {
|
|
74
82
|
let processedBuffer = file.buffer;
|
|
@@ -101,7 +109,6 @@ import { v4 as uuidv4 } from 'uuid';
|
|
|
101
109
|
// Generate key that includes the base path (relative to cwd)
|
|
102
110
|
// This makes the key self-contained for serving files
|
|
103
111
|
const relativeKey = path.join(this.relativeBasePath, options.folderPath || '', fileName).replace(/\\/g, '/'); // Normalize path separators
|
|
104
|
-
this.logger.log(`Uploaded file to local storage: ${relativeKey}`);
|
|
105
112
|
return {
|
|
106
113
|
name: fileName,
|
|
107
114
|
key: relativeKey,
|
|
@@ -131,26 +138,22 @@ import { v4 as uuidv4 } from 'uuid';
|
|
|
131
138
|
try {
|
|
132
139
|
await fs.access(fallbackPath);
|
|
133
140
|
filePath = fallbackPath;
|
|
134
|
-
this.logger.debug(`Using fallback path for delete: ${fallbackPath}`);
|
|
135
141
|
} catch {
|
|
136
|
-
// File doesn't exist at either location
|
|
137
|
-
this.logger.warn(`File not found for deletion: ${key}`);
|
|
142
|
+
// File doesn't exist at either location - silent return
|
|
138
143
|
return;
|
|
139
144
|
}
|
|
140
145
|
}
|
|
141
146
|
await fs.unlink(filePath);
|
|
142
|
-
this.logger.log(`Deleted file from local storage: ${key}`);
|
|
143
147
|
} catch (error) {
|
|
144
148
|
// Re-throw security errors
|
|
145
149
|
if (error instanceof Error && error.message.includes('Invalid')) {
|
|
146
150
|
throw error;
|
|
147
151
|
}
|
|
148
|
-
|
|
152
|
+
// Silent failure for other errors
|
|
149
153
|
}
|
|
150
154
|
}
|
|
151
155
|
async deleteMultipleFiles(keys) {
|
|
152
156
|
await Promise.all(keys.map((key)=>this.deleteFile(key)));
|
|
153
|
-
this.logger.log(`Deleted ${keys.length} files from local storage`);
|
|
154
157
|
}
|
|
155
158
|
async generatePresignedUrl(key, _expiresInSeconds) {
|
|
156
159
|
// Return public URL or relative path
|
|
@@ -172,7 +175,6 @@ import { v4 as uuidv4 } from 'uuid';
|
|
|
172
175
|
}
|
|
173
176
|
}
|
|
174
177
|
constructor(){
|
|
175
|
-
_define_property(this, "logger", new Logger(LocalProvider.name));
|
|
176
178
|
_define_property(this, "basePath", '');
|
|
177
179
|
_define_property(this, "baseUrl", '');
|
|
178
180
|
_define_property(this, "relativeBasePath", ''); // Path relative to cwd for key generation
|
|
@@ -14,7 +14,8 @@ function _define_property(obj, key, value) {
|
|
|
14
14
|
/**
|
|
15
15
|
* Optional AWS S3 Storage Provider
|
|
16
16
|
* Requires: npm install @aws-sdk/client-s3 @aws-sdk/lib-storage @aws-sdk/s3-request-presigner
|
|
17
|
-
*/ import {
|
|
17
|
+
*/ import { InternalServerErrorException } from '@nestjs/common';
|
|
18
|
+
import { SYSTEM_MESSAGES } from '@flusys/nestjs-shared/constants';
|
|
18
19
|
import { v4 as uuidv4 } from 'uuid';
|
|
19
20
|
import { ImageCompressor } from '../utils/image-compressor.util';
|
|
20
21
|
export class S3Provider {
|
|
@@ -31,9 +32,14 @@ export class S3Provider {
|
|
|
31
32
|
secretAccessKey: config.secretAccessKey
|
|
32
33
|
} : undefined
|
|
33
34
|
});
|
|
34
|
-
this.logger.log(`S3 Provider initialized: region=${config.region}, bucket=${config.bucket}`);
|
|
35
35
|
} catch {
|
|
36
|
-
throw new
|
|
36
|
+
throw new InternalServerErrorException({
|
|
37
|
+
message: 'AWS SDK not found',
|
|
38
|
+
messageKey: SYSTEM_MESSAGES.SDK_NOT_INSTALLED,
|
|
39
|
+
messageParams: {
|
|
40
|
+
sdk: '@aws-sdk/client-s3'
|
|
41
|
+
}
|
|
42
|
+
});
|
|
37
43
|
}
|
|
38
44
|
}
|
|
39
45
|
async uploadFile(file, options) {
|
|
@@ -77,7 +83,6 @@ export class S3Provider {
|
|
|
77
83
|
Bucket: this.bucketName,
|
|
78
84
|
Key: key
|
|
79
85
|
}));
|
|
80
|
-
this.logger.log(`Deleted file from S3: ${key}`);
|
|
81
86
|
}
|
|
82
87
|
async deleteMultipleFiles(keys) {
|
|
83
88
|
const { DeleteObjectsCommand } = await import('@aws-sdk/client-s3');
|
|
@@ -89,7 +94,6 @@ export class S3Provider {
|
|
|
89
94
|
}))
|
|
90
95
|
}
|
|
91
96
|
}));
|
|
92
|
-
this.logger.log(`Deleted ${keys.length} files from S3`);
|
|
93
97
|
}
|
|
94
98
|
async generatePresignedUrl(key, expiresInSeconds = 3600) {
|
|
95
99
|
const { GetObjectCommand } = await import('@aws-sdk/client-s3');
|
|
@@ -113,7 +117,6 @@ export class S3Provider {
|
|
|
113
117
|
}
|
|
114
118
|
}
|
|
115
119
|
constructor(){
|
|
116
|
-
_define_property(this, "logger", new Logger(S3Provider.name));
|
|
117
120
|
_define_property(this, "s3", void 0);
|
|
118
121
|
_define_property(this, "bucketName", '');
|
|
119
122
|
_define_property(this, "region", '');
|
|
@@ -1,3 +1,16 @@
|
|
|
1
|
+
function _define_property(obj, key, value) {
|
|
2
|
+
if (key in obj) {
|
|
3
|
+
Object.defineProperty(obj, key, {
|
|
4
|
+
value: value,
|
|
5
|
+
enumerable: true,
|
|
6
|
+
configurable: true,
|
|
7
|
+
writable: true
|
|
8
|
+
});
|
|
9
|
+
} else {
|
|
10
|
+
obj[key] = value;
|
|
11
|
+
}
|
|
12
|
+
return obj;
|
|
13
|
+
}
|
|
1
14
|
/**
|
|
2
15
|
* OPTIONAL SFTP Provider
|
|
3
16
|
*
|
|
@@ -13,21 +26,9 @@
|
|
|
13
26
|
* import { StorageProviderRegistry, FileLocationEnum } from '@flusys/nestjs-storage';
|
|
14
27
|
*
|
|
15
28
|
* StorageProviderRegistry.register(FileLocationEnum.SFTP, SftpProvider);
|
|
16
|
-
*/
|
|
17
|
-
|
|
18
|
-
Object.defineProperty(obj, key, {
|
|
19
|
-
value: value,
|
|
20
|
-
enumerable: true,
|
|
21
|
-
configurable: true,
|
|
22
|
-
writable: true
|
|
23
|
-
});
|
|
24
|
-
} else {
|
|
25
|
-
obj[key] = value;
|
|
26
|
-
}
|
|
27
|
-
return obj;
|
|
28
|
-
}
|
|
29
|
+
*/ import { InternalServerErrorException } from '@nestjs/common';
|
|
30
|
+
import { SYSTEM_MESSAGES } from '@flusys/nestjs-shared/constants';
|
|
29
31
|
import { ImageCompressor } from '../utils/image-compressor.util';
|
|
30
|
-
import { Logger } from '@nestjs/common';
|
|
31
32
|
import * as path from 'path';
|
|
32
33
|
import { v4 as uuidv4 } from 'uuid';
|
|
33
34
|
/**
|
|
@@ -54,10 +55,14 @@ import { v4 as uuidv4 } from 'uuid';
|
|
|
54
55
|
await this.connect();
|
|
55
56
|
// Ensure base directory exists
|
|
56
57
|
await this.ensureDirectory(this.config.basePath);
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
58
|
+
} catch {
|
|
59
|
+
throw new InternalServerErrorException({
|
|
60
|
+
message: 'SFTP client not found',
|
|
61
|
+
messageKey: SYSTEM_MESSAGES.SDK_NOT_INSTALLED,
|
|
62
|
+
messageParams: {
|
|
63
|
+
sdk: 'ssh2-sftp-client'
|
|
64
|
+
}
|
|
65
|
+
});
|
|
61
66
|
}
|
|
62
67
|
}
|
|
63
68
|
async connect() {
|
|
@@ -75,7 +80,7 @@ import { v4 as uuidv4 } from 'uuid';
|
|
|
75
80
|
async ensureDirectory(dirPath) {
|
|
76
81
|
try {
|
|
77
82
|
await this.sftp.mkdir(dirPath, true);
|
|
78
|
-
} catch
|
|
83
|
+
} catch {
|
|
79
84
|
// Directory might already exist
|
|
80
85
|
}
|
|
81
86
|
}
|
|
@@ -115,14 +120,12 @@ import { v4 as uuidv4 } from 'uuid';
|
|
|
115
120
|
await this.connect();
|
|
116
121
|
try {
|
|
117
122
|
await this.sftp.delete(key);
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
this.logger.warn(`Failed to delete file from SFTP: ${key}`);
|
|
123
|
+
} catch {
|
|
124
|
+
// Silent failure
|
|
121
125
|
}
|
|
122
126
|
}
|
|
123
127
|
async deleteMultipleFiles(keys) {
|
|
124
128
|
await Promise.all(keys.map((key)=>this.deleteFile(key)));
|
|
125
|
-
this.logger.log(`Deleted ${keys.length} files from SFTP`);
|
|
126
129
|
}
|
|
127
130
|
async generatePresignedUrl(key, _expiresInSeconds) {
|
|
128
131
|
// SFTP doesn't support presigned URLs, return the key
|
|
@@ -145,7 +148,6 @@ import { v4 as uuidv4 } from 'uuid';
|
|
|
145
148
|
}
|
|
146
149
|
}
|
|
147
150
|
constructor(){
|
|
148
|
-
_define_property(this, "logger", new Logger(SftpProvider.name));
|
|
149
151
|
_define_property(this, "sftp", void 0); // SftpClient
|
|
150
152
|
_define_property(this, "config", void 0);
|
|
151
153
|
_define_property(this, "connected", false);
|
|
@@ -25,8 +25,9 @@ function _ts_param(paramIndex, decorator) {
|
|
|
25
25
|
decorator(target, key, paramIndex);
|
|
26
26
|
};
|
|
27
27
|
}
|
|
28
|
-
import { Inject, Injectable,
|
|
28
|
+
import { Inject, Injectable, InternalServerErrorException, NotFoundException } from '@nestjs/common';
|
|
29
29
|
import * as crypto from 'crypto';
|
|
30
|
+
import { SYSTEM_MESSAGES } from '@flusys/nestjs-shared/constants';
|
|
30
31
|
import { StorageConfigService } from '../services';
|
|
31
32
|
import { StorageProviderRegistry } from './storage-provider.registry';
|
|
32
33
|
export class StorageFactoryService {
|
|
@@ -36,17 +37,30 @@ export class StorageFactoryService {
|
|
|
36
37
|
if (cached) return cached;
|
|
37
38
|
const ProviderClass = StorageProviderRegistry.get(config.provider);
|
|
38
39
|
if (!ProviderClass) {
|
|
39
|
-
throw new NotFoundException(
|
|
40
|
+
throw new NotFoundException({
|
|
41
|
+
message: `Storage provider '${config.provider}' not registered. Available: ${StorageProviderRegistry.getAll().join(', ')}`,
|
|
42
|
+
messageKey: SYSTEM_MESSAGES.SERVICE_NOT_AVAILABLE,
|
|
43
|
+
messageParams: {
|
|
44
|
+
provider: config.provider,
|
|
45
|
+
available: StorageProviderRegistry.getAll().join(', ')
|
|
46
|
+
}
|
|
47
|
+
});
|
|
40
48
|
}
|
|
41
49
|
try {
|
|
42
50
|
const instance = new ProviderClass();
|
|
43
51
|
await this.initializeProvider(instance, config);
|
|
44
52
|
this.cache.set(cacheKey, instance);
|
|
45
|
-
this.logger.log(`Created provider: ${config.provider} (${cacheKey})`);
|
|
46
53
|
return instance;
|
|
47
54
|
} catch (error) {
|
|
48
|
-
const
|
|
49
|
-
throw new
|
|
55
|
+
const errorMessage = error instanceof Error ? error.message : 'Unknown error';
|
|
56
|
+
throw new InternalServerErrorException({
|
|
57
|
+
message: `Failed to initialize '${config.provider}': ${errorMessage}`,
|
|
58
|
+
messageKey: SYSTEM_MESSAGES.INTERNAL_ERROR,
|
|
59
|
+
messageParams: {
|
|
60
|
+
provider: config.provider,
|
|
61
|
+
error: errorMessage
|
|
62
|
+
}
|
|
63
|
+
});
|
|
50
64
|
}
|
|
51
65
|
}
|
|
52
66
|
async getProvider(providerName) {
|
|
@@ -66,11 +80,9 @@ export class StorageFactoryService {
|
|
|
66
80
|
this.cache.clear();
|
|
67
81
|
}
|
|
68
82
|
async onModuleDestroy() {
|
|
69
|
-
this.
|
|
70
|
-
const closePromises = Array.from(this.cache.entries()).filter(([, p])=>'close' in p && typeof p.close === 'function').map(([key, p])=>p.close().then(()=>this.logger.debug(`Closed: ${key}`)).catch((e)=>this.logger.warn(`Failed to close ${key}: ${e.message}`)));
|
|
83
|
+
const closePromises = Array.from(this.cache.entries()).filter(([, p])=>'close' in p && typeof p.close === 'function').map(([, p])=>p.close().catch(()=>{}));
|
|
71
84
|
await Promise.allSettled(closePromises);
|
|
72
85
|
this.cache.clear();
|
|
73
|
-
this.logger.log('Storage provider cleanup complete');
|
|
74
86
|
}
|
|
75
87
|
// ─── Private Helpers ──────────────────────────────────────────────────────────
|
|
76
88
|
generateCacheKey(config) {
|
|
@@ -91,17 +103,14 @@ export class StorageFactoryService {
|
|
|
91
103
|
...config.config,
|
|
92
104
|
baseUrl: appUrl
|
|
93
105
|
};
|
|
94
|
-
this.logger.debug(`Using appUrl as baseUrl: ${appUrl}`);
|
|
95
106
|
}
|
|
96
107
|
}
|
|
97
108
|
await instance.initialize(initConfig);
|
|
98
109
|
}
|
|
99
110
|
constructor(configService){
|
|
100
111
|
_define_property(this, "configService", void 0);
|
|
101
|
-
_define_property(this, "logger", void 0);
|
|
102
112
|
_define_property(this, "cache", void 0);
|
|
103
113
|
this.configService = configService;
|
|
104
|
-
this.logger = new Logger(StorageFactoryService.name);
|
|
105
114
|
this.cache = new Map();
|
|
106
115
|
}
|
|
107
116
|
}
|
|
@@ -27,7 +27,8 @@ function _ts_param(paramIndex, decorator) {
|
|
|
27
27
|
}
|
|
28
28
|
import { HybridCache, RequestScopedApiService } from '@flusys/nestjs-shared/classes';
|
|
29
29
|
import { UtilsService } from '@flusys/nestjs-shared/modules';
|
|
30
|
-
import { applyCompanyFilter
|
|
30
|
+
import { applyCompanyFilter } from '@flusys/nestjs-shared/utils';
|
|
31
|
+
import { FILE_MESSAGES, FOLDER_MESSAGES, UPLOAD_MESSAGES } from '../config';
|
|
31
32
|
import { BadRequestException, Inject, Injectable, NotFoundException, Scope } from '@nestjs/common';
|
|
32
33
|
import { Brackets, In } from 'typeorm';
|
|
33
34
|
import { StorageConfigService } from './storage-config.service';
|
|
@@ -67,7 +68,10 @@ export class FileManagerService extends RequestScopedApiService {
|
|
|
67
68
|
id: dto.id
|
|
68
69
|
}
|
|
69
70
|
});
|
|
70
|
-
if (!existing) throw new NotFoundException(
|
|
71
|
+
if (!existing) throw new NotFoundException({
|
|
72
|
+
message: 'Entity not found for update',
|
|
73
|
+
messageKey: FILE_MESSAGES.NOT_FOUND
|
|
74
|
+
});
|
|
71
75
|
entity = existing;
|
|
72
76
|
}
|
|
73
77
|
const validatedFolder = dto.folderId ? await this.validateFolder(dto.folderId, user) : null;
|
|
@@ -97,7 +101,7 @@ export class FileManagerService extends RequestScopedApiService {
|
|
|
97
101
|
}
|
|
98
102
|
async getFilterQuery(query, filter, _user) {
|
|
99
103
|
for (const [key, value] of Object.entries(filter)){
|
|
100
|
-
if (key === 'contentType') {
|
|
104
|
+
if (key === 'contentType' && typeof value === 'string') {
|
|
101
105
|
this.applyContentTypeFilter(query, value);
|
|
102
106
|
} else {
|
|
103
107
|
query.andWhere(`${this.entityName}.${key} = :value`, {
|
|
@@ -169,8 +173,7 @@ export class FileManagerService extends RequestScopedApiService {
|
|
|
169
173
|
...item,
|
|
170
174
|
providerName: item.storageConfigId ? nameMap.get(item.storageConfigId) : undefined
|
|
171
175
|
}));
|
|
172
|
-
} catch
|
|
173
|
-
this.logger.warn(`Failed to fetch provider names: ${ErrorHandler.getErrorMessage(error)}`);
|
|
176
|
+
} catch {
|
|
174
177
|
return items.map((item)=>({
|
|
175
178
|
...item,
|
|
176
179
|
providerName: undefined
|
|
@@ -180,7 +183,10 @@ export class FileManagerService extends RequestScopedApiService {
|
|
|
180
183
|
async getFiles(dtos, protocol, host, user) {
|
|
181
184
|
await this.ensureRepositoryInitialized();
|
|
182
185
|
const ids = dtos.map((d)=>d.id).filter(Boolean);
|
|
183
|
-
if (!ids.length) throw new BadRequestException(
|
|
186
|
+
if (!ids.length) throw new BadRequestException({
|
|
187
|
+
message: 'No valid file IDs provided',
|
|
188
|
+
messageKey: UPLOAD_MESSAGES.NO_FILES_PROVIDED
|
|
189
|
+
});
|
|
184
190
|
const files = await this.repository.findBy({
|
|
185
191
|
id: In(ids)
|
|
186
192
|
});
|
|
@@ -194,17 +200,13 @@ export class FileManagerService extends RequestScopedApiService {
|
|
|
194
200
|
if (updatedFiles.length) {
|
|
195
201
|
try {
|
|
196
202
|
await this.repository.save(updatedFiles);
|
|
197
|
-
} catch
|
|
198
|
-
|
|
203
|
+
} catch {
|
|
204
|
+
// Silent failure - URL update is non-critical
|
|
199
205
|
}
|
|
200
206
|
}
|
|
201
207
|
return responses;
|
|
202
208
|
}
|
|
203
209
|
// ─── 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
210
|
buildWhereWithCompany(id, user) {
|
|
209
211
|
const where = {
|
|
210
212
|
id
|
|
@@ -214,6 +216,10 @@ export class FileManagerService extends RequestScopedApiService {
|
|
|
214
216
|
}
|
|
215
217
|
return where;
|
|
216
218
|
}
|
|
219
|
+
async getStorageConfigRepository() {
|
|
220
|
+
const entity = this.storageConfig.isCompanyFeatureEnabled() ? (await import('../entities')).StorageConfigWithCompany : (await import('../entities')).StorageConfig;
|
|
221
|
+
return this.dataSourceProvider.getRepository(entity);
|
|
222
|
+
}
|
|
217
223
|
async getStorageConfigBasePath(configId) {
|
|
218
224
|
try {
|
|
219
225
|
const repo = await this.getStorageConfigRepository();
|
|
@@ -227,8 +233,7 @@ export class FileManagerService extends RequestScopedApiService {
|
|
|
227
233
|
]
|
|
228
234
|
});
|
|
229
235
|
return config?.config?.basePath?.replace(/^\.\//, '') ?? null;
|
|
230
|
-
} catch
|
|
231
|
-
this.logger.warn(`Failed to get basePath for ${configId}: ${ErrorHandler.getErrorMessage(error)}`);
|
|
236
|
+
} catch {
|
|
232
237
|
return null;
|
|
233
238
|
}
|
|
234
239
|
}
|
|
@@ -239,7 +244,13 @@ export class FileManagerService extends RequestScopedApiService {
|
|
|
239
244
|
where: this.buildWhereWithCompany(folderId, user)
|
|
240
245
|
});
|
|
241
246
|
if (!folder) {
|
|
242
|
-
throw new BadRequestException(
|
|
247
|
+
throw new BadRequestException({
|
|
248
|
+
message: `Folder "${folderId}" not found or access denied`,
|
|
249
|
+
messageKey: FOLDER_MESSAGES.NOT_FOUND,
|
|
250
|
+
messageParams: {
|
|
251
|
+
folderId
|
|
252
|
+
}
|
|
253
|
+
});
|
|
243
254
|
}
|
|
244
255
|
return folder;
|
|
245
256
|
}
|
|
@@ -252,8 +263,7 @@ export class FileManagerService extends RequestScopedApiService {
|
|
|
252
263
|
where: this.buildWhereWithCompany(configId, user)
|
|
253
264
|
});
|
|
254
265
|
return config?.storage || dto.location || FileLocationEnum.LOCAL;
|
|
255
|
-
} catch
|
|
256
|
-
this.logger.warn(`Failed to resolve storage location: ${ErrorHandler.getErrorMessage(error)}`);
|
|
266
|
+
} catch {
|
|
257
267
|
return dto.location || FileLocationEnum.LOCAL;
|
|
258
268
|
}
|
|
259
269
|
}
|
|
@@ -299,12 +309,10 @@ export class FileManagerService extends RequestScopedApiService {
|
|
|
299
309
|
return grouped;
|
|
300
310
|
}
|
|
301
311
|
getFileBaseUrl(protocol, host) {
|
|
302
|
-
|
|
312
|
+
const appUrl = this.storageConfig.getAppUrl()?.replace(/\/$/, '');
|
|
313
|
+
return appUrl || `${protocol}://${host}`;
|
|
303
314
|
}
|
|
304
315
|
async refreshFileUrl(file, protocol, host, now, user) {
|
|
305
|
-
if (!file.storageConfigId) {
|
|
306
|
-
this.logger.warn(`File ${file.id} has no storageConfigId`);
|
|
307
|
-
}
|
|
308
316
|
const isCloudProvider = file.location === FileLocationEnum.AWS || file.location === FileLocationEnum.AZURE;
|
|
309
317
|
const needsNewUrl = !file.url || isCloudProvider && (typeof file.expiresAt !== 'number' || now >= file.expiresAt);
|
|
310
318
|
if (needsNewUrl && file.storageConfigId) {
|
|
@@ -312,8 +320,7 @@ export class FileManagerService extends RequestScopedApiService {
|
|
|
312
320
|
file.url = await this.uploadService.makeFileUrl(file.key, file.storageConfigId, URL_EXPIRY_SECONDS, user);
|
|
313
321
|
file.expiresAt = now + URL_EXPIRY_SECONDS * 1000;
|
|
314
322
|
return true;
|
|
315
|
-
} catch
|
|
316
|
-
this.logger.error(`Failed to generate URL for ${file.id}: ${ErrorHandler.getErrorMessage(error)}`);
|
|
323
|
+
} catch {
|
|
317
324
|
file.url = `${this.getFileBaseUrl(protocol, host)}/storage/upload/file/${file.key}`;
|
|
318
325
|
}
|
|
319
326
|
}
|
|
@@ -342,7 +349,7 @@ export class FileManagerService extends RequestScopedApiService {
|
|
|
342
349
|
};
|
|
343
350
|
}
|
|
344
351
|
constructor(cacheManager, utilsService, uploadService, storageConfig, dataSourceProvider){
|
|
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;
|
|
352
|
+
super('file_manager', null, cacheManager, utilsService, FileManagerService.name, true, 'storage'), _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;
|
|
346
353
|
}
|
|
347
354
|
}
|
|
348
355
|
FileManagerService = _ts_decorate([
|
|
@@ -39,6 +39,7 @@ export class FolderService extends RequestScopedApiService {
|
|
|
39
39
|
getDataSourceProvider() {
|
|
40
40
|
return this.dataSourceProvider;
|
|
41
41
|
}
|
|
42
|
+
// ─── Override Methods ───────────────────────────────────────────────────────
|
|
42
43
|
async convertSingleDtoToEntity(dto, user) {
|
|
43
44
|
const entity = await super.convertSingleDtoToEntity(dto, user);
|
|
44
45
|
if (this.storageConfig.isCompanyFeatureEnabled()) {
|
|
@@ -74,7 +75,7 @@ export class FolderService extends RequestScopedApiService {
|
|
|
74
75
|
return result;
|
|
75
76
|
}
|
|
76
77
|
constructor(cacheManager, utilsService, storageConfig, dataSourceProvider){
|
|
77
|
-
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;
|
|
78
|
+
super('folder', null, cacheManager, utilsService, FolderService.name, true, 'storage'), _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;
|
|
78
79
|
}
|
|
79
80
|
}
|
|
80
81
|
FolderService = _ts_decorate([
|
|
@@ -26,7 +26,8 @@ function _ts_param(paramIndex, decorator) {
|
|
|
26
26
|
};
|
|
27
27
|
}
|
|
28
28
|
import { MultiTenantDataSourceService } from '@flusys/nestjs-shared/modules';
|
|
29
|
-
import { Inject, Injectable,
|
|
29
|
+
import { Inject, Injectable, InternalServerErrorException, Optional, Scope } from '@nestjs/common';
|
|
30
|
+
import { SYSTEM_MESSAGES } from '@flusys/nestjs-shared/constants';
|
|
30
31
|
import { REQUEST } from '@nestjs/core';
|
|
31
32
|
import { Request } from 'express';
|
|
32
33
|
import { StorageConfigService } from './storage-config.service';
|
|
@@ -90,7 +91,10 @@ export class StorageDataSourceProvider extends MultiTenantDataSourceService {
|
|
|
90
91
|
}
|
|
91
92
|
const config = this.getDefaultDatabaseConfig();
|
|
92
93
|
if (!config) {
|
|
93
|
-
throw new
|
|
94
|
+
throw new InternalServerErrorException({
|
|
95
|
+
message: 'No database config available. Provide defaultDatabaseConfig or tenantDefaultDatabaseConfig.',
|
|
96
|
+
messageKey: SYSTEM_MESSAGES.DATABASE_CONFIG_NOT_AVAILABLE
|
|
97
|
+
});
|
|
94
98
|
}
|
|
95
99
|
// Create connection with lock to prevent race conditions
|
|
96
100
|
const connectionPromise = this.createDataSourceFromConfig(config);
|
|
@@ -129,7 +133,7 @@ export class StorageDataSourceProvider extends MultiTenantDataSourceService {
|
|
|
129
133
|
}
|
|
130
134
|
}
|
|
131
135
|
constructor(configService, request){
|
|
132
|
-
super(StorageDataSourceProvider.buildParentOptions(configService.getOptions()), request), _define_property(this, "configService", void 0),
|
|
136
|
+
super(StorageDataSourceProvider.buildParentOptions(configService.getOptions()), request), _define_property(this, "configService", void 0), this.configService = configService;
|
|
133
137
|
}
|
|
134
138
|
}
|
|
135
139
|
// Override parent's static properties to have Storage-specific cache
|
|
@@ -39,6 +39,7 @@ export class StorageProviderConfigService extends RequestScopedApiService {
|
|
|
39
39
|
getDataSourceProvider() {
|
|
40
40
|
return this.dataSourceProvider;
|
|
41
41
|
}
|
|
42
|
+
// ─── Override Methods ───────────────────────────────────────────────────────
|
|
42
43
|
async convertSingleDtoToEntity(dto, user) {
|
|
43
44
|
const entity = await super.convertSingleDtoToEntity(dto, user);
|
|
44
45
|
if (this.storageConfig.isCompanyFeatureEnabled()) {
|
|
@@ -120,7 +121,7 @@ export class StorageProviderConfigService extends RequestScopedApiService {
|
|
|
120
121
|
});
|
|
121
122
|
}
|
|
122
123
|
constructor(cacheManager, utilsService, storageConfig, dataSourceProvider){
|
|
123
|
-
super('storageConfig', null, cacheManager, utilsService, StorageProviderConfigService.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;
|
|
124
|
+
super('storageConfig', null, cacheManager, utilsService, StorageProviderConfigService.name, true, 'storage'), _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;
|
|
124
125
|
}
|
|
125
126
|
}
|
|
126
127
|
StorageProviderConfigService = _ts_decorate([
|