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