@nest-omni/core 4.1.3-12 → 4.1.3-14
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/cache/dependencies/db.dependency.d.ts +55 -6
- package/cache/dependencies/db.dependency.js +64 -13
- package/common/boilerplate.polyfill.js +1 -1
- package/file-upload/decorators/column.decorator.d.ts +151 -0
- package/file-upload/decorators/column.decorator.js +273 -0
- package/file-upload/decorators/csv-data.decorator.d.ts +17 -31
- package/file-upload/decorators/csv-data.decorator.js +45 -91
- package/file-upload/decorators/csv-import.decorator.d.ts +34 -0
- package/file-upload/decorators/csv-import.decorator.js +24 -0
- package/file-upload/decorators/examples/column-mapping.example.d.ts +76 -0
- package/file-upload/decorators/examples/column-mapping.example.js +122 -0
- package/file-upload/decorators/excel-data.decorator.d.ts +15 -29
- package/file-upload/decorators/excel-data.decorator.js +42 -82
- package/file-upload/decorators/index.d.ts +3 -2
- package/file-upload/decorators/index.js +20 -2
- package/file-upload/decorators/validate-data.decorator.d.ts +91 -0
- package/file-upload/decorators/validate-data.decorator.js +39 -0
- package/file-upload/dto/update-file.dto.d.ts +0 -1
- package/file-upload/dto/update-file.dto.js +0 -4
- package/file-upload/entities/file-metadata.entity.d.ts +6 -3
- package/file-upload/entities/file-metadata.entity.js +2 -10
- package/file-upload/entities/file.entity.d.ts +3 -18
- package/file-upload/entities/file.entity.js +0 -34
- package/file-upload/file-upload.module.d.ts +1 -1
- package/file-upload/file-upload.module.js +44 -16
- package/file-upload/index.d.ts +13 -2
- package/file-upload/index.js +21 -3
- package/file-upload/interceptors/file-upload.interceptor.d.ts +61 -8
- package/file-upload/interceptors/file-upload.interceptor.js +417 -257
- package/file-upload/interfaces/file-processor.interface.d.ts +93 -0
- package/file-upload/interfaces/file-processor.interface.js +2 -0
- package/file-upload/interfaces/file-upload-options.interface.d.ts +3 -46
- package/file-upload/interfaces/file-upload-options.interface.js +3 -0
- package/file-upload/interfaces/processor-options.interface.d.ts +102 -0
- package/file-upload/interfaces/processor-options.interface.js +2 -0
- package/file-upload/processors/csv.processor.d.ts +98 -0
- package/file-upload/processors/csv.processor.js +391 -0
- package/file-upload/processors/excel.processor.d.ts +130 -0
- package/file-upload/processors/excel.processor.js +547 -0
- package/file-upload/processors/image.processor.d.ts +199 -0
- package/file-upload/processors/image.processor.js +377 -0
- package/file-upload/services/file.service.d.ts +3 -0
- package/file-upload/services/file.service.js +39 -10
- package/file-upload/services/malicious-file-detector.service.d.ts +29 -3
- package/file-upload/services/malicious-file-detector.service.js +256 -57
- package/file-upload/utils/dynamic-import.util.d.ts +6 -2
- package/file-upload/utils/dynamic-import.util.js +17 -5
- package/http-client/decorators/http-client.decorators.d.ts +4 -2
- package/http-client/decorators/http-client.decorators.js +2 -1
- package/http-client/entities/http-log.entity.js +1 -9
- package/http-client/examples/proxy-from-environment.example.d.ts +133 -0
- package/http-client/examples/proxy-from-environment.example.js +410 -0
- package/http-client/http-client.module.js +65 -6
- package/http-client/interfaces/http-client-config.interface.d.ts +6 -0
- package/http-client/services/http-client.service.d.ts +8 -0
- package/http-client/services/http-client.service.js +61 -17
- package/http-client/services/logging.service.d.ts +1 -1
- package/http-client/services/logging.service.js +74 -58
- package/http-client/utils/index.d.ts +1 -0
- package/http-client/utils/index.js +1 -0
- package/http-client/utils/proxy-environment.util.d.ts +42 -0
- package/http-client/utils/proxy-environment.util.js +148 -0
- package/package.json +9 -5
- package/shared/service-registry.module.js +18 -0
- package/transaction/data-source.util.d.ts +142 -0
- package/transaction/data-source.util.js +330 -0
- package/transaction/index.d.ts +1 -0
- package/transaction/index.js +12 -1
- package/validators/is-exists.validator.d.ts +19 -2
- package/validators/is-exists.validator.js +27 -2
- package/validators/is-unique.validator.d.ts +12 -1
- package/validators/is-unique.validator.js +26 -1
|
@@ -28,6 +28,7 @@ const core_1 = require("@nestjs/core");
|
|
|
28
28
|
const rxjs_1 = require("rxjs");
|
|
29
29
|
const operators_1 = require("rxjs/operators");
|
|
30
30
|
const file_upload_decorator_1 = require("../decorators/file-upload.decorator");
|
|
31
|
+
const file_entity_interface_1 = require("../interfaces/file-entity.interface");
|
|
31
32
|
const file_service_1 = require("../services/file.service");
|
|
32
33
|
const file_signature_validator_service_1 = require("../services/file-signature-validator.service");
|
|
33
34
|
const malicious_file_detector_service_1 = require("../services/malicious-file-detector.service");
|
|
@@ -37,7 +38,7 @@ const file_upload_exception_1 = require("../exceptions/file-upload.exception");
|
|
|
37
38
|
const fs = require("fs-extra");
|
|
38
39
|
const crypto = require("crypto");
|
|
39
40
|
/**
|
|
40
|
-
*
|
|
41
|
+
* 文件上传拦截器(优化版)
|
|
41
42
|
* 负责创建文件记录、执行钩子、处理器等后置逻辑
|
|
42
43
|
*/
|
|
43
44
|
let FileUploadInterceptor = FileUploadInterceptor_1 = class FileUploadInterceptor {
|
|
@@ -53,187 +54,186 @@ let FileUploadInterceptor = FileUploadInterceptor_1 = class FileUploadIntercepto
|
|
|
53
54
|
}
|
|
54
55
|
intercept(context, next) {
|
|
55
56
|
return __awaiter(this, void 0, void 0, function* () {
|
|
56
|
-
var _a, _b, _c, _d, _e, _f, _g, _h, _j;
|
|
57
57
|
const options = this.reflector.get(file_upload_decorator_1.FILE_UPLOAD_OPTIONS, context.getHandler());
|
|
58
|
-
this.logger.debug(`FileUploadInterceptor: Received options: ${JSON.stringify(options)}`);
|
|
59
58
|
if (!options) {
|
|
60
|
-
this.logger.debug('FileUploadInterceptor: No options provided, continuing');
|
|
61
59
|
return next.handle();
|
|
62
60
|
}
|
|
63
61
|
const request = context.switchToHttp().getRequest();
|
|
64
62
|
const file = request.file;
|
|
65
|
-
this.logger.debug(`FileUploadInterceptor: File info: ${JSON.stringify({
|
|
66
|
-
originalname: file === null || file === void 0 ? void 0 : file.originalname,
|
|
67
|
-
mimetype: file === null || file === void 0 ? void 0 : file.mimetype,
|
|
68
|
-
size: file === null || file === void 0 ? void 0 : file.size,
|
|
69
|
-
path: file === null || file === void 0 ? void 0 : file.path,
|
|
70
|
-
})}`);
|
|
71
63
|
if (!file) {
|
|
72
|
-
this.logger.debug('FileUploadInterceptor: No file in request, continuing');
|
|
73
64
|
return next.handle();
|
|
74
65
|
}
|
|
75
|
-
//
|
|
76
|
-
|
|
66
|
+
// 快速路径:如果没有配置验证选项,直接跳过验证
|
|
67
|
+
if (this.shouldSkipValidation(options, request)) {
|
|
68
|
+
return this.processFile(file, request, options, next);
|
|
69
|
+
}
|
|
70
|
+
// 执行验证
|
|
71
|
+
yield this.validateFile(file, options, request);
|
|
72
|
+
// 处理文件
|
|
73
|
+
return this.processFile(file, request, options, next);
|
|
74
|
+
});
|
|
75
|
+
}
|
|
76
|
+
/**
|
|
77
|
+
* 判断是否应该跳过验证
|
|
78
|
+
*/
|
|
79
|
+
shouldSkipValidation(options, request) {
|
|
80
|
+
var _a, _b;
|
|
81
|
+
return (!options.maxSize &&
|
|
82
|
+
!((_a = options.types) === null || _a === void 0 ? void 0 : _a.length) &&
|
|
83
|
+
!((_b = options.exts) === null || _b === void 0 ? void 0 : _b.length) &&
|
|
84
|
+
!options.auth &&
|
|
85
|
+
options.validateSignature === false &&
|
|
86
|
+
options.maliciousCheck === false);
|
|
87
|
+
}
|
|
88
|
+
/**
|
|
89
|
+
* 验证文件
|
|
90
|
+
*/
|
|
91
|
+
validateFile(file, options, request) {
|
|
92
|
+
return __awaiter(this, void 0, void 0, function* () {
|
|
93
|
+
// 1. 验证文件大小
|
|
77
94
|
if (options.maxSize && file.size > options.maxSize) {
|
|
78
|
-
this.logger.debug(`FileUploadInterceptor: File size exceeded: ${file.size} > ${options.maxSize}`);
|
|
79
95
|
throw new file_upload_exception_1.FileSizeExceededException(file.size, options.maxSize);
|
|
80
96
|
}
|
|
81
|
-
// 2.
|
|
82
|
-
if (options.types && options.types.length > 0) {
|
|
83
|
-
const allowedTypes = this.mimeRegistry.resolveFileTypes(options.types);
|
|
84
|
-
this.logger.debug(`FileUploadInterceptor: Allowed types: ${JSON.stringify(allowedTypes)}`);
|
|
85
|
-
// 验证 MIME 类型
|
|
86
|
-
if (!this.validateMimeType(file.mimetype, allowedTypes.mimes)) {
|
|
87
|
-
this.logger.debug(`FileUploadInterceptor: File type not allowed: ${file.mimetype}`);
|
|
88
|
-
throw new file_upload_exception_1.FileTypeNotAllowedException(file.mimetype, allowedTypes.mimes);
|
|
89
|
-
}
|
|
90
|
-
// 验证扩展名
|
|
91
|
-
if (allowedTypes.exts.length > 0) {
|
|
92
|
-
const ext = utils_1.FileNameUtil.extractExtension(file.originalname);
|
|
93
|
-
if (!this.validateExtension(ext, allowedTypes.exts)) {
|
|
94
|
-
this.logger.debug(`FileUploadInterceptor: File extension not allowed: ${ext}`);
|
|
95
|
-
throw new file_upload_exception_1.FileExtensionNotAllowedException(ext, allowedTypes.exts);
|
|
96
|
-
}
|
|
97
|
-
}
|
|
98
|
-
}
|
|
99
|
-
// 3. 验证自定义扩展名
|
|
100
|
-
if (options.exts && options.exts.length > 0) {
|
|
101
|
-
const ext = utils_1.FileNameUtil.extractExtension(file.originalname);
|
|
102
|
-
if (!this.validateExtension(ext, options.exts)) {
|
|
103
|
-
this.logger.debug(`FileUploadInterceptor: Custom extension not allowed: ${ext}`);
|
|
104
|
-
throw new file_upload_exception_1.FileExtensionNotAllowedException(ext, options.exts);
|
|
105
|
-
}
|
|
106
|
-
}
|
|
107
|
-
// 4. 验证访问权限
|
|
97
|
+
// 2. 验证权限
|
|
108
98
|
if (options.auth && !request.user) {
|
|
109
|
-
this.logger.debug('FileUploadInterceptor: Authentication required but not provided');
|
|
110
99
|
throw new common_1.UnauthorizedException('Authentication required for file upload');
|
|
111
100
|
}
|
|
112
|
-
//
|
|
101
|
+
// 3. 验证文件名
|
|
113
102
|
if (options.sanitize !== false) {
|
|
114
103
|
const sanitized = utils_1.FileNameUtil.sanitize(file.originalname);
|
|
115
104
|
if (!sanitized || sanitized.length === 0) {
|
|
116
|
-
this.logger.debug(`FileUploadInterceptor: Invalid filename after sanitization: ${file.originalname}`);
|
|
117
105
|
throw new file_upload_exception_1.InvalidFilenameException(file.originalname);
|
|
118
106
|
}
|
|
119
|
-
// 注意:这里不修改 file.originalname,保持原始文件名用于后续处理
|
|
120
|
-
this.logger.debug(`FileUploadInterceptor: Filename sanitized to: ${sanitized}`);
|
|
121
107
|
}
|
|
122
|
-
//
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
108
|
+
// 4. 批量验证文件类型和扩展名
|
|
109
|
+
yield this.validateFileTypeAndExtension(file, options);
|
|
110
|
+
// 5. 文件签名验证(可选)
|
|
111
|
+
if (this.shouldPerformSignatureValidation(options) && file.path) {
|
|
112
|
+
yield this.validateFileSignature(file, options);
|
|
113
|
+
}
|
|
114
|
+
// 6. 恶意文件检测(可选)
|
|
115
|
+
if (this.shouldPerformMaliciousCheck(options)) {
|
|
116
|
+
yield this.checkMaliciousFile(file, options);
|
|
117
|
+
}
|
|
118
|
+
});
|
|
119
|
+
}
|
|
120
|
+
/**
|
|
121
|
+
* 批量验证文件类型和扩展名
|
|
122
|
+
*/
|
|
123
|
+
validateFileTypeAndExtension(file, options) {
|
|
124
|
+
return __awaiter(this, void 0, void 0, function* () {
|
|
125
|
+
var _a, _b, _c;
|
|
126
|
+
if (!((_a = options.types) === null || _a === void 0 ? void 0 : _a.length) && !((_b = options.exts) === null || _b === void 0 ? void 0 : _b.length)) {
|
|
127
|
+
return;
|
|
128
|
+
}
|
|
129
|
+
const allowedTypes = ((_c = options.types) === null || _c === void 0 ? void 0 : _c.length)
|
|
130
|
+
? this.mimeRegistry.resolveFileTypes(options.types)
|
|
131
|
+
: { mimes: [], exts: options.exts || [] };
|
|
132
|
+
// 获取文件扩展名
|
|
133
|
+
const ext = utils_1.FileNameUtil.extractExtension(file.originalname);
|
|
134
|
+
// 同时验证 MIME 类型和扩展名
|
|
135
|
+
const mimeValid = !allowedTypes.mimes.length ||
|
|
136
|
+
allowedTypes.mimes.some((mime) => mime.endsWith('/*')
|
|
137
|
+
? file.mimetype.startsWith(mime.slice(0, -2))
|
|
138
|
+
: mime === file.mimetype);
|
|
139
|
+
const extValid = !allowedTypes.exts.length ||
|
|
140
|
+
allowedTypes.exts.some((allowedExt) => allowedExt.toLowerCase() === ext.toLowerCase());
|
|
141
|
+
if (!mimeValid || !extValid) {
|
|
142
|
+
if (!mimeValid) {
|
|
143
|
+
throw new file_upload_exception_1.FileTypeNotAllowedException(file.mimetype, allowedTypes.mimes);
|
|
144
|
+
}
|
|
145
|
+
if (!extValid) {
|
|
146
|
+
throw new file_upload_exception_1.FileExtensionNotAllowedException(ext, allowedTypes.exts);
|
|
154
147
|
}
|
|
155
148
|
}
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
149
|
+
});
|
|
150
|
+
}
|
|
151
|
+
/**
|
|
152
|
+
* 判断是否需要执行文件签名验证
|
|
153
|
+
*/
|
|
154
|
+
shouldPerformSignatureValidation(options) {
|
|
155
|
+
var _a, _b;
|
|
156
|
+
return (options.validateSignature !== false &&
|
|
157
|
+
((_a = this.moduleOptions) === null || _a === void 0 ? void 0 : _a.validateSignature) !== false &&
|
|
158
|
+
this.signatureValidator &&
|
|
159
|
+
((_b = options.types) === null || _b === void 0 ? void 0 : _b.length) > 0);
|
|
160
|
+
}
|
|
161
|
+
/**
|
|
162
|
+
* 验证文件签名
|
|
163
|
+
*/
|
|
164
|
+
validateFileSignature(file, options) {
|
|
165
|
+
return __awaiter(this, void 0, void 0, function* () {
|
|
166
|
+
var _a, _b, _c, _d;
|
|
167
|
+
try {
|
|
168
|
+
const allowedTypes = this.mimeRegistry.resolveFileTypes(options.types);
|
|
169
|
+
if (allowedTypes.signatureTypes.length === 0) {
|
|
170
|
+
return;
|
|
173
171
|
}
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
172
|
+
const signatureResult = yield this.signatureValidator.validateFileSignature(file.path, allowedTypes.signatureTypes, (_a = this.moduleOptions) === null || _a === void 0 ? void 0 : _a.signatureValidation);
|
|
173
|
+
if (!signatureResult.valid) {
|
|
174
|
+
const detectedType = (_b = signatureResult.detectedTypes[0]) === null || _b === void 0 ? void 0 : _b.type;
|
|
175
|
+
throw new file_upload_exception_1.FileSignatureMismatchException(signatureResult.errors.join(', '), file.mimetype, detectedType);
|
|
176
|
+
}
|
|
177
|
+
// 附加验证数据到请求
|
|
178
|
+
if (signatureResult) {
|
|
179
|
+
file._validationData = signatureResult;
|
|
180
180
|
}
|
|
181
181
|
}
|
|
182
|
-
|
|
183
|
-
|
|
182
|
+
catch (error) {
|
|
183
|
+
if (error instanceof file_upload_exception_1.FileSignatureMismatchException) {
|
|
184
|
+
throw error;
|
|
185
|
+
}
|
|
186
|
+
if ((_d = (_c = this.moduleOptions) === null || _c === void 0 ? void 0 : _c.signatureValidation) === null || _d === void 0 ? void 0 : _d.strict) {
|
|
187
|
+
throw error;
|
|
188
|
+
}
|
|
189
|
+
// 非严格模式下只记录警告
|
|
190
|
+
this.logger.warn(`Signature validation failed: ${error.message}`);
|
|
184
191
|
}
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
fileType: fileEntity.fileType,
|
|
213
|
-
extension: fileEntity.extension,
|
|
214
|
-
});
|
|
215
|
-
// 添加验证数据(如果有)
|
|
216
|
-
if (request.fileValidationData) {
|
|
217
|
-
fileEntity.addValidationData({
|
|
218
|
-
detectedTypes: request.fileValidationData.detectedTypes || [],
|
|
219
|
-
signatures: ((_j = request.fileValidationData.metadata) === null || _j === void 0 ? void 0 : _j.signatures) || [],
|
|
220
|
-
integrityChecks: [],
|
|
221
|
-
});
|
|
222
|
-
yield this.fileService.update(fileEntity.id, {
|
|
223
|
-
validationData: fileEntity.validationData,
|
|
224
|
-
});
|
|
225
|
-
}
|
|
226
|
-
// 附加到请求对象
|
|
227
|
-
request.fileEntity = fileEntity;
|
|
228
|
-
request.uploadId = uploadId;
|
|
229
|
-
this.logger.debug(`File entity created: ${fileEntity.id}`);
|
|
192
|
+
});
|
|
193
|
+
}
|
|
194
|
+
/**
|
|
195
|
+
* 判断是否需要执行恶意文件检测
|
|
196
|
+
*/
|
|
197
|
+
shouldPerformMaliciousCheck(options) {
|
|
198
|
+
var _a, _b;
|
|
199
|
+
return (options.maliciousCheck !== false &&
|
|
200
|
+
((_b = (_a = this.moduleOptions) === null || _a === void 0 ? void 0 : _a.maliciousDetection) === null || _b === void 0 ? void 0 : _b.enabled) !== false &&
|
|
201
|
+
!!this.maliciousDetector);
|
|
202
|
+
}
|
|
203
|
+
/**
|
|
204
|
+
* 检测恶意文件
|
|
205
|
+
*/
|
|
206
|
+
checkMaliciousFile(file, options) {
|
|
207
|
+
return __awaiter(this, void 0, void 0, function* () {
|
|
208
|
+
try {
|
|
209
|
+
// For in-memory files, use buffer; for disk files, use path
|
|
210
|
+
const maliciousCheck = file.path
|
|
211
|
+
? yield this.maliciousDetector.scanFile(file.path)
|
|
212
|
+
: yield this.maliciousDetector.scanFile(undefined, file.buffer);
|
|
213
|
+
if (!maliciousCheck.safe) {
|
|
214
|
+
throw new file_upload_exception_1.MaliciousFileException(maliciousCheck.threats);
|
|
215
|
+
}
|
|
216
|
+
// 附加检测数据到请求
|
|
217
|
+
if (maliciousCheck) {
|
|
218
|
+
file._maliciousCheck = maliciousCheck;
|
|
230
219
|
}
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
220
|
+
}
|
|
221
|
+
catch (error) {
|
|
222
|
+
if (error instanceof file_upload_exception_1.MaliciousFileException) {
|
|
223
|
+
throw error;
|
|
235
224
|
}
|
|
225
|
+
// 检测出错,记录日志但继续
|
|
226
|
+
this.logger.error(`Malicious detection error: ${error.message}`);
|
|
236
227
|
}
|
|
228
|
+
});
|
|
229
|
+
}
|
|
230
|
+
/**
|
|
231
|
+
* 处理文件
|
|
232
|
+
*/
|
|
233
|
+
processFile(file, request, options, next) {
|
|
234
|
+
return __awaiter(this, void 0, void 0, function* () {
|
|
235
|
+
// 创建文件实体记录(如果可用)
|
|
236
|
+
const { fileEntity, uploadId } = yield this.createFileEntity(file, request);
|
|
237
237
|
// 创建上传上下文
|
|
238
238
|
const uploadContext = this.createUploadContext(request, fileEntity);
|
|
239
239
|
try {
|
|
@@ -241,91 +241,36 @@ let FileUploadInterceptor = FileUploadInterceptor_1 = class FileUploadIntercepto
|
|
|
241
241
|
if (options.before) {
|
|
242
242
|
yield options.before(file, uploadContext);
|
|
243
243
|
}
|
|
244
|
-
//
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
: options.processor;
|
|
249
|
-
const processor = this.getProcessor(processorConfig.name);
|
|
250
|
-
if (processor) {
|
|
251
|
-
// 记录处理步骤开始
|
|
252
|
-
if (fileEntity) {
|
|
253
|
-
yield this.fileService.updateProcessingStatus(fileEntity.id, processorConfig.name, 'pending');
|
|
254
|
-
}
|
|
255
|
-
const result = yield processor.process(file, processorConfig.options);
|
|
256
|
-
request._processResult = result;
|
|
257
|
-
// 记录处理成功
|
|
258
|
-
if (fileEntity) {
|
|
259
|
-
yield this.fileService.updateProcessingStatus(fileEntity.id, processorConfig.name, 'completed', result);
|
|
260
|
-
}
|
|
261
|
-
}
|
|
262
|
-
}
|
|
263
|
-
// 执行参数装饰器指定的处理器
|
|
264
|
-
if (request._processParams) {
|
|
265
|
-
const { name, options: procOptions } = request._processParams;
|
|
266
|
-
const processor = this.getProcessor(name);
|
|
267
|
-
if (processor) {
|
|
268
|
-
const result = yield processor.process(file, procOptions);
|
|
269
|
-
request._processResult = result;
|
|
270
|
-
}
|
|
244
|
+
// 执行处理器
|
|
245
|
+
const processResult = yield this.executeProcessors(file, request, options);
|
|
246
|
+
if (processResult) {
|
|
247
|
+
request._processResult = processResult;
|
|
271
248
|
}
|
|
249
|
+
// 如果需要,在处理后删除原文件
|
|
250
|
+
const shouldDeleteFile = this.shouldDeleteAfterProcess(options, processResult);
|
|
272
251
|
return next.handle().pipe((0, operators_1.tap)((response) => __awaiter(this, void 0, void 0, function* () {
|
|
273
252
|
// 执行 after 钩子
|
|
274
253
|
if (options.after) {
|
|
275
254
|
yield options.after(file, uploadContext);
|
|
276
255
|
}
|
|
277
|
-
//
|
|
256
|
+
// 更新文件实体状态
|
|
278
257
|
if (fileEntity && this.fileService) {
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
path: relativePath || (response === null || response === void 0 ? void 0 : response.path),
|
|
285
|
-
status: 'completed',
|
|
286
|
-
});
|
|
287
|
-
// 将fileId和uploadId添加到响应中
|
|
288
|
-
if (typeof response === 'object' && response !== null) {
|
|
289
|
-
response.fileId = fileEntity.id;
|
|
290
|
-
response.uploadId = uploadId;
|
|
291
|
-
}
|
|
292
|
-
this.logger.debug(`File upload completed: ${fileEntity.id}`);
|
|
293
|
-
}
|
|
294
|
-
catch (error) {
|
|
295
|
-
this.logger.error(`Error updating file record: ${error.message}`);
|
|
296
|
-
}
|
|
258
|
+
yield this.updateFileEntityOnSuccess(fileEntity, uploadId, file, response);
|
|
259
|
+
}
|
|
260
|
+
// 处理完成后删除原文件
|
|
261
|
+
if (shouldDeleteFile && file.path) {
|
|
262
|
+
yield this.deleteOriginalFile(file, fileEntity);
|
|
297
263
|
}
|
|
298
264
|
})), (0, operators_1.catchError)((error) => __awaiter(this, void 0, void 0, function* () {
|
|
299
|
-
//
|
|
265
|
+
// 清理资源
|
|
300
266
|
if (fileEntity && this.fileService) {
|
|
301
|
-
|
|
302
|
-
yield this.fileService.update(fileEntity.id, {
|
|
303
|
-
status: 'failed',
|
|
304
|
-
errorMessage: error.message || 'Unknown error',
|
|
305
|
-
});
|
|
306
|
-
// 清理临时文件(避免TOCTOU竞态条件)
|
|
307
|
-
if (file.path) {
|
|
308
|
-
try {
|
|
309
|
-
yield fs.unlink(file.path);
|
|
310
|
-
}
|
|
311
|
-
catch (unlinkError) {
|
|
312
|
-
// 文件可能已被删除,忽略ENOENT错误
|
|
313
|
-
if (unlinkError.code !== 'ENOENT') {
|
|
314
|
-
this.logger.error(`Failed to cleanup file: ${unlinkError.message}`);
|
|
315
|
-
}
|
|
316
|
-
}
|
|
317
|
-
}
|
|
318
|
-
this.logger.warn(`File upload failed: ${fileEntity.id}`);
|
|
319
|
-
}
|
|
320
|
-
catch (cleanupError) {
|
|
321
|
-
this.logger.error(`Error during cleanup: ${cleanupError.message}`);
|
|
322
|
-
}
|
|
267
|
+
yield this.cleanupOnFailure(fileEntity, file, error);
|
|
323
268
|
}
|
|
324
269
|
return (0, rxjs_1.throwError)(() => error);
|
|
325
270
|
})));
|
|
326
271
|
}
|
|
327
272
|
catch (error) {
|
|
328
|
-
//
|
|
273
|
+
// 处理失败,更新状态
|
|
329
274
|
if (fileEntity && this.fileService) {
|
|
330
275
|
yield this.fileService.update(fileEntity.id, {
|
|
331
276
|
status: 'failed',
|
|
@@ -336,16 +281,182 @@ let FileUploadInterceptor = FileUploadInterceptor_1 = class FileUploadIntercepto
|
|
|
336
281
|
}
|
|
337
282
|
});
|
|
338
283
|
}
|
|
284
|
+
/**
|
|
285
|
+
* 创建文件实体
|
|
286
|
+
*/
|
|
287
|
+
createFileEntity(file, request) {
|
|
288
|
+
return __awaiter(this, void 0, void 0, function* () {
|
|
289
|
+
var _a, _b;
|
|
290
|
+
if (!this.fileService) {
|
|
291
|
+
return { fileEntity: null, uploadId: null };
|
|
292
|
+
}
|
|
293
|
+
try {
|
|
294
|
+
const uploadId = request.body.uploadId || this.generateUploadId();
|
|
295
|
+
const checksum = file.path
|
|
296
|
+
? yield utils_1.ChecksumUtil.calculate(file.path, 'md5')
|
|
297
|
+
: undefined;
|
|
298
|
+
const originalName = utils_1.FileNameUtil.decodeAndNormalize(file.originalname);
|
|
299
|
+
const fileEntity = yield this.fileService.create({
|
|
300
|
+
originalName,
|
|
301
|
+
filename: file.filename,
|
|
302
|
+
mimeType: file.mimetype,
|
|
303
|
+
size: file.size,
|
|
304
|
+
tempPath: file.path,
|
|
305
|
+
storageProvider: 'local',
|
|
306
|
+
userId: (_a = request.user) === null || _a === void 0 ? void 0 : _a.id,
|
|
307
|
+
uploadId,
|
|
308
|
+
status: 'processing',
|
|
309
|
+
checksum,
|
|
310
|
+
});
|
|
311
|
+
// 自动检测并设置文件类型
|
|
312
|
+
fileEntity.detectAndSetFileType();
|
|
313
|
+
yield this.fileService.update(fileEntity.id, {
|
|
314
|
+
fileType: fileEntity.fileType,
|
|
315
|
+
extension: fileEntity.extension,
|
|
316
|
+
});
|
|
317
|
+
// 添加验证数据(如果有)
|
|
318
|
+
const validationData = file._validationData;
|
|
319
|
+
if (validationData) {
|
|
320
|
+
fileEntity.addValidationData({
|
|
321
|
+
detectedTypes: validationData.detectedTypes || [],
|
|
322
|
+
signatures: ((_b = validationData.metadata) === null || _b === void 0 ? void 0 : _b.signatures) || [],
|
|
323
|
+
integrityChecks: [],
|
|
324
|
+
});
|
|
325
|
+
yield this.fileService.update(fileEntity.id, {
|
|
326
|
+
validationData: fileEntity.validationData,
|
|
327
|
+
});
|
|
328
|
+
}
|
|
329
|
+
// 附加到请求对象
|
|
330
|
+
request.fileEntity = fileEntity;
|
|
331
|
+
request.uploadId = uploadId;
|
|
332
|
+
return { fileEntity, uploadId };
|
|
333
|
+
}
|
|
334
|
+
catch (error) {
|
|
335
|
+
this.logger.error(`Failed to create file entity: ${error.message}`);
|
|
336
|
+
return { fileEntity: null, uploadId: null };
|
|
337
|
+
}
|
|
338
|
+
});
|
|
339
|
+
}
|
|
340
|
+
/**
|
|
341
|
+
* 执行处理器
|
|
342
|
+
*/
|
|
343
|
+
executeProcessors(file, request, options) {
|
|
344
|
+
return __awaiter(this, void 0, void 0, function* () {
|
|
345
|
+
// 装饰器配置的处理器
|
|
346
|
+
if (options.processor) {
|
|
347
|
+
const processorConfig = typeof options.processor === 'string'
|
|
348
|
+
? { name: options.processor, options: undefined }
|
|
349
|
+
: options.processor;
|
|
350
|
+
const processor = this.getProcessor(processorConfig.name);
|
|
351
|
+
if (processor) {
|
|
352
|
+
const fileEntity = this.convertToFileEntity(file);
|
|
353
|
+
// 使用请求上的 fileEntity(如果有)
|
|
354
|
+
const actualFileEntity = request.fileEntity || fileEntity;
|
|
355
|
+
if (request.fileEntity) {
|
|
356
|
+
fileEntity.id = request.fileEntity.id;
|
|
357
|
+
}
|
|
358
|
+
// 记录处理开始
|
|
359
|
+
if (actualFileEntity.id && this.fileService) {
|
|
360
|
+
yield this.fileService.updateProcessingStatus(actualFileEntity.id, processorConfig.name, 'pending');
|
|
361
|
+
}
|
|
362
|
+
// 合并处理器选项
|
|
363
|
+
const mergedOptions = Object.assign(Object.assign({}, processorConfig.options), options.processorOptions);
|
|
364
|
+
const result = yield processor.process(fileEntity, mergedOptions);
|
|
365
|
+
// 将处理结果保存到 file_metadata 表
|
|
366
|
+
if (result && actualFileEntity.id && this.fileService) {
|
|
367
|
+
yield this.saveProcessorResultToMetadata(actualFileEntity.id, processorConfig.name, result, mergedOptions);
|
|
368
|
+
}
|
|
369
|
+
// 记录处理成功
|
|
370
|
+
if (actualFileEntity.id && this.fileService) {
|
|
371
|
+
yield this.fileService.updateProcessingStatus(actualFileEntity.id, processorConfig.name, 'completed', result);
|
|
372
|
+
}
|
|
373
|
+
return result;
|
|
374
|
+
}
|
|
375
|
+
}
|
|
376
|
+
// 参数装饰器指定的处理器
|
|
377
|
+
if (request._processParams) {
|
|
378
|
+
const { name, options: procOptions } = request._processParams;
|
|
379
|
+
const processor = this.getProcessor(name);
|
|
380
|
+
if (processor) {
|
|
381
|
+
const fileEntity = this.convertToFileEntity(file);
|
|
382
|
+
return yield processor.process(fileEntity, procOptions);
|
|
383
|
+
}
|
|
384
|
+
}
|
|
385
|
+
return null;
|
|
386
|
+
});
|
|
387
|
+
}
|
|
388
|
+
/**
|
|
389
|
+
* 转换为 FileEntity
|
|
390
|
+
*/
|
|
391
|
+
convertToFileEntity(file) {
|
|
392
|
+
return {
|
|
393
|
+
id: file.filename || '',
|
|
394
|
+
originalName: file.originalname,
|
|
395
|
+
fileName: file.filename,
|
|
396
|
+
mimeType: file.mimetype,
|
|
397
|
+
size: file.size,
|
|
398
|
+
hash: '',
|
|
399
|
+
storage: 'local',
|
|
400
|
+
path: file.path,
|
|
401
|
+
uploadedAt: new Date(),
|
|
402
|
+
status: file_entity_interface_1.FileStatus.PROCESSING,
|
|
403
|
+
};
|
|
404
|
+
}
|
|
405
|
+
/**
|
|
406
|
+
* 更新成功状态
|
|
407
|
+
*/
|
|
408
|
+
updateFileEntityOnSuccess(fileEntity, uploadId, file, response) {
|
|
409
|
+
return __awaiter(this, void 0, void 0, function* () {
|
|
410
|
+
try {
|
|
411
|
+
const relativePath = utils_1.FilePathUtil.toRelativePath(file.path);
|
|
412
|
+
yield this.fileService.update(fileEntity.id, {
|
|
413
|
+
path: relativePath || (response === null || response === void 0 ? void 0 : response.path),
|
|
414
|
+
status: 'completed',
|
|
415
|
+
});
|
|
416
|
+
// 添加文件信息到响应
|
|
417
|
+
if (typeof response === 'object' && response !== null) {
|
|
418
|
+
response.fileId = fileEntity.id;
|
|
419
|
+
response.uploadId = uploadId;
|
|
420
|
+
}
|
|
421
|
+
// 同时将 fileId 添加到 file 对象,方便 controller 访问
|
|
422
|
+
file.fileId = fileEntity.id;
|
|
423
|
+
}
|
|
424
|
+
catch (error) {
|
|
425
|
+
this.logger.error(`Error updating file record: ${error.message}`);
|
|
426
|
+
}
|
|
427
|
+
});
|
|
428
|
+
}
|
|
429
|
+
/**
|
|
430
|
+
* 清理失败的文件
|
|
431
|
+
*/
|
|
432
|
+
cleanupOnFailure(fileEntity, file, error) {
|
|
433
|
+
return __awaiter(this, void 0, void 0, function* () {
|
|
434
|
+
try {
|
|
435
|
+
yield this.fileService.update(fileEntity.id, {
|
|
436
|
+
status: 'failed',
|
|
437
|
+
errorMessage: error.message || 'Unknown error',
|
|
438
|
+
});
|
|
439
|
+
// 清理临时文件
|
|
440
|
+
if (file.path) {
|
|
441
|
+
yield fs.unlink(file.path).catch((unlinkError) => {
|
|
442
|
+
if (unlinkError.code !== 'ENOENT') {
|
|
443
|
+
this.logger.error(`Failed to cleanup file: ${unlinkError.message}`);
|
|
444
|
+
}
|
|
445
|
+
});
|
|
446
|
+
}
|
|
447
|
+
}
|
|
448
|
+
catch (cleanupError) {
|
|
449
|
+
this.logger.error(`Error during cleanup: ${cleanupError.message}`);
|
|
450
|
+
}
|
|
451
|
+
});
|
|
452
|
+
}
|
|
339
453
|
/**
|
|
340
454
|
* 创建上传上下文
|
|
341
455
|
*/
|
|
342
456
|
createUploadContext(request, fileEntity) {
|
|
343
457
|
return {
|
|
344
|
-
getProcessor: (name) =>
|
|
345
|
-
return this.getProcessor(name);
|
|
346
|
-
},
|
|
458
|
+
getProcessor: (name) => this.getProcessor(name),
|
|
347
459
|
saveMetadata: (fileId, metadata) => __awaiter(this, void 0, void 0, function* () {
|
|
348
|
-
// 保存到数据库(如果fileService可用)
|
|
349
460
|
if (this.fileService && fileEntity) {
|
|
350
461
|
try {
|
|
351
462
|
for (const [key, value] of Object.entries(metadata)) {
|
|
@@ -356,18 +467,11 @@ let FileUploadInterceptor = FileUploadInterceptor_1 = class FileUploadIntercepto
|
|
|
356
467
|
this.logger.error(`Failed to save metadata: ${error.message}`);
|
|
357
468
|
}
|
|
358
469
|
}
|
|
359
|
-
|
|
360
|
-
if (!request['_fileMetadata']) {
|
|
361
|
-
request['_fileMetadata'] = {};
|
|
362
|
-
}
|
|
470
|
+
request['_fileMetadata'] = request['_fileMetadata'] || {};
|
|
363
471
|
request['_fileMetadata'][fileId] = metadata;
|
|
364
472
|
}),
|
|
365
|
-
getUser: () =>
|
|
366
|
-
|
|
367
|
-
},
|
|
368
|
-
getRequest: () => {
|
|
369
|
-
return request;
|
|
370
|
-
},
|
|
473
|
+
getUser: () => request['user'],
|
|
474
|
+
getRequest: () => request,
|
|
371
475
|
shared: request['_shared'] || (request['_shared'] = {}),
|
|
372
476
|
};
|
|
373
477
|
}
|
|
@@ -375,10 +479,8 @@ let FileUploadInterceptor = FileUploadInterceptor_1 = class FileUploadIntercepto
|
|
|
375
479
|
* 获取处理器
|
|
376
480
|
*/
|
|
377
481
|
getProcessor(name) {
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
}
|
|
381
|
-
return this.processors.get(name) || null;
|
|
482
|
+
var _a;
|
|
483
|
+
return ((_a = this.processors) === null || _a === void 0 ? void 0 : _a.get(name)) || null;
|
|
382
484
|
}
|
|
383
485
|
/**
|
|
384
486
|
* 生成uploadId
|
|
@@ -387,32 +489,90 @@ let FileUploadInterceptor = FileUploadInterceptor_1 = class FileUploadIntercepto
|
|
|
387
489
|
return `upload_${Date.now()}_${crypto.randomBytes(4).toString('hex')}`;
|
|
388
490
|
}
|
|
389
491
|
/**
|
|
390
|
-
*
|
|
492
|
+
* 判断是否应该在处理后删除原文件
|
|
391
493
|
*/
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
494
|
+
shouldDeleteAfterProcess(options, processResult) {
|
|
495
|
+
var _a, _b;
|
|
496
|
+
// 检查 processorOptions 中的 deleteAfterProcess 选项
|
|
497
|
+
if (((_a = options.processorOptions) === null || _a === void 0 ? void 0 : _a.deleteAfterProcess) === true) {
|
|
498
|
+
return true;
|
|
499
|
+
}
|
|
500
|
+
// 检查 processor 配置中的 deleteAfterProcess
|
|
501
|
+
if (typeof options.processor === 'object') {
|
|
502
|
+
if (((_b = options.processor.options) === null || _b === void 0 ? void 0 : _b.deleteAfterProcess) === true) {
|
|
503
|
+
return true;
|
|
504
|
+
}
|
|
505
|
+
}
|
|
506
|
+
return false;
|
|
396
507
|
}
|
|
397
508
|
/**
|
|
398
|
-
*
|
|
509
|
+
* 将处理器结果保存到 file_metadata 表
|
|
510
|
+
* 处理器可以通过返回结果中的 metadata 字段来指定要保存的内容
|
|
399
511
|
*/
|
|
400
|
-
|
|
401
|
-
return
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
512
|
+
saveProcessorResultToMetadata(fileId, processorName, result, options) {
|
|
513
|
+
return __awaiter(this, void 0, void 0, function* () {
|
|
514
|
+
try {
|
|
515
|
+
// 检查是否启用 saveToMetadata
|
|
516
|
+
if ((options === null || options === void 0 ? void 0 : options.saveToMetadata) === false) {
|
|
517
|
+
return;
|
|
518
|
+
}
|
|
519
|
+
// 如果结果为空或不是对象,直接返回
|
|
520
|
+
if (!result || typeof result !== 'object') {
|
|
521
|
+
return;
|
|
522
|
+
}
|
|
523
|
+
const metadataKey = (options === null || options === void 0 ? void 0 : options.metadataKey) || `${processorName}_parsed_data`;
|
|
524
|
+
// 准备要保存的数据
|
|
525
|
+
let metadataValue;
|
|
526
|
+
// 优先使用处理器返回的 metadata 字段
|
|
527
|
+
if (result.metadata && typeof result.metadata === 'object') {
|
|
528
|
+
// 处理器自己决定要保存的内容
|
|
529
|
+
metadataValue = Object.assign(Object.assign({}, result.metadata), { timestamp: result.metadata.timestamp || new Date().toISOString(), processorName: result.metadata.processorName || processorName });
|
|
530
|
+
}
|
|
531
|
+
else {
|
|
532
|
+
// 备选方案:保存整个结果(向后兼容)
|
|
533
|
+
metadataValue = {
|
|
534
|
+
timestamp: new Date().toISOString(),
|
|
535
|
+
processorName,
|
|
536
|
+
result,
|
|
537
|
+
};
|
|
538
|
+
}
|
|
539
|
+
// 保存到 file_metadata 表
|
|
540
|
+
yield this.fileService.addMetadata(fileId, metadataKey, metadataValue);
|
|
541
|
+
this.logger.log(`Processor result saved to metadata: ${metadataKey} for file ${fileId}`);
|
|
542
|
+
}
|
|
543
|
+
catch (error) {
|
|
544
|
+
this.logger.error(`Failed to save processor result to metadata: ${error.message}`);
|
|
545
|
+
// 不抛出错误,避免影响主流程
|
|
406
546
|
}
|
|
407
|
-
return mime === fileMimeType;
|
|
408
547
|
});
|
|
409
548
|
}
|
|
410
549
|
/**
|
|
411
|
-
*
|
|
550
|
+
* 删除原文件
|
|
412
551
|
*/
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
552
|
+
deleteOriginalFile(file, fileEntity) {
|
|
553
|
+
return __awaiter(this, void 0, void 0, function* () {
|
|
554
|
+
try {
|
|
555
|
+
// 删除物理文件
|
|
556
|
+
yield fs.unlink(file.path).catch((error) => {
|
|
557
|
+
if (error.code !== 'ENOENT') {
|
|
558
|
+
throw error;
|
|
559
|
+
}
|
|
560
|
+
});
|
|
561
|
+
this.logger.log(`Original file deleted after processing: ${file.originalname} (${file.path})`);
|
|
562
|
+
// 如果有文件实体,更新状态或标记
|
|
563
|
+
if (fileEntity && this.fileService) {
|
|
564
|
+
yield this.fileService.addMetadata(fileEntity.id, 'original_file_deleted', {
|
|
565
|
+
deletedAt: new Date().toISOString(),
|
|
566
|
+
reason: 'deleted_after_processing',
|
|
567
|
+
originalPath: file.path,
|
|
568
|
+
});
|
|
569
|
+
}
|
|
570
|
+
}
|
|
571
|
+
catch (error) {
|
|
572
|
+
this.logger.error(`Failed to delete original file after processing: ${error.message}`);
|
|
573
|
+
// 不抛出错误,避免影响主流程
|
|
574
|
+
}
|
|
575
|
+
});
|
|
416
576
|
}
|
|
417
577
|
};
|
|
418
578
|
exports.FileUploadInterceptor = FileUploadInterceptor;
|