230 lines
6.9 KiB
TypeScript
230 lines
6.9 KiB
TypeScript
import { createHash } from "crypto";
|
||
import fs from "fs";
|
||
import path from "path";
|
||
|
||
/**
|
||
* MinIO上传配置接口
|
||
*/
|
||
export interface MinIOConfig {
|
||
/** MinIO服务器地址 */
|
||
endpoint: string;
|
||
/** 访问密钥ID */
|
||
accessKeyId: string;
|
||
/** 访问密钥Secret */
|
||
secretAccessKey: string;
|
||
/** 存储桶名称 */
|
||
bucketName: string;
|
||
/** 是否使用SSL */
|
||
useSSL: boolean;
|
||
/** 上传路径前缀 */
|
||
pathPrefix?: string;
|
||
}
|
||
|
||
/**
|
||
* 上传结果接口
|
||
*/
|
||
export interface UploadResult {
|
||
/** 是否成功 */
|
||
success: boolean;
|
||
/** 错误信息 */
|
||
error?: string;
|
||
/** 上传的文件列表 */
|
||
uploadedFiles: string[];
|
||
}
|
||
|
||
/**
|
||
* MinIO上传器类
|
||
*/
|
||
export class MinIOUploader {
|
||
private config: MinIOConfig;
|
||
|
||
constructor(config: MinIOConfig) {
|
||
this.config = config;
|
||
}
|
||
|
||
/**
|
||
* 验证MinIO配置
|
||
*/
|
||
private validateConfig(): boolean {
|
||
const required = ['endpoint', 'accessKeyId', 'secretAccessKey', 'bucketName'];
|
||
for (const field of required) {
|
||
if (!this.config[field as keyof MinIOConfig] ||
|
||
String(this.config[field as keyof MinIOConfig]).trim() === '') {
|
||
console.error(`MinIO配置错误: ${field} 不能为空`);
|
||
return false;
|
||
}
|
||
}
|
||
return true;
|
||
}
|
||
|
||
/**
|
||
* 上传目录到MinIO
|
||
* @param localDir 本地目录路径
|
||
* @param remotePrefix 远程路径前缀
|
||
* @returns 上传结果
|
||
*/
|
||
async uploadDirectory(localDir: string, remotePrefix: string = ''): Promise<UploadResult> {
|
||
const result: UploadResult = {
|
||
success: false,
|
||
uploadedFiles: []
|
||
};
|
||
|
||
try {
|
||
// 验证配置
|
||
if (!this.validateConfig()) {
|
||
result.error = 'MinIO配置验证失败';
|
||
return result;
|
||
}
|
||
|
||
// 验证本地目录是否存在
|
||
if (!fs.existsSync(localDir)) {
|
||
result.error = `本地目录不存在: ${localDir}`;
|
||
return result;
|
||
}
|
||
|
||
console.log(`开始上传目录到MinIO: ${localDir} -> ${remotePrefix}`);
|
||
|
||
// 动态导入minio模块
|
||
const MinIO = await this.loadMinIOModule();
|
||
if (!MinIO) {
|
||
result.error = 'MinIO模块加载失败,请确保已安装minio依赖';
|
||
return result;
|
||
}
|
||
|
||
// 创建MinIO客户端
|
||
const minioClient = new MinIO.Client({
|
||
endPoint: this.config.endpoint.replace(/^https?:\/\//, ''),
|
||
port: this.getPortFromEndpoint(),
|
||
useSSL: this.config.useSSL,
|
||
accessKey: this.config.accessKeyId,
|
||
secretKey: this.config.secretAccessKey
|
||
});
|
||
|
||
// 检查存储桶是否存在
|
||
const bucketExists = await minioClient.bucketExists(this.config.bucketName);
|
||
if (!bucketExists) {
|
||
console.log(`存储桶不存在,尝试创建: ${this.config.bucketName}`);
|
||
await minioClient.makeBucket(this.config.bucketName);
|
||
}
|
||
|
||
// 获取所有文件
|
||
const files = this.getAllFiles(localDir);
|
||
console.log(`找到 ${files.length} 个文件需要上传`);
|
||
|
||
// 上传文件
|
||
for (const filePath of files) {
|
||
try {
|
||
const relativePath = path.relative(localDir, filePath);
|
||
const objectName = this.buildObjectName(remotePrefix, relativePath);
|
||
|
||
console.log(`上传文件: ${filePath} -> ${objectName}`);
|
||
|
||
await minioClient.fPutObject(this.config.bucketName, objectName, filePath);
|
||
result.uploadedFiles.push(objectName);
|
||
|
||
console.log(`文件上传成功: ${objectName}`);
|
||
} catch (fileError) {
|
||
console.error(`文件上传失败: ${filePath}`, fileError);
|
||
// 继续上传其他文件,不中断整个流程
|
||
}
|
||
}
|
||
|
||
result.success = result.uploadedFiles.length > 0;
|
||
if (!result.success) {
|
||
result.error = '没有文件上传成功';
|
||
}
|
||
|
||
console.log(`MinIO上传完成,成功上传 ${result.uploadedFiles.length} 个文件`);
|
||
|
||
} catch (error) {
|
||
console.error('MinIO上传过程中发生错误:', error);
|
||
result.error = error instanceof Error ? error.message : String(error);
|
||
}
|
||
|
||
return result;
|
||
}
|
||
|
||
/**
|
||
* 动态加载MinIO模块
|
||
*/
|
||
private async loadMinIOModule(): Promise<any> {
|
||
try {
|
||
// 尝试动态导入minio模块
|
||
const minio = await import('minio');
|
||
return minio;
|
||
} catch (error) {
|
||
console.error('MinIO模块导入失败:', error);
|
||
console.log('请运行以下命令安装MinIO依赖:');
|
||
console.log('npm install minio');
|
||
return null;
|
||
}
|
||
}
|
||
|
||
/**
|
||
* 从endpoint中提取端口号
|
||
*/
|
||
private getPortFromEndpoint(): number | undefined {
|
||
try {
|
||
const url = new URL(this.config.endpoint.startsWith('http') ?
|
||
this.config.endpoint : `http://${this.config.endpoint}`);
|
||
return url.port ? parseInt(url.port) : (this.config.useSSL ? 443 : 80);
|
||
} catch {
|
||
return this.config.useSSL ? 443 : 80;
|
||
}
|
||
}
|
||
|
||
/**
|
||
* 构建对象名称
|
||
*/
|
||
private buildObjectName(remotePrefix: string, relativePath: string): string {
|
||
const prefix = this.config.pathPrefix || '';
|
||
const parts = [prefix, remotePrefix, relativePath].filter(part => part && part.trim() !== '');
|
||
return parts.join('/').replace(/\/+/g, '/');
|
||
}
|
||
|
||
/**
|
||
* 递归获取目录下的所有文件
|
||
*/
|
||
private getAllFiles(dir: string): string[] {
|
||
const files: string[] = [];
|
||
|
||
const traverse = (currentDir: string) => {
|
||
const items = fs.readdirSync(currentDir);
|
||
|
||
for (const item of items) {
|
||
// 跳过以点开头的隐藏文件和隐藏目录
|
||
if (item.startsWith('.')) {
|
||
continue;
|
||
}
|
||
|
||
const fullPath = path.join(currentDir, item);
|
||
const stat = fs.statSync(fullPath);
|
||
|
||
if (stat.isDirectory()) {
|
||
traverse(fullPath);
|
||
} else if (stat.isFile()) {
|
||
files.push(fullPath);
|
||
}
|
||
}
|
||
};
|
||
|
||
traverse(dir);
|
||
return files;
|
||
}
|
||
|
||
/**
|
||
* 计算文件MD5
|
||
*/
|
||
private calculateFileMD5(filePath: string): string {
|
||
const content = fs.readFileSync(filePath);
|
||
return createHash('md5').update(content).digest('hex');
|
||
}
|
||
}
|
||
|
||
/**
|
||
* 创建MinIO上传器实例
|
||
*/
|
||
export function createMinIOUploader(config: MinIOConfig): MinIOUploader {
|
||
return new MinIOUploader(config);
|
||
}
|