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);
 | 
						||
}
 |