@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.
Files changed (72) hide show
  1. package/cache/dependencies/db.dependency.d.ts +55 -6
  2. package/cache/dependencies/db.dependency.js +64 -13
  3. package/common/boilerplate.polyfill.js +1 -1
  4. package/file-upload/decorators/column.decorator.d.ts +151 -0
  5. package/file-upload/decorators/column.decorator.js +273 -0
  6. package/file-upload/decorators/csv-data.decorator.d.ts +17 -31
  7. package/file-upload/decorators/csv-data.decorator.js +45 -91
  8. package/file-upload/decorators/csv-import.decorator.d.ts +34 -0
  9. package/file-upload/decorators/csv-import.decorator.js +24 -0
  10. package/file-upload/decorators/examples/column-mapping.example.d.ts +76 -0
  11. package/file-upload/decorators/examples/column-mapping.example.js +122 -0
  12. package/file-upload/decorators/excel-data.decorator.d.ts +15 -29
  13. package/file-upload/decorators/excel-data.decorator.js +42 -82
  14. package/file-upload/decorators/index.d.ts +3 -2
  15. package/file-upload/decorators/index.js +20 -2
  16. package/file-upload/decorators/validate-data.decorator.d.ts +91 -0
  17. package/file-upload/decorators/validate-data.decorator.js +39 -0
  18. package/file-upload/dto/update-file.dto.d.ts +0 -1
  19. package/file-upload/dto/update-file.dto.js +0 -4
  20. package/file-upload/entities/file-metadata.entity.d.ts +6 -3
  21. package/file-upload/entities/file-metadata.entity.js +2 -10
  22. package/file-upload/entities/file.entity.d.ts +3 -18
  23. package/file-upload/entities/file.entity.js +0 -34
  24. package/file-upload/file-upload.module.d.ts +1 -1
  25. package/file-upload/file-upload.module.js +44 -16
  26. package/file-upload/index.d.ts +13 -2
  27. package/file-upload/index.js +21 -3
  28. package/file-upload/interceptors/file-upload.interceptor.d.ts +61 -8
  29. package/file-upload/interceptors/file-upload.interceptor.js +417 -257
  30. package/file-upload/interfaces/file-processor.interface.d.ts +93 -0
  31. package/file-upload/interfaces/file-processor.interface.js +2 -0
  32. package/file-upload/interfaces/file-upload-options.interface.d.ts +3 -46
  33. package/file-upload/interfaces/file-upload-options.interface.js +3 -0
  34. package/file-upload/interfaces/processor-options.interface.d.ts +102 -0
  35. package/file-upload/interfaces/processor-options.interface.js +2 -0
  36. package/file-upload/processors/csv.processor.d.ts +98 -0
  37. package/file-upload/processors/csv.processor.js +391 -0
  38. package/file-upload/processors/excel.processor.d.ts +130 -0
  39. package/file-upload/processors/excel.processor.js +547 -0
  40. package/file-upload/processors/image.processor.d.ts +199 -0
  41. package/file-upload/processors/image.processor.js +377 -0
  42. package/file-upload/services/file.service.d.ts +3 -0
  43. package/file-upload/services/file.service.js +39 -10
  44. package/file-upload/services/malicious-file-detector.service.d.ts +29 -3
  45. package/file-upload/services/malicious-file-detector.service.js +256 -57
  46. package/file-upload/utils/dynamic-import.util.d.ts +6 -2
  47. package/file-upload/utils/dynamic-import.util.js +17 -5
  48. package/http-client/decorators/http-client.decorators.d.ts +4 -2
  49. package/http-client/decorators/http-client.decorators.js +2 -1
  50. package/http-client/entities/http-log.entity.js +1 -9
  51. package/http-client/examples/proxy-from-environment.example.d.ts +133 -0
  52. package/http-client/examples/proxy-from-environment.example.js +410 -0
  53. package/http-client/http-client.module.js +65 -6
  54. package/http-client/interfaces/http-client-config.interface.d.ts +6 -0
  55. package/http-client/services/http-client.service.d.ts +8 -0
  56. package/http-client/services/http-client.service.js +61 -17
  57. package/http-client/services/logging.service.d.ts +1 -1
  58. package/http-client/services/logging.service.js +74 -58
  59. package/http-client/utils/index.d.ts +1 -0
  60. package/http-client/utils/index.js +1 -0
  61. package/http-client/utils/proxy-environment.util.d.ts +42 -0
  62. package/http-client/utils/proxy-environment.util.js +148 -0
  63. package/package.json +9 -5
  64. package/shared/service-registry.module.js +18 -0
  65. package/transaction/data-source.util.d.ts +142 -0
  66. package/transaction/data-source.util.js +330 -0
  67. package/transaction/index.d.ts +1 -0
  68. package/transaction/index.js +12 -1
  69. package/validators/is-exists.validator.d.ts +19 -2
  70. package/validators/is-exists.validator.js +27 -2
  71. package/validators/is-unique.validator.d.ts +12 -1
  72. 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
- // 1. 验证文件大小(读取真实大小)
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
- // 5. 文件名清理(默认启用)
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
- // 6. 文件签名验证(如果文件已上传到磁盘)
123
- if (file.path && options.types && options.types.length > 0) {
124
- const validateSignature = options.validateSignature !== undefined
125
- ? options.validateSignature
126
- : ((_a = this.moduleOptions) === null || _a === void 0 ? void 0 : _a.validateSignature) !== false;
127
- this.logger.debug(`FileUploadInterceptor: Signature validation - will validate: ${validateSignature}`);
128
- if (validateSignature && this.signatureValidator) {
129
- try {
130
- const allowedTypes = this.mimeRegistry.resolveFileTypes(options.types);
131
- if (allowedTypes.signatureTypes.length > 0) {
132
- const signatureResult = yield this.signatureValidator.validateFileSignature(file.path, allowedTypes.signatureTypes, (_b = this.moduleOptions) === null || _b === void 0 ? void 0 : _b.signatureValidation);
133
- if (!signatureResult.valid) {
134
- const detectedType = (_c = signatureResult.detectedTypes[0]) === null || _c === void 0 ? void 0 : _c.type;
135
- this.logger.debug(`FileUploadInterceptor: File signature mismatch: ${signatureResult.errors.join(', ')}`);
136
- throw new file_upload_exception_1.FileSignatureMismatchException(signatureResult.errors.join(', '), file.mimetype, detectedType);
137
- }
138
- // 将验证数据附加到请求
139
- request.fileValidationData = signatureResult;
140
- this.logger.debug('FileUploadInterceptor: File signature validation passed');
141
- }
142
- }
143
- catch (error) {
144
- if (error instanceof file_upload_exception_1.FileSignatureMismatchException) {
145
- throw error;
146
- }
147
- // 签名验证出错
148
- if ((_e = (_d = this.moduleOptions) === null || _d === void 0 ? void 0 : _d.signatureValidation) === null || _e === void 0 ? void 0 : _e.strict) {
149
- this.logger.error(`Signature validation failed in strict mode: ${error.message}`, error.stack);
150
- throw error;
151
- }
152
- this.logger.warn(`Signature validation error (non-strict mode): ${error.message}`);
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
- // 7. 恶意文件检测(如果文件已上传到磁盘)
157
- const shouldPerformMaliciousCheck = options.maliciousCheck !== undefined
158
- ? options.maliciousCheck
159
- : ((_g = (_f = this.moduleOptions) === null || _f === void 0 ? void 0 : _f.maliciousDetection) === null || _g === void 0 ? void 0 : _g.enabled) !== false;
160
- this.logger.debug(`FileUploadInterceptor: Malicious check - should perform: ${shouldPerformMaliciousCheck}`);
161
- if (shouldPerformMaliciousCheck && this.maliciousDetector && file.path) {
162
- this.logger.debug(`FileUploadInterceptor: Performing malicious file detection on: ${file.path}`);
163
- try {
164
- const maliciousCheck = yield this.maliciousDetector.scanFile(file.path);
165
- this.logger.debug(`FileUploadInterceptor: Malicious check result: safe=${maliciousCheck.safe}, score=${maliciousCheck.riskScore}, threats=${maliciousCheck.threats.join(', ')}`);
166
- if (!maliciousCheck.safe) {
167
- this.logger.warn(`FileUploadInterceptor: Malicious file detected: ${file.originalname}`, maliciousCheck.threats);
168
- throw new file_upload_exception_1.MaliciousFileException(maliciousCheck.threats);
169
- }
170
- // 附加检测数据到请求
171
- request.fileMaliciousCheck = maliciousCheck;
172
- this.logger.debug('FileUploadInterceptor: Malicious file detection passed');
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
- catch (error) {
175
- if (error instanceof file_upload_exception_1.MaliciousFileException) {
176
- throw error;
177
- }
178
- // 检测出错,记录日志但继续
179
- this.logger.error(`FileUploadInterceptor: Malicious detection error: ${error.message}`);
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
- else {
183
- this.logger.debug(`FileUploadInterceptor: Skipping malicious check - detector: ${!!this.maliciousDetector}, file.path: ${!!file.path}`);
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
- // 创建文件实体记录(如果FileService可用)
187
- let fileEntity = null;
188
- let uploadId = null;
189
- if (this.fileService) {
190
- try {
191
- uploadId = request.body.uploadId || this.generateUploadId();
192
- const checksum = file.path
193
- ? yield this.calculateChecksum(file.path)
194
- : undefined;
195
- // 处理 originalName: UTF-8 编码并截取到 100 字符(使用统一工具类)
196
- const processedOriginalName = utils_1.FileNameUtil.decodeAndNormalize(file.originalname);
197
- fileEntity = yield this.fileService.create({
198
- originalName: processedOriginalName,
199
- filename: file.filename,
200
- mimeType: file.mimetype,
201
- size: file.size,
202
- tempPath: file.path,
203
- storageProvider: 'local',
204
- userId: (_h = request.user) === null || _h === void 0 ? void 0 : _h.id,
205
- uploadId,
206
- status: 'processing',
207
- checksum,
208
- });
209
- // 自动识别并设置文件类型
210
- fileEntity.detectAndSetFileType();
211
- yield this.fileService.update(fileEntity.id, {
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
- catch (error) {
232
- this.logger.error(`Failed to create file entity: ${error.message}`);
233
- // 设置fileEntity为null,后续代码会检查并跳过数据库操作
234
- fileEntity = null;
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
- if (options.processor) {
246
- const processorConfig = typeof options.processor === 'string'
247
- ? { name: options.processor, options: undefined }
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
- try {
280
- // 计算相对路径(使用统一工具类)
281
- const relativePath = utils_1.FilePathUtil.toRelativePath(file.path);
282
- this.logger.debug(`Saving relative path: ${relativePath}`);
283
- yield this.fileService.update(fileEntity.id, {
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
- try {
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
- return request['user'];
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
- if (!this.processors) {
379
- return null;
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
- calculateChecksum(filePath) {
393
- return __awaiter(this, void 0, void 0, function* () {
394
- return utils_1.ChecksumUtil.calculate(filePath, 'md5');
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
- * 验证 MIME 类型
509
+ * 将处理器结果保存到 file_metadata
510
+ * 处理器可以通过返回结果中的 metadata 字段来指定要保存的内容
399
511
  */
400
- validateMimeType(fileMimeType, allowedMimes) {
401
- return allowedMimes.some((mime) => {
402
- // 支持通配符,如 image/*
403
- if (mime.endsWith('/*')) {
404
- const prefix = mime.slice(0, -2);
405
- return fileMimeType.startsWith(prefix);
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
- validateExtension(fileExt, allowedExts) {
414
- const normalizedExt = fileExt.toLowerCase();
415
- return allowedExts.some((ext) => ext.toLowerCase() === normalizedExt);
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;