@nest-omni/core 4.1.3-11 → 4.1.3-12
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/audit/audit.module.js +17 -0
- package/audit/controllers/audit.controller.d.ts +64 -0
- package/audit/controllers/audit.controller.js +50 -0
- package/audit/decorators/audit-action.decorator.d.ts +74 -0
- package/audit/decorators/audit-action.decorator.js +42 -0
- package/audit/decorators/entity-audit.decorator.d.ts +10 -1
- package/audit/decorators/entity-audit.decorator.js +34 -16
- package/audit/decorators/index.d.ts +1 -0
- package/audit/decorators/index.js +1 -0
- package/audit/entities/audit-action-summary.entity.d.ts +23 -0
- package/audit/entities/audit-action-summary.entity.js +101 -0
- package/audit/entities/entity-audit-log.entity.d.ts +3 -0
- package/audit/entities/entity-audit-log.entity.js +25 -2
- package/audit/entities/entity-transaction.entity.d.ts +3 -4
- package/audit/entities/entity-transaction.entity.js +10 -3
- package/audit/entities/index.d.ts +1 -0
- package/audit/entities/index.js +1 -0
- package/audit/entities/manual-operation-log.entity.js +8 -1
- package/audit/enums/audit.enums.d.ts +1 -10
- package/audit/enums/audit.enums.js +7 -17
- package/audit/index.d.ts +2 -1
- package/audit/index.js +5 -1
- package/audit/interceptors/audit-action.interceptor.d.ts +38 -0
- package/audit/interceptors/audit-action.interceptor.js +215 -0
- package/audit/interceptors/index.d.ts +1 -0
- package/audit/interceptors/index.js +1 -0
- package/audit/interfaces/audit.interfaces.d.ts +10 -5
- package/audit/services/audit-action.service.d.ts +141 -0
- package/audit/services/audit-action.service.js +244 -0
- package/audit/services/audit-context.service.d.ts +82 -0
- package/audit/services/audit-context.service.js +170 -0
- package/audit/services/entity-audit.service.d.ts +104 -3
- package/audit/services/entity-audit.service.js +306 -9
- package/audit/services/index.d.ts +1 -0
- package/audit/services/index.js +1 -0
- package/audit/services/manual-audit-log.service.d.ts +24 -23
- package/audit/services/manual-audit-log.service.js +32 -53
- package/audit/services/operation-description.service.d.ts +13 -3
- package/audit/services/operation-description.service.js +161 -24
- package/audit/services/transaction-audit.service.js +3 -3
- package/audit/subscribers/entity-audit.subscriber.d.ts +4 -0
- package/audit/subscribers/entity-audit.subscriber.js +47 -0
- package/file-upload/controllers/file-access.controller.d.ts +23 -0
- package/file-upload/controllers/file-access.controller.js +128 -0
- package/file-upload/decorators/csv-data.decorator.d.ts +44 -0
- package/file-upload/decorators/csv-data.decorator.js +131 -0
- package/file-upload/decorators/excel-data.decorator.d.ts +44 -0
- package/file-upload/decorators/excel-data.decorator.js +125 -0
- package/file-upload/decorators/file-upload.decorator.d.ts +83 -0
- package/file-upload/decorators/file-upload.decorator.js +172 -0
- package/file-upload/decorators/index.d.ts +4 -0
- package/file-upload/decorators/index.js +20 -0
- package/file-upload/decorators/process.decorator.d.ts +40 -0
- package/file-upload/decorators/process.decorator.js +52 -0
- package/file-upload/dto/create-file.dto.d.ts +24 -0
- package/file-upload/dto/create-file.dto.js +112 -0
- package/file-upload/dto/find-files.dto.d.ts +15 -0
- package/file-upload/dto/find-files.dto.js +76 -0
- package/file-upload/dto/index.d.ts +4 -0
- package/file-upload/dto/index.js +20 -0
- package/file-upload/dto/pagination.dto.d.ts +7 -0
- package/file-upload/dto/pagination.dto.js +39 -0
- package/file-upload/dto/update-file.dto.d.ts +16 -0
- package/file-upload/dto/update-file.dto.js +71 -0
- package/file-upload/entities/file-metadata.entity.d.ts +22 -0
- package/file-upload/entities/file-metadata.entity.js +84 -0
- package/file-upload/entities/file.entity.d.ts +129 -0
- package/file-upload/entities/file.entity.js +384 -0
- package/file-upload/entities/index.d.ts +2 -0
- package/file-upload/entities/index.js +18 -0
- package/file-upload/enums/file-type.enum.d.ts +72 -0
- package/file-upload/enums/file-type.enum.js +212 -0
- package/file-upload/exceptions/file-upload.exception.d.ts +57 -0
- package/file-upload/exceptions/file-upload.exception.js +120 -0
- package/file-upload/exceptions/index.d.ts +1 -0
- package/file-upload/exceptions/index.js +17 -0
- package/file-upload/file-upload.module.d.ts +89 -0
- package/file-upload/file-upload.module.js +264 -0
- package/file-upload/index.d.ts +26 -0
- package/file-upload/index.js +59 -0
- package/file-upload/interceptors/file-upload.interceptor.d.ts +48 -0
- package/file-upload/interceptors/file-upload.interceptor.js +434 -0
- package/file-upload/interceptors/index.d.ts +1 -0
- package/file-upload/interceptors/index.js +17 -0
- package/file-upload/interfaces/custom-file-type.interface.d.ts +72 -0
- package/file-upload/interfaces/custom-file-type.interface.js +2 -0
- package/file-upload/interfaces/file-buffer.interface.d.ts +72 -0
- package/file-upload/interfaces/file-buffer.interface.js +2 -0
- package/file-upload/interfaces/file-entity.interface.d.ts +142 -0
- package/file-upload/interfaces/file-entity.interface.js +28 -0
- package/file-upload/interfaces/file-metadata.interface.d.ts +21 -0
- package/file-upload/interfaces/file-metadata.interface.js +2 -0
- package/file-upload/interfaces/file-upload-options.interface.d.ts +117 -0
- package/file-upload/interfaces/file-upload-options.interface.js +2 -0
- package/file-upload/interfaces/index.d.ts +7 -0
- package/file-upload/interfaces/index.js +24 -0
- package/file-upload/interfaces/storage-provider.interface.d.ts +239 -0
- package/file-upload/interfaces/storage-provider.interface.js +2 -0
- package/file-upload/interfaces/upload-options.interface.d.ts +19 -0
- package/file-upload/interfaces/upload-options.interface.js +2 -0
- package/file-upload/providers/index.d.ts +2 -0
- package/file-upload/providers/index.js +18 -0
- package/file-upload/providers/local-storage.provider.d.ts +98 -0
- package/file-upload/providers/local-storage.provider.js +484 -0
- package/file-upload/providers/s3-storage.provider.d.ts +87 -0
- package/file-upload/providers/s3-storage.provider.js +455 -0
- package/file-upload/services/file-signature-validator.service.d.ts +118 -0
- package/file-upload/services/file-signature-validator.service.js +376 -0
- package/file-upload/services/file.service.d.ts +190 -0
- package/file-upload/services/file.service.js +609 -0
- package/file-upload/services/index.d.ts +4 -0
- package/file-upload/services/index.js +20 -0
- package/file-upload/services/malicious-file-detector.service.d.ts +274 -0
- package/file-upload/services/malicious-file-detector.service.js +1035 -0
- package/file-upload/services/mime-registry.service.d.ts +47 -0
- package/file-upload/services/mime-registry.service.js +167 -0
- package/file-upload/utils/checksum.util.d.ts +28 -0
- package/file-upload/utils/checksum.util.js +65 -0
- package/file-upload/utils/dynamic-import.util.d.ts +50 -0
- package/file-upload/utils/dynamic-import.util.js +144 -0
- package/file-upload/utils/filename.util.d.ts +59 -0
- package/file-upload/utils/filename.util.js +184 -0
- package/file-upload/utils/filepath.util.d.ts +70 -0
- package/file-upload/utils/filepath.util.js +152 -0
- package/file-upload/utils/index.d.ts +4 -0
- package/file-upload/utils/index.js +20 -0
- package/index.d.ts +3 -1
- package/index.js +4 -1
- package/package.json +4 -5
- package/setup/bootstrap.setup.d.ts +1 -0
- package/setup/bootstrap.setup.js +1 -0
- package/shared/index.d.ts +1 -1
- package/shared/index.js +1 -1
- package/shared/{serviceRegistryModule.js → service-registry.module.js} +0 -12
- package/shared/services/index.d.ts +0 -1
- package/shared/services/index.js +0 -1
- package/transaction/__tests__/mocks.d.ts +9 -0
- package/transaction/__tests__/mocks.js +33 -0
- package/transaction/base-service-transaction.d.ts +99 -0
- package/transaction/base-service-transaction.js +286 -0
- package/transaction/cls-compatibility.service.d.ts +55 -0
- package/transaction/cls-compatibility.service.js +127 -0
- package/transaction/data-source-registry.d.ts +91 -0
- package/transaction/data-source-registry.js +349 -0
- package/transaction/database-adapter.d.ts +44 -0
- package/transaction/database-adapter.js +240 -0
- package/transaction/decorators/entity-datasource.decorator.d.ts +62 -0
- package/transaction/decorators/entity-datasource.decorator.js +105 -0
- package/transaction/index.d.ts +14 -0
- package/transaction/index.js +57 -0
- package/transaction/logging-transactional.interceptor.d.ts +18 -0
- package/transaction/logging-transactional.interceptor.js +163 -0
- package/transaction/transaction-context.service.d.ts +137 -0
- package/transaction/transaction-context.service.js +411 -0
- package/transaction/transaction-manager.d.ts +230 -0
- package/transaction/transaction-manager.js +1001 -0
- package/transaction/transaction-synchronization.d.ts +171 -0
- package/transaction/transaction-synchronization.js +380 -0
- package/transaction/transaction.errors.d.ts +91 -0
- package/transaction/transaction.errors.js +206 -0
- package/transaction/transaction.module.d.ts +30 -0
- package/transaction/transaction.module.js +98 -0
- package/transaction/transactional.decorator.d.ts +82 -0
- package/transaction/transactional.decorator.js +319 -0
- package/transaction/typeorm-module-wrapper.d.ts +96 -0
- package/transaction/typeorm-module-wrapper.js +197 -0
- package/validators/file-mimetype.validator.d.ts +0 -2
- package/validators/file-mimetype.validator.js +4 -6
- package/validators/is-exists.validator.d.ts +2 -5
- package/validators/is-exists.validator.js +4 -6
- package/validators/is-unique.validator.d.ts +2 -5
- package/validators/is-unique.validator.js +6 -11
- package/shared/services/validator.service.d.ts +0 -3
- package/shared/services/validator.service.js +0 -20
- /package/shared/{serviceRegistryModule.d.ts → service-registry.module.d.ts} +0 -0
|
@@ -0,0 +1,434 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __decorate = (this && this.__decorate) || function (decorators, target, key, desc) {
|
|
3
|
+
var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d;
|
|
4
|
+
if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc);
|
|
5
|
+
else for (var i = decorators.length - 1; i >= 0; i--) if (d = decorators[i]) r = (c < 3 ? d(r) : c > 3 ? d(target, key, r) : d(target, key)) || r;
|
|
6
|
+
return c > 3 && r && Object.defineProperty(target, key, r), r;
|
|
7
|
+
};
|
|
8
|
+
var __metadata = (this && this.__metadata) || function (k, v) {
|
|
9
|
+
if (typeof Reflect === "object" && typeof Reflect.metadata === "function") return Reflect.metadata(k, v);
|
|
10
|
+
};
|
|
11
|
+
var __param = (this && this.__param) || function (paramIndex, decorator) {
|
|
12
|
+
return function (target, key) { decorator(target, key, paramIndex); }
|
|
13
|
+
};
|
|
14
|
+
var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) {
|
|
15
|
+
function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); }
|
|
16
|
+
return new (P || (P = Promise))(function (resolve, reject) {
|
|
17
|
+
function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }
|
|
18
|
+
function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } }
|
|
19
|
+
function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); }
|
|
20
|
+
step((generator = generator.apply(thisArg, _arguments || [])).next());
|
|
21
|
+
});
|
|
22
|
+
};
|
|
23
|
+
var FileUploadInterceptor_1;
|
|
24
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
25
|
+
exports.FileUploadInterceptor = void 0;
|
|
26
|
+
const common_1 = require("@nestjs/common");
|
|
27
|
+
const core_1 = require("@nestjs/core");
|
|
28
|
+
const rxjs_1 = require("rxjs");
|
|
29
|
+
const operators_1 = require("rxjs/operators");
|
|
30
|
+
const file_upload_decorator_1 = require("../decorators/file-upload.decorator");
|
|
31
|
+
const file_service_1 = require("../services/file.service");
|
|
32
|
+
const file_signature_validator_service_1 = require("../services/file-signature-validator.service");
|
|
33
|
+
const malicious_file_detector_service_1 = require("../services/malicious-file-detector.service");
|
|
34
|
+
const mime_registry_service_1 = require("../services/mime-registry.service");
|
|
35
|
+
const utils_1 = require("../utils");
|
|
36
|
+
const file_upload_exception_1 = require("../exceptions/file-upload.exception");
|
|
37
|
+
const fs = require("fs-extra");
|
|
38
|
+
const crypto = require("crypto");
|
|
39
|
+
/**
|
|
40
|
+
* 文件上传拦截器(增强版)
|
|
41
|
+
* 负责创建文件记录、执行钩子、处理器等后置逻辑
|
|
42
|
+
*/
|
|
43
|
+
let FileUploadInterceptor = FileUploadInterceptor_1 = class FileUploadInterceptor {
|
|
44
|
+
constructor(reflector, mimeRegistry, processors, fileService, signatureValidator, maliciousDetector, moduleOptions) {
|
|
45
|
+
this.reflector = reflector;
|
|
46
|
+
this.mimeRegistry = mimeRegistry;
|
|
47
|
+
this.processors = processors;
|
|
48
|
+
this.fileService = fileService;
|
|
49
|
+
this.signatureValidator = signatureValidator;
|
|
50
|
+
this.maliciousDetector = maliciousDetector;
|
|
51
|
+
this.moduleOptions = moduleOptions;
|
|
52
|
+
this.logger = new common_1.Logger(FileUploadInterceptor_1.name);
|
|
53
|
+
}
|
|
54
|
+
intercept(context, next) {
|
|
55
|
+
return __awaiter(this, void 0, void 0, function* () {
|
|
56
|
+
var _a, _b, _c, _d, _e, _f, _g, _h, _j;
|
|
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
|
+
if (!options) {
|
|
60
|
+
this.logger.debug('FileUploadInterceptor: No options provided, continuing');
|
|
61
|
+
return next.handle();
|
|
62
|
+
}
|
|
63
|
+
const request = context.switchToHttp().getRequest();
|
|
64
|
+
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
|
+
if (!file) {
|
|
72
|
+
this.logger.debug('FileUploadInterceptor: No file in request, continuing');
|
|
73
|
+
return next.handle();
|
|
74
|
+
}
|
|
75
|
+
// ===== 在拦截器中执行所有验证(文件已上传到磁盘) =====
|
|
76
|
+
// 1. 验证文件大小(读取真实大小)
|
|
77
|
+
if (options.maxSize && file.size > options.maxSize) {
|
|
78
|
+
this.logger.debug(`FileUploadInterceptor: File size exceeded: ${file.size} > ${options.maxSize}`);
|
|
79
|
+
throw new file_upload_exception_1.FileSizeExceededException(file.size, options.maxSize);
|
|
80
|
+
}
|
|
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. 验证访问权限
|
|
108
|
+
if (options.auth && !request.user) {
|
|
109
|
+
this.logger.debug('FileUploadInterceptor: Authentication required but not provided');
|
|
110
|
+
throw new common_1.UnauthorizedException('Authentication required for file upload');
|
|
111
|
+
}
|
|
112
|
+
// 5. 文件名清理(默认启用)
|
|
113
|
+
if (options.sanitize !== false) {
|
|
114
|
+
const sanitized = utils_1.FileNameUtil.sanitize(file.originalname);
|
|
115
|
+
if (!sanitized || sanitized.length === 0) {
|
|
116
|
+
this.logger.debug(`FileUploadInterceptor: Invalid filename after sanitization: ${file.originalname}`);
|
|
117
|
+
throw new file_upload_exception_1.InvalidFilenameException(file.originalname);
|
|
118
|
+
}
|
|
119
|
+
// 注意:这里不修改 file.originalname,保持原始文件名用于后续处理
|
|
120
|
+
this.logger.debug(`FileUploadInterceptor: Filename sanitized to: ${sanitized}`);
|
|
121
|
+
}
|
|
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
|
+
}
|
|
154
|
+
}
|
|
155
|
+
}
|
|
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');
|
|
173
|
+
}
|
|
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}`);
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
else {
|
|
183
|
+
this.logger.debug(`FileUploadInterceptor: Skipping malicious check - detector: ${!!this.maliciousDetector}, file.path: ${!!file.path}`);
|
|
184
|
+
}
|
|
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}`);
|
|
230
|
+
}
|
|
231
|
+
catch (error) {
|
|
232
|
+
this.logger.error(`Failed to create file entity: ${error.message}`);
|
|
233
|
+
// 设置fileEntity为null,后续代码会检查并跳过数据库操作
|
|
234
|
+
fileEntity = null;
|
|
235
|
+
}
|
|
236
|
+
}
|
|
237
|
+
// 创建上传上下文
|
|
238
|
+
const uploadContext = this.createUploadContext(request, fileEntity);
|
|
239
|
+
try {
|
|
240
|
+
// 执行 before 钩子
|
|
241
|
+
if (options.before) {
|
|
242
|
+
yield options.before(file, uploadContext);
|
|
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
|
+
}
|
|
271
|
+
}
|
|
272
|
+
return next.handle().pipe((0, operators_1.tap)((response) => __awaiter(this, void 0, void 0, function* () {
|
|
273
|
+
// 执行 after 钩子
|
|
274
|
+
if (options.after) {
|
|
275
|
+
yield options.after(file, uploadContext);
|
|
276
|
+
}
|
|
277
|
+
// 更新文件实体为完成状态
|
|
278
|
+
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
|
+
}
|
|
297
|
+
}
|
|
298
|
+
})), (0, operators_1.catchError)((error) => __awaiter(this, void 0, void 0, function* () {
|
|
299
|
+
// 更新文件实体为失败状态
|
|
300
|
+
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
|
+
}
|
|
323
|
+
}
|
|
324
|
+
return (0, rxjs_1.throwError)(() => error);
|
|
325
|
+
})));
|
|
326
|
+
}
|
|
327
|
+
catch (error) {
|
|
328
|
+
// 处理过程中的错误
|
|
329
|
+
if (fileEntity && this.fileService) {
|
|
330
|
+
yield this.fileService.update(fileEntity.id, {
|
|
331
|
+
status: 'failed',
|
|
332
|
+
errorMessage: error.message,
|
|
333
|
+
});
|
|
334
|
+
}
|
|
335
|
+
throw error;
|
|
336
|
+
}
|
|
337
|
+
});
|
|
338
|
+
}
|
|
339
|
+
/**
|
|
340
|
+
* 创建上传上下文
|
|
341
|
+
*/
|
|
342
|
+
createUploadContext(request, fileEntity) {
|
|
343
|
+
return {
|
|
344
|
+
getProcessor: (name) => {
|
|
345
|
+
return this.getProcessor(name);
|
|
346
|
+
},
|
|
347
|
+
saveMetadata: (fileId, metadata) => __awaiter(this, void 0, void 0, function* () {
|
|
348
|
+
// 保存到数据库(如果fileService可用)
|
|
349
|
+
if (this.fileService && fileEntity) {
|
|
350
|
+
try {
|
|
351
|
+
for (const [key, value] of Object.entries(metadata)) {
|
|
352
|
+
yield this.fileService.addMetadata(fileEntity.id, key, value);
|
|
353
|
+
}
|
|
354
|
+
}
|
|
355
|
+
catch (error) {
|
|
356
|
+
this.logger.error(`Failed to save metadata: ${error.message}`);
|
|
357
|
+
}
|
|
358
|
+
}
|
|
359
|
+
// 也附加到请求对象
|
|
360
|
+
if (!request['_fileMetadata']) {
|
|
361
|
+
request['_fileMetadata'] = {};
|
|
362
|
+
}
|
|
363
|
+
request['_fileMetadata'][fileId] = metadata;
|
|
364
|
+
}),
|
|
365
|
+
getUser: () => {
|
|
366
|
+
return request['user'];
|
|
367
|
+
},
|
|
368
|
+
getRequest: () => {
|
|
369
|
+
return request;
|
|
370
|
+
},
|
|
371
|
+
shared: request['_shared'] || (request['_shared'] = {}),
|
|
372
|
+
};
|
|
373
|
+
}
|
|
374
|
+
/**
|
|
375
|
+
* 获取处理器
|
|
376
|
+
*/
|
|
377
|
+
getProcessor(name) {
|
|
378
|
+
if (!this.processors) {
|
|
379
|
+
return null;
|
|
380
|
+
}
|
|
381
|
+
return this.processors.get(name) || null;
|
|
382
|
+
}
|
|
383
|
+
/**
|
|
384
|
+
* 生成uploadId
|
|
385
|
+
*/
|
|
386
|
+
generateUploadId() {
|
|
387
|
+
return `upload_${Date.now()}_${crypto.randomBytes(4).toString('hex')}`;
|
|
388
|
+
}
|
|
389
|
+
/**
|
|
390
|
+
* 计算文件校验和
|
|
391
|
+
*/
|
|
392
|
+
calculateChecksum(filePath) {
|
|
393
|
+
return __awaiter(this, void 0, void 0, function* () {
|
|
394
|
+
return utils_1.ChecksumUtil.calculate(filePath, 'md5');
|
|
395
|
+
});
|
|
396
|
+
}
|
|
397
|
+
/**
|
|
398
|
+
* 验证 MIME 类型
|
|
399
|
+
*/
|
|
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);
|
|
406
|
+
}
|
|
407
|
+
return mime === fileMimeType;
|
|
408
|
+
});
|
|
409
|
+
}
|
|
410
|
+
/**
|
|
411
|
+
* 验证扩展名
|
|
412
|
+
*/
|
|
413
|
+
validateExtension(fileExt, allowedExts) {
|
|
414
|
+
const normalizedExt = fileExt.toLowerCase();
|
|
415
|
+
return allowedExts.some((ext) => ext.toLowerCase() === normalizedExt);
|
|
416
|
+
}
|
|
417
|
+
};
|
|
418
|
+
exports.FileUploadInterceptor = FileUploadInterceptor;
|
|
419
|
+
exports.FileUploadInterceptor = FileUploadInterceptor = FileUploadInterceptor_1 = __decorate([
|
|
420
|
+
(0, common_1.Injectable)(),
|
|
421
|
+
__param(2, (0, common_1.Optional)()),
|
|
422
|
+
__param(2, (0, common_1.Inject)('FILE_PROCESSORS')),
|
|
423
|
+
__param(3, (0, common_1.Optional)()),
|
|
424
|
+
__param(4, (0, common_1.Optional)()),
|
|
425
|
+
__param(5, (0, common_1.Optional)()),
|
|
426
|
+
__param(6, (0, common_1.Optional)()),
|
|
427
|
+
__param(6, (0, common_1.Inject)('FILE_UPLOAD_OPTIONS')),
|
|
428
|
+
__metadata("design:paramtypes", [core_1.Reflector,
|
|
429
|
+
mime_registry_service_1.MimeRegistryService,
|
|
430
|
+
Map,
|
|
431
|
+
file_service_1.FileService,
|
|
432
|
+
file_signature_validator_service_1.FileSignatureValidator,
|
|
433
|
+
malicious_file_detector_service_1.MaliciousFileDetector, Object])
|
|
434
|
+
], FileUploadInterceptor);
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export * from './file-upload.interceptor';
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
|
3
|
+
if (k2 === undefined) k2 = k;
|
|
4
|
+
var desc = Object.getOwnPropertyDescriptor(m, k);
|
|
5
|
+
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
|
6
|
+
desc = { enumerable: true, get: function() { return m[k]; } };
|
|
7
|
+
}
|
|
8
|
+
Object.defineProperty(o, k2, desc);
|
|
9
|
+
}) : (function(o, m, k, k2) {
|
|
10
|
+
if (k2 === undefined) k2 = k;
|
|
11
|
+
o[k2] = m[k];
|
|
12
|
+
}));
|
|
13
|
+
var __exportStar = (this && this.__exportStar) || function(m, exports) {
|
|
14
|
+
for (var p in m) if (p !== "default" && !Object.prototype.hasOwnProperty.call(exports, p)) __createBinding(exports, m, p);
|
|
15
|
+
};
|
|
16
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
17
|
+
__exportStar(require("./file-upload.interceptor"), exports);
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 文件签名定义
|
|
3
|
+
*/
|
|
4
|
+
export interface FileSignature {
|
|
5
|
+
/** 签名内容 - 字节数组或字符串 */
|
|
6
|
+
bytes: number[] | string;
|
|
7
|
+
/** 签名偏移量(默认 0) */
|
|
8
|
+
offset?: number;
|
|
9
|
+
/** 是否精确匹配(默认 true) */
|
|
10
|
+
exact?: boolean;
|
|
11
|
+
/** 文件尾部签名(可选) */
|
|
12
|
+
trailer?: FileSignature;
|
|
13
|
+
/** 签名长度(用于变体检测) */
|
|
14
|
+
length?: number;
|
|
15
|
+
/** 掩码(用于忽略某些位) */
|
|
16
|
+
mask?: number[] | string;
|
|
17
|
+
}
|
|
18
|
+
/**
|
|
19
|
+
* 自定义文件类型定义
|
|
20
|
+
*/
|
|
21
|
+
export interface CustomFileType {
|
|
22
|
+
/** MIME 类型 */
|
|
23
|
+
mime: string | string[];
|
|
24
|
+
/** 文件扩展名 */
|
|
25
|
+
extension?: string | string[];
|
|
26
|
+
/** 文件签名/魔数 */
|
|
27
|
+
signature?: FileSignature | FileSignature[];
|
|
28
|
+
/** 文件类型描述 */
|
|
29
|
+
description?: string;
|
|
30
|
+
/** 文件类型图标 */
|
|
31
|
+
icon?: string;
|
|
32
|
+
/** 是否为图像类型 */
|
|
33
|
+
isImage?: boolean;
|
|
34
|
+
/** 是否为文档类型 */
|
|
35
|
+
isDocument?: boolean;
|
|
36
|
+
/** 是否为音视频类型 */
|
|
37
|
+
isMedia?: boolean;
|
|
38
|
+
}
|
|
39
|
+
/**
|
|
40
|
+
* 自定义文件类型集合
|
|
41
|
+
*/
|
|
42
|
+
export interface CustomFileTypes {
|
|
43
|
+
[typeName: string]: CustomFileType;
|
|
44
|
+
}
|
|
45
|
+
/**
|
|
46
|
+
* MIME 配置
|
|
47
|
+
*/
|
|
48
|
+
export interface MimeConfig {
|
|
49
|
+
/** MIME 类型列表 */
|
|
50
|
+
mimes: string[];
|
|
51
|
+
/** 扩展名列表 */
|
|
52
|
+
extensions: string[];
|
|
53
|
+
/** 描述 */
|
|
54
|
+
description?: string;
|
|
55
|
+
/** 图标 */
|
|
56
|
+
icon?: string;
|
|
57
|
+
/** 签名 */
|
|
58
|
+
signatures?: FileSignature | FileSignature[];
|
|
59
|
+
/** 文件类型 */
|
|
60
|
+
fileType?: string;
|
|
61
|
+
}
|
|
62
|
+
/**
|
|
63
|
+
* 文件类型解析结果
|
|
64
|
+
*/
|
|
65
|
+
export interface ResolvedFileTypes {
|
|
66
|
+
/** MIME 类型列表 */
|
|
67
|
+
mimes: string[];
|
|
68
|
+
/** 扩展名列表 */
|
|
69
|
+
exts: string[];
|
|
70
|
+
/** 需要签名验证的类型 */
|
|
71
|
+
signatureTypes: string[];
|
|
72
|
+
}
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
/** 文件缓冲区接口 */
|
|
2
|
+
export interface FileBuffer {
|
|
3
|
+
/** 文件二进制内容 */
|
|
4
|
+
buffer: Buffer;
|
|
5
|
+
/** 文件大小(字节) */
|
|
6
|
+
size: number;
|
|
7
|
+
/** MIME类型 */
|
|
8
|
+
mimeType: string;
|
|
9
|
+
/** 原始文件名 */
|
|
10
|
+
originalName: string;
|
|
11
|
+
/** 文件编码 */
|
|
12
|
+
encoding?: string;
|
|
13
|
+
/** 文件哈希值(SHA-256) */
|
|
14
|
+
hash?: string;
|
|
15
|
+
/** 文件流(适用于大文件) */
|
|
16
|
+
stream?: NodeJS.ReadableStream;
|
|
17
|
+
/** 分片信息(用于分片上传) */
|
|
18
|
+
chunk?: {
|
|
19
|
+
index: number;
|
|
20
|
+
total: number;
|
|
21
|
+
size: number;
|
|
22
|
+
start: number;
|
|
23
|
+
end: number;
|
|
24
|
+
};
|
|
25
|
+
/** 临时文件路径(用于大文件) */
|
|
26
|
+
tempPath?: string;
|
|
27
|
+
/** 上传进度回调 */
|
|
28
|
+
onProgress?: (progress: UploadProgress) => void;
|
|
29
|
+
}
|
|
30
|
+
/** 上传进度 */
|
|
31
|
+
export interface UploadProgress {
|
|
32
|
+
/** 已上传字节数 */
|
|
33
|
+
loaded: number;
|
|
34
|
+
/** 总字节数 */
|
|
35
|
+
total: number;
|
|
36
|
+
/** 上传百分比 */
|
|
37
|
+
percentage: number;
|
|
38
|
+
/** 上传速度(字节/秒) */
|
|
39
|
+
speed: number;
|
|
40
|
+
/** 剩余时间(秒) */
|
|
41
|
+
eta: number;
|
|
42
|
+
/** 当前状态 */
|
|
43
|
+
status: 'uploading' | 'paused' | 'completed' | 'error';
|
|
44
|
+
/** 错误信息 */
|
|
45
|
+
error?: string;
|
|
46
|
+
}
|
|
47
|
+
/** 文件块 */
|
|
48
|
+
export interface FileChunk {
|
|
49
|
+
/** 块索引 */
|
|
50
|
+
index: number;
|
|
51
|
+
/** 块数据 */
|
|
52
|
+
data: Buffer;
|
|
53
|
+
/** 块大小 */
|
|
54
|
+
size: number;
|
|
55
|
+
/** 块哈希 */
|
|
56
|
+
hash?: string;
|
|
57
|
+
/** 块在文件中的起始位置 */
|
|
58
|
+
offset: number;
|
|
59
|
+
}
|
|
60
|
+
/** 文件流选项 */
|
|
61
|
+
export interface FileStreamOptions {
|
|
62
|
+
/** 分块大小 */
|
|
63
|
+
chunkSize?: number;
|
|
64
|
+
/** 是否使用缓冲 */
|
|
65
|
+
useBuffer?: boolean;
|
|
66
|
+
/** 缓冲区大小 */
|
|
67
|
+
bufferSize?: number;
|
|
68
|
+
/** 是否自动清理临时文件 */
|
|
69
|
+
autoCleanup?: boolean;
|
|
70
|
+
/** 临时文件目录 */
|
|
71
|
+
tempDir?: string;
|
|
72
|
+
}
|