@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,98 @@
|
|
|
1
|
+
import { BatchDeleteResult, CopyResult, HealthStatus, IStorageProvider, ListResult, LocalStorageOptions, MoveResult, PreSignedUploadInfo, UploadResult } from '../interfaces/storage-provider.interface';
|
|
2
|
+
import { FileBuffer } from '../interfaces/file-buffer.interface';
|
|
3
|
+
import { UploadOptions } from '../interfaces/upload-options.interface';
|
|
4
|
+
import { FileMetadata } from '../interfaces/file-metadata.interface';
|
|
5
|
+
/**
|
|
6
|
+
* 本地文件存储提供者
|
|
7
|
+
*/
|
|
8
|
+
export declare class LocalStorageProvider implements IStorageProvider {
|
|
9
|
+
private readonly options;
|
|
10
|
+
name: string;
|
|
11
|
+
private readonly logger;
|
|
12
|
+
private initialized;
|
|
13
|
+
constructor(options: LocalStorageOptions);
|
|
14
|
+
/**
|
|
15
|
+
* 初始化存储配置
|
|
16
|
+
*/
|
|
17
|
+
initialize(): Promise<void>;
|
|
18
|
+
/**
|
|
19
|
+
* 上传文件
|
|
20
|
+
*/
|
|
21
|
+
upload(file: FileBuffer, options: UploadOptions): Promise<UploadResult>;
|
|
22
|
+
/**
|
|
23
|
+
* 下载文件
|
|
24
|
+
*/
|
|
25
|
+
download(filePath: string): Promise<FileBuffer>;
|
|
26
|
+
/**
|
|
27
|
+
* 删除文件
|
|
28
|
+
*/
|
|
29
|
+
delete(filePath: string): Promise<void>;
|
|
30
|
+
/**
|
|
31
|
+
* 批量删除
|
|
32
|
+
*/
|
|
33
|
+
deleteBatch(paths: string[]): Promise<BatchDeleteResult>;
|
|
34
|
+
/**
|
|
35
|
+
* 检查文件是否存在
|
|
36
|
+
*/
|
|
37
|
+
exists(filePath: string): Promise<boolean>;
|
|
38
|
+
/**
|
|
39
|
+
* 获取文件元数据
|
|
40
|
+
*/
|
|
41
|
+
getMetadata(filePath: string): Promise<FileMetadata>;
|
|
42
|
+
/**
|
|
43
|
+
* 获取预签名URL(本地存储不支持,返回直接URL)
|
|
44
|
+
*/
|
|
45
|
+
getSignedUrl(filePath: string, expiresIn?: number): Promise<string>;
|
|
46
|
+
/**
|
|
47
|
+
* 获取预签名上传URL(本地存储不支持)
|
|
48
|
+
*/
|
|
49
|
+
getSignedUploadUrl(filePath: string, expiresIn?: number, contentType?: string): Promise<PreSignedUploadInfo>;
|
|
50
|
+
/**
|
|
51
|
+
* 复制文件
|
|
52
|
+
*/
|
|
53
|
+
copy(sourcePath: string, destinationPath: string): Promise<CopyResult>;
|
|
54
|
+
/**
|
|
55
|
+
* 移动文件
|
|
56
|
+
*/
|
|
57
|
+
move(sourcePath: string, destinationPath: string): Promise<MoveResult>;
|
|
58
|
+
/**
|
|
59
|
+
* 列出文件
|
|
60
|
+
*/
|
|
61
|
+
list(prefix?: string, limit?: number): Promise<ListResult>;
|
|
62
|
+
/**
|
|
63
|
+
* 健康检查
|
|
64
|
+
*/
|
|
65
|
+
healthCheck(): Promise<HealthStatus>;
|
|
66
|
+
/**
|
|
67
|
+
* 生成文件路径
|
|
68
|
+
*/
|
|
69
|
+
private generateFilePath;
|
|
70
|
+
/**
|
|
71
|
+
* 获取文件URL
|
|
72
|
+
*/
|
|
73
|
+
private getFileUrl;
|
|
74
|
+
/**
|
|
75
|
+
* 计算文件哈希
|
|
76
|
+
*/
|
|
77
|
+
private calculateFileHash;
|
|
78
|
+
/**
|
|
79
|
+
* 递归读取目录
|
|
80
|
+
*/
|
|
81
|
+
private readDirRecursive;
|
|
82
|
+
/**
|
|
83
|
+
* 验证路径安全性,防止路径遍历攻击
|
|
84
|
+
*/
|
|
85
|
+
private validatePath;
|
|
86
|
+
/**
|
|
87
|
+
* 验证文件是否在上传目录内
|
|
88
|
+
*/
|
|
89
|
+
private ensurePathWithinUpload;
|
|
90
|
+
/**
|
|
91
|
+
* 根据扩展名获取 MIME 类型
|
|
92
|
+
*/
|
|
93
|
+
private getMimeTypeFromExtension;
|
|
94
|
+
/**
|
|
95
|
+
* 确保已初始化
|
|
96
|
+
*/
|
|
97
|
+
private ensureInitialized;
|
|
98
|
+
}
|
|
@@ -0,0 +1,484 @@
|
|
|
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 __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) {
|
|
12
|
+
function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); }
|
|
13
|
+
return new (P || (P = Promise))(function (resolve, reject) {
|
|
14
|
+
function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }
|
|
15
|
+
function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } }
|
|
16
|
+
function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); }
|
|
17
|
+
step((generator = generator.apply(thisArg, _arguments || [])).next());
|
|
18
|
+
});
|
|
19
|
+
};
|
|
20
|
+
var LocalStorageProvider_1;
|
|
21
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
22
|
+
exports.LocalStorageProvider = void 0;
|
|
23
|
+
const common_1 = require("@nestjs/common");
|
|
24
|
+
const file_upload_exception_1 = require("../exceptions/file-upload.exception");
|
|
25
|
+
const checksum_util_1 = require("../utils/checksum.util");
|
|
26
|
+
const fs = require("fs-extra");
|
|
27
|
+
const path = require("path");
|
|
28
|
+
const crypto = require("crypto");
|
|
29
|
+
/**
|
|
30
|
+
* 本地文件存储提供者
|
|
31
|
+
*/
|
|
32
|
+
let LocalStorageProvider = LocalStorageProvider_1 = class LocalStorageProvider {
|
|
33
|
+
constructor(options) {
|
|
34
|
+
this.options = options;
|
|
35
|
+
this.name = 'local';
|
|
36
|
+
this.logger = new common_1.Logger(LocalStorageProvider_1.name);
|
|
37
|
+
this.initialized = false;
|
|
38
|
+
}
|
|
39
|
+
/**
|
|
40
|
+
* 初始化存储配置
|
|
41
|
+
*/
|
|
42
|
+
initialize() {
|
|
43
|
+
return __awaiter(this, void 0, void 0, function* () {
|
|
44
|
+
if (this.initialized) {
|
|
45
|
+
return;
|
|
46
|
+
}
|
|
47
|
+
// 确保上传目录存在
|
|
48
|
+
yield fs.ensureDir(this.options.uploadPath);
|
|
49
|
+
// 设置目录权限
|
|
50
|
+
if (this.options.dirMode) {
|
|
51
|
+
yield fs.chmod(this.options.uploadPath, parseInt(this.options.dirMode, 8));
|
|
52
|
+
}
|
|
53
|
+
this.initialized = true;
|
|
54
|
+
this.logger.log(`Local storage initialized at: ${this.options.uploadPath}`);
|
|
55
|
+
});
|
|
56
|
+
}
|
|
57
|
+
/**
|
|
58
|
+
* 上传文件
|
|
59
|
+
*/
|
|
60
|
+
upload(file, options) {
|
|
61
|
+
return __awaiter(this, void 0, void 0, function* () {
|
|
62
|
+
yield this.ensureInitialized();
|
|
63
|
+
// 生成文件路径
|
|
64
|
+
const filePath = yield this.generateFilePath(file, options);
|
|
65
|
+
const fullPath = path.join(this.options.uploadPath, filePath);
|
|
66
|
+
// 确保目录存在
|
|
67
|
+
yield fs.ensureDir(path.dirname(fullPath));
|
|
68
|
+
// 写入文件
|
|
69
|
+
yield fs.writeFile(fullPath, file.buffer);
|
|
70
|
+
// 设置文件权限
|
|
71
|
+
if (this.options.fileMode) {
|
|
72
|
+
yield fs.chmod(fullPath, parseInt(this.options.fileMode, 8));
|
|
73
|
+
}
|
|
74
|
+
// 计算文件哈希
|
|
75
|
+
const hash = yield this.calculateFileHash(fullPath);
|
|
76
|
+
// 获取文件统计信息
|
|
77
|
+
const stats = yield fs.stat(fullPath);
|
|
78
|
+
return {
|
|
79
|
+
key: filePath,
|
|
80
|
+
url: this.getFileUrl(filePath),
|
|
81
|
+
etag: hash,
|
|
82
|
+
size: stats.size,
|
|
83
|
+
uploadedAt: new Date(),
|
|
84
|
+
metadata: {
|
|
85
|
+
originalName: file.originalName,
|
|
86
|
+
mimeType: file.mimeType,
|
|
87
|
+
hash,
|
|
88
|
+
},
|
|
89
|
+
};
|
|
90
|
+
});
|
|
91
|
+
}
|
|
92
|
+
/**
|
|
93
|
+
* 下载文件
|
|
94
|
+
*/
|
|
95
|
+
download(filePath) {
|
|
96
|
+
return __awaiter(this, void 0, void 0, function* () {
|
|
97
|
+
yield this.ensureInitialized();
|
|
98
|
+
// 验证路径安全性
|
|
99
|
+
const normalizedPath = this.validatePath(filePath);
|
|
100
|
+
// 构建完整路径
|
|
101
|
+
const fullPath = path.join(this.options.uploadPath, normalizedPath);
|
|
102
|
+
// 确保最终路径在上传目录内
|
|
103
|
+
this.ensurePathWithinUpload(fullPath);
|
|
104
|
+
try {
|
|
105
|
+
// 直接尝试读取文件,避免 TOCTOU 竞态条件
|
|
106
|
+
const buffer = yield fs.readFile(fullPath);
|
|
107
|
+
const stats = yield fs.stat(fullPath);
|
|
108
|
+
// 根据扩展名猜测 MIME 类型
|
|
109
|
+
const mimeType = this.getMimeTypeFromExtension(normalizedPath);
|
|
110
|
+
return {
|
|
111
|
+
buffer,
|
|
112
|
+
size: stats.size,
|
|
113
|
+
mimeType,
|
|
114
|
+
originalName: path.basename(normalizedPath),
|
|
115
|
+
};
|
|
116
|
+
}
|
|
117
|
+
catch (error) {
|
|
118
|
+
if (error.code === 'ENOENT') {
|
|
119
|
+
throw new file_upload_exception_1.StorageException('download', new Error(`File not found: ${filePath}`));
|
|
120
|
+
}
|
|
121
|
+
throw new file_upload_exception_1.StorageException('download', error);
|
|
122
|
+
}
|
|
123
|
+
});
|
|
124
|
+
}
|
|
125
|
+
/**
|
|
126
|
+
* 删除文件
|
|
127
|
+
*/
|
|
128
|
+
delete(filePath) {
|
|
129
|
+
return __awaiter(this, void 0, void 0, function* () {
|
|
130
|
+
yield this.ensureInitialized();
|
|
131
|
+
const fullPath = path.join(this.options.uploadPath, filePath);
|
|
132
|
+
if (yield fs.pathExists(fullPath)) {
|
|
133
|
+
yield fs.unlink(fullPath);
|
|
134
|
+
this.logger.log(`File deleted: ${filePath}`);
|
|
135
|
+
}
|
|
136
|
+
});
|
|
137
|
+
}
|
|
138
|
+
/**
|
|
139
|
+
* 批量删除
|
|
140
|
+
*/
|
|
141
|
+
deleteBatch(paths) {
|
|
142
|
+
return __awaiter(this, void 0, void 0, function* () {
|
|
143
|
+
yield this.ensureInitialized();
|
|
144
|
+
const deleted = [];
|
|
145
|
+
const failed = [];
|
|
146
|
+
for (const filePath of paths) {
|
|
147
|
+
try {
|
|
148
|
+
yield this.delete(filePath);
|
|
149
|
+
deleted.push(filePath);
|
|
150
|
+
}
|
|
151
|
+
catch (error) {
|
|
152
|
+
failed.push({
|
|
153
|
+
path: filePath,
|
|
154
|
+
error: error.message,
|
|
155
|
+
});
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
return {
|
|
159
|
+
deleted,
|
|
160
|
+
failed,
|
|
161
|
+
total: paths.length,
|
|
162
|
+
};
|
|
163
|
+
});
|
|
164
|
+
}
|
|
165
|
+
/**
|
|
166
|
+
* 检查文件是否存在
|
|
167
|
+
*/
|
|
168
|
+
exists(filePath) {
|
|
169
|
+
return __awaiter(this, void 0, void 0, function* () {
|
|
170
|
+
yield this.ensureInitialized();
|
|
171
|
+
const fullPath = path.join(this.options.uploadPath, filePath);
|
|
172
|
+
return fs.pathExists(fullPath);
|
|
173
|
+
});
|
|
174
|
+
}
|
|
175
|
+
/**
|
|
176
|
+
* 获取文件元数据
|
|
177
|
+
*/
|
|
178
|
+
getMetadata(filePath) {
|
|
179
|
+
return __awaiter(this, void 0, void 0, function* () {
|
|
180
|
+
yield this.ensureInitialized();
|
|
181
|
+
const fullPath = path.join(this.options.uploadPath, filePath);
|
|
182
|
+
if (!(yield fs.pathExists(fullPath))) {
|
|
183
|
+
throw new file_upload_exception_1.StorageException('getMetadata', new Error(`File not found: ${filePath}`));
|
|
184
|
+
}
|
|
185
|
+
const stats = yield fs.stat(fullPath);
|
|
186
|
+
const hash = yield this.calculateFileHash(fullPath);
|
|
187
|
+
return {
|
|
188
|
+
size: stats.size,
|
|
189
|
+
mimeType: this.getMimeTypeFromExtension(filePath),
|
|
190
|
+
lastModified: stats.mtime,
|
|
191
|
+
etag: hash,
|
|
192
|
+
extension: path.extname(filePath),
|
|
193
|
+
metadata: {
|
|
194
|
+
created: stats.birthtime,
|
|
195
|
+
modified: stats.mtime,
|
|
196
|
+
accessed: stats.atime,
|
|
197
|
+
},
|
|
198
|
+
};
|
|
199
|
+
});
|
|
200
|
+
}
|
|
201
|
+
/**
|
|
202
|
+
* 获取预签名URL(本地存储不支持,返回直接URL)
|
|
203
|
+
*/
|
|
204
|
+
getSignedUrl(filePath, expiresIn) {
|
|
205
|
+
return __awaiter(this, void 0, void 0, function* () {
|
|
206
|
+
return this.getFileUrl(filePath);
|
|
207
|
+
});
|
|
208
|
+
}
|
|
209
|
+
/**
|
|
210
|
+
* 获取预签名上传URL(本地存储不支持)
|
|
211
|
+
*/
|
|
212
|
+
getSignedUploadUrl(filePath, expiresIn, contentType) {
|
|
213
|
+
return __awaiter(this, void 0, void 0, function* () {
|
|
214
|
+
throw new file_upload_exception_1.StorageException('getSignedUploadUrl', new Error('Pre-signed upload URL not supported for local storage'));
|
|
215
|
+
});
|
|
216
|
+
}
|
|
217
|
+
/**
|
|
218
|
+
* 复制文件
|
|
219
|
+
*/
|
|
220
|
+
copy(sourcePath, destinationPath) {
|
|
221
|
+
return __awaiter(this, void 0, void 0, function* () {
|
|
222
|
+
yield this.ensureInitialized();
|
|
223
|
+
const sourceFullPath = path.join(this.options.uploadPath, sourcePath);
|
|
224
|
+
const destFullPath = path.join(this.options.uploadPath, destinationPath);
|
|
225
|
+
if (!(yield fs.pathExists(sourceFullPath))) {
|
|
226
|
+
throw new file_upload_exception_1.StorageException('copy', new Error(`Source file not found: ${sourcePath}`));
|
|
227
|
+
}
|
|
228
|
+
// 确保目标目录存在
|
|
229
|
+
yield fs.ensureDir(path.dirname(destFullPath));
|
|
230
|
+
// 复制文件
|
|
231
|
+
yield fs.copy(sourceFullPath, destFullPath);
|
|
232
|
+
const stats = yield fs.stat(destFullPath);
|
|
233
|
+
const hash = yield this.calculateFileHash(destFullPath);
|
|
234
|
+
return {
|
|
235
|
+
destinationPath,
|
|
236
|
+
size: stats.size,
|
|
237
|
+
copiedAt: new Date(),
|
|
238
|
+
etag: hash,
|
|
239
|
+
};
|
|
240
|
+
});
|
|
241
|
+
}
|
|
242
|
+
/**
|
|
243
|
+
* 移动文件
|
|
244
|
+
*/
|
|
245
|
+
move(sourcePath, destinationPath) {
|
|
246
|
+
return __awaiter(this, void 0, void 0, function* () {
|
|
247
|
+
yield this.ensureInitialized();
|
|
248
|
+
const sourceFullPath = path.join(this.options.uploadPath, sourcePath);
|
|
249
|
+
const destFullPath = path.join(this.options.uploadPath, destinationPath);
|
|
250
|
+
if (!(yield fs.pathExists(sourceFullPath))) {
|
|
251
|
+
throw new file_upload_exception_1.StorageException('move', new Error(`Source file not found: ${sourcePath}`));
|
|
252
|
+
}
|
|
253
|
+
// 确保目标目录存在
|
|
254
|
+
yield fs.ensureDir(path.dirname(destFullPath));
|
|
255
|
+
// 移动文件
|
|
256
|
+
yield fs.move(sourceFullPath, destFullPath);
|
|
257
|
+
const stats = yield fs.stat(destFullPath);
|
|
258
|
+
const hash = yield this.calculateFileHash(destFullPath);
|
|
259
|
+
return {
|
|
260
|
+
destinationPath,
|
|
261
|
+
size: stats.size,
|
|
262
|
+
movedAt: new Date(),
|
|
263
|
+
etag: hash,
|
|
264
|
+
};
|
|
265
|
+
});
|
|
266
|
+
}
|
|
267
|
+
/**
|
|
268
|
+
* 列出文件
|
|
269
|
+
*/
|
|
270
|
+
list(prefix, limit) {
|
|
271
|
+
return __awaiter(this, void 0, void 0, function* () {
|
|
272
|
+
yield this.ensureInitialized();
|
|
273
|
+
const searchPath = prefix
|
|
274
|
+
? path.join(this.options.uploadPath, prefix)
|
|
275
|
+
: this.options.uploadPath;
|
|
276
|
+
const files = [];
|
|
277
|
+
if (yield fs.pathExists(searchPath)) {
|
|
278
|
+
const entries = yield this.readDirRecursive(searchPath);
|
|
279
|
+
for (const entry of entries) {
|
|
280
|
+
const relativePath = path.relative(this.options.uploadPath, entry);
|
|
281
|
+
const stats = yield fs.stat(entry);
|
|
282
|
+
if (stats.isFile()) {
|
|
283
|
+
const hash = yield this.calculateFileHash(entry);
|
|
284
|
+
files.push({
|
|
285
|
+
key: relativePath,
|
|
286
|
+
size: stats.size,
|
|
287
|
+
lastModified: stats.mtime,
|
|
288
|
+
etag: hash,
|
|
289
|
+
});
|
|
290
|
+
if (limit && files.length >= limit) {
|
|
291
|
+
break;
|
|
292
|
+
}
|
|
293
|
+
}
|
|
294
|
+
}
|
|
295
|
+
}
|
|
296
|
+
return {
|
|
297
|
+
files,
|
|
298
|
+
hasMore: false,
|
|
299
|
+
totalCount: files.length,
|
|
300
|
+
};
|
|
301
|
+
});
|
|
302
|
+
}
|
|
303
|
+
/**
|
|
304
|
+
* 健康检查
|
|
305
|
+
*/
|
|
306
|
+
healthCheck() {
|
|
307
|
+
return __awaiter(this, void 0, void 0, function* () {
|
|
308
|
+
const startTime = Date.now();
|
|
309
|
+
try {
|
|
310
|
+
yield this.ensureInitialized();
|
|
311
|
+
// 检查目录是否可写
|
|
312
|
+
const testFile = path.join(this.options.uploadPath, '.health-check');
|
|
313
|
+
yield fs.writeFile(testFile, 'test');
|
|
314
|
+
yield fs.unlink(testFile);
|
|
315
|
+
return {
|
|
316
|
+
healthy: true,
|
|
317
|
+
responseTime: Date.now() - startTime,
|
|
318
|
+
details: {
|
|
319
|
+
uploadPath: this.options.uploadPath,
|
|
320
|
+
writable: true,
|
|
321
|
+
},
|
|
322
|
+
};
|
|
323
|
+
}
|
|
324
|
+
catch (error) {
|
|
325
|
+
return {
|
|
326
|
+
healthy: false,
|
|
327
|
+
responseTime: Date.now() - startTime,
|
|
328
|
+
error: error.message,
|
|
329
|
+
details: {
|
|
330
|
+
uploadPath: this.options.uploadPath,
|
|
331
|
+
writable: false,
|
|
332
|
+
},
|
|
333
|
+
};
|
|
334
|
+
}
|
|
335
|
+
});
|
|
336
|
+
}
|
|
337
|
+
/**
|
|
338
|
+
* 生成文件路径
|
|
339
|
+
*/
|
|
340
|
+
generateFilePath(file, options) {
|
|
341
|
+
return __awaiter(this, void 0, void 0, function* () {
|
|
342
|
+
const ext = path.extname(file.originalName);
|
|
343
|
+
const basename = path.basename(file.originalName, ext);
|
|
344
|
+
// 生成文件名
|
|
345
|
+
const filename = options.filename
|
|
346
|
+
? `${options.filename}${ext}`
|
|
347
|
+
: `${basename}-${Date.now()}${ext}`;
|
|
348
|
+
// 根据配置生成子目录
|
|
349
|
+
let subdirectory = '';
|
|
350
|
+
if (this.options.subdirectoryFormat) {
|
|
351
|
+
switch (this.options.subdirectoryFormat) {
|
|
352
|
+
case 'date': {
|
|
353
|
+
const now = new Date();
|
|
354
|
+
const year = now.getFullYear();
|
|
355
|
+
const month = String(now.getMonth() + 1).padStart(2, '0');
|
|
356
|
+
const day = String(now.getDate()).padStart(2, '0');
|
|
357
|
+
subdirectory = `${year}/${month}/${day}`;
|
|
358
|
+
break;
|
|
359
|
+
}
|
|
360
|
+
case 'hash': {
|
|
361
|
+
const hash = crypto.createHash('md5').update(filename).digest('hex');
|
|
362
|
+
subdirectory = `${hash.substring(0, 2)}/${hash.substring(2, 4)}`;
|
|
363
|
+
break;
|
|
364
|
+
}
|
|
365
|
+
case 'none':
|
|
366
|
+
default:
|
|
367
|
+
break;
|
|
368
|
+
}
|
|
369
|
+
}
|
|
370
|
+
// 组合路径
|
|
371
|
+
const relativePath = options.path || '';
|
|
372
|
+
return path.join(relativePath, subdirectory, filename);
|
|
373
|
+
});
|
|
374
|
+
}
|
|
375
|
+
/**
|
|
376
|
+
* 获取文件URL
|
|
377
|
+
*/
|
|
378
|
+
getFileUrl(filePath) {
|
|
379
|
+
if (this.options.baseUrl) {
|
|
380
|
+
return `${this.options.baseUrl}/${filePath}`;
|
|
381
|
+
}
|
|
382
|
+
return filePath;
|
|
383
|
+
}
|
|
384
|
+
/**
|
|
385
|
+
* 计算文件哈希
|
|
386
|
+
*/
|
|
387
|
+
calculateFileHash(fullPath) {
|
|
388
|
+
return __awaiter(this, void 0, void 0, function* () {
|
|
389
|
+
return checksum_util_1.ChecksumUtil.calculate(fullPath, 'md5');
|
|
390
|
+
});
|
|
391
|
+
}
|
|
392
|
+
/**
|
|
393
|
+
* 递归读取目录
|
|
394
|
+
*/
|
|
395
|
+
readDirRecursive(dir) {
|
|
396
|
+
return __awaiter(this, void 0, void 0, function* () {
|
|
397
|
+
const entries = yield fs.readdir(dir, { withFileTypes: true });
|
|
398
|
+
const files = [];
|
|
399
|
+
for (const entry of entries) {
|
|
400
|
+
const fullPath = path.join(dir, entry.name);
|
|
401
|
+
if (entry.isDirectory()) {
|
|
402
|
+
const subFiles = yield this.readDirRecursive(fullPath);
|
|
403
|
+
files.push(...subFiles);
|
|
404
|
+
}
|
|
405
|
+
else {
|
|
406
|
+
files.push(fullPath);
|
|
407
|
+
}
|
|
408
|
+
}
|
|
409
|
+
return files;
|
|
410
|
+
});
|
|
411
|
+
}
|
|
412
|
+
/**
|
|
413
|
+
* 验证路径安全性,防止路径遍历攻击
|
|
414
|
+
*/
|
|
415
|
+
validatePath(filePath) {
|
|
416
|
+
// 规范化路径
|
|
417
|
+
const normalizedPath = path.normalize(filePath);
|
|
418
|
+
// 检查是否为绝对路径
|
|
419
|
+
if (path.isAbsolute(normalizedPath)) {
|
|
420
|
+
throw new file_upload_exception_1.StorageException('download', new Error('Absolute paths not allowed'));
|
|
421
|
+
}
|
|
422
|
+
// 检查是否包含向上目录遍历
|
|
423
|
+
if (normalizedPath.includes('..')) {
|
|
424
|
+
throw new file_upload_exception_1.StorageException('download', new Error('Path traversal not allowed'));
|
|
425
|
+
}
|
|
426
|
+
// 确保路径不包含危险字符
|
|
427
|
+
if (normalizedPath.includes('\0')) {
|
|
428
|
+
throw new file_upload_exception_1.StorageException('download', new Error('Null character in path'));
|
|
429
|
+
}
|
|
430
|
+
return normalizedPath;
|
|
431
|
+
}
|
|
432
|
+
/**
|
|
433
|
+
* 验证文件是否在上传目录内
|
|
434
|
+
*/
|
|
435
|
+
ensurePathWithinUpload(fullPath) {
|
|
436
|
+
const resolvedUploadPath = path.resolve(this.options.uploadPath);
|
|
437
|
+
const resolvedFullPath = path.resolve(fullPath);
|
|
438
|
+
if (!resolvedFullPath.startsWith(resolvedUploadPath)) {
|
|
439
|
+
throw new file_upload_exception_1.StorageException('download', new Error('Path traversal detected: file is outside upload directory'));
|
|
440
|
+
}
|
|
441
|
+
}
|
|
442
|
+
/**
|
|
443
|
+
* 根据扩展名获取 MIME 类型
|
|
444
|
+
*/
|
|
445
|
+
getMimeTypeFromExtension(filename) {
|
|
446
|
+
const ext = path.extname(filename).toLowerCase();
|
|
447
|
+
const mimeTypes = {
|
|
448
|
+
'.jpg': 'image/jpeg',
|
|
449
|
+
'.jpeg': 'image/jpeg',
|
|
450
|
+
'.png': 'image/png',
|
|
451
|
+
'.gif': 'image/gif',
|
|
452
|
+
'.webp': 'image/webp',
|
|
453
|
+
'.svg': 'image/svg+xml',
|
|
454
|
+
'.pdf': 'application/pdf',
|
|
455
|
+
'.doc': 'application/msword',
|
|
456
|
+
'.docx': 'application/vnd.openxmlformats-officedocument.wordprocessingml.document',
|
|
457
|
+
'.xls': 'application/vnd.ms-excel',
|
|
458
|
+
'.xlsx': 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet',
|
|
459
|
+
'.txt': 'text/plain',
|
|
460
|
+
'.csv': 'text/csv',
|
|
461
|
+
'.json': 'application/json',
|
|
462
|
+
'.xml': 'application/xml',
|
|
463
|
+
'.zip': 'application/zip',
|
|
464
|
+
'.mp4': 'video/mp4',
|
|
465
|
+
'.mp3': 'audio/mpeg',
|
|
466
|
+
};
|
|
467
|
+
return mimeTypes[ext] || 'application/octet-stream';
|
|
468
|
+
}
|
|
469
|
+
/**
|
|
470
|
+
* 确保已初始化
|
|
471
|
+
*/
|
|
472
|
+
ensureInitialized() {
|
|
473
|
+
return __awaiter(this, void 0, void 0, function* () {
|
|
474
|
+
if (!this.initialized) {
|
|
475
|
+
yield this.initialize();
|
|
476
|
+
}
|
|
477
|
+
});
|
|
478
|
+
}
|
|
479
|
+
};
|
|
480
|
+
exports.LocalStorageProvider = LocalStorageProvider;
|
|
481
|
+
exports.LocalStorageProvider = LocalStorageProvider = LocalStorageProvider_1 = __decorate([
|
|
482
|
+
(0, common_1.Injectable)(),
|
|
483
|
+
__metadata("design:paramtypes", [Object])
|
|
484
|
+
], LocalStorageProvider);
|
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
import { BatchDeleteResult, CopyResult, HealthStatus, IStorageProvider, ListResult, MoveResult, PreSignedUploadInfo, S3StorageOptions, UploadResult } from '../interfaces/storage-provider.interface';
|
|
2
|
+
import { FileBuffer } from '../interfaces/file-buffer.interface';
|
|
3
|
+
import { UploadOptions } from '../interfaces/upload-options.interface';
|
|
4
|
+
import { FileMetadata } from '../interfaces/file-metadata.interface';
|
|
5
|
+
/**
|
|
6
|
+
* AWS S3 存储提供者
|
|
7
|
+
* 使用动态导入,只在实际使用时加载 AWS SDK
|
|
8
|
+
*/
|
|
9
|
+
export declare class S3StorageProvider implements IStorageProvider {
|
|
10
|
+
private readonly options;
|
|
11
|
+
name: string;
|
|
12
|
+
private readonly logger;
|
|
13
|
+
private initialized;
|
|
14
|
+
private s3Client;
|
|
15
|
+
private S3Commands;
|
|
16
|
+
private s3GetSignedUrl;
|
|
17
|
+
constructor(options: S3StorageOptions);
|
|
18
|
+
/**
|
|
19
|
+
* 初始化存储配置
|
|
20
|
+
* 动态导入 AWS S3 SDK
|
|
21
|
+
*/
|
|
22
|
+
initialize(): Promise<void>;
|
|
23
|
+
/**
|
|
24
|
+
* 上传文件
|
|
25
|
+
*/
|
|
26
|
+
upload(file: FileBuffer, options: UploadOptions): Promise<UploadResult>;
|
|
27
|
+
/**
|
|
28
|
+
* 下载文件
|
|
29
|
+
*/
|
|
30
|
+
download(key: string): Promise<FileBuffer>;
|
|
31
|
+
/**
|
|
32
|
+
* 删除文件
|
|
33
|
+
*/
|
|
34
|
+
delete(key: string): Promise<void>;
|
|
35
|
+
/**
|
|
36
|
+
* 批量删除
|
|
37
|
+
*/
|
|
38
|
+
deleteBatch(keys: string[]): Promise<BatchDeleteResult>;
|
|
39
|
+
/**
|
|
40
|
+
* 检查文件是否存在
|
|
41
|
+
*/
|
|
42
|
+
exists(key: string): Promise<boolean>;
|
|
43
|
+
/**
|
|
44
|
+
* 获取文件元数据
|
|
45
|
+
*/
|
|
46
|
+
getMetadata(key: string): Promise<FileMetadata>;
|
|
47
|
+
/**
|
|
48
|
+
* 获取预签名URL(下载)
|
|
49
|
+
*/
|
|
50
|
+
getSignedUrl(key: string, expiresIn?: number): Promise<string>;
|
|
51
|
+
/**
|
|
52
|
+
* 获取预签名上传URL
|
|
53
|
+
*/
|
|
54
|
+
getSignedUploadUrl(key: string, expiresIn?: number, contentType?: string): Promise<PreSignedUploadInfo>;
|
|
55
|
+
/**
|
|
56
|
+
* 复制文件
|
|
57
|
+
*/
|
|
58
|
+
copy(sourceKey: string, destinationKey: string): Promise<CopyResult>;
|
|
59
|
+
/**
|
|
60
|
+
* 移动文件(复制后删除)
|
|
61
|
+
*/
|
|
62
|
+
move(sourceKey: string, destinationKey: string): Promise<MoveResult>;
|
|
63
|
+
/**
|
|
64
|
+
* 列出文件
|
|
65
|
+
*/
|
|
66
|
+
list(prefix?: string, limit?: number): Promise<ListResult>;
|
|
67
|
+
/**
|
|
68
|
+
* 健康检查
|
|
69
|
+
*/
|
|
70
|
+
healthCheck(): Promise<HealthStatus>;
|
|
71
|
+
/**
|
|
72
|
+
* 生成文件 Key
|
|
73
|
+
*/
|
|
74
|
+
private generateKey;
|
|
75
|
+
/**
|
|
76
|
+
* 获取文件访问 URL
|
|
77
|
+
*/
|
|
78
|
+
private getFileUrl;
|
|
79
|
+
/**
|
|
80
|
+
* 将流转换为 Buffer(带超时和大小限制)
|
|
81
|
+
*/
|
|
82
|
+
private streamToBuffer;
|
|
83
|
+
/**
|
|
84
|
+
* 确保已初始化
|
|
85
|
+
*/
|
|
86
|
+
private ensureInitialized;
|
|
87
|
+
}
|