Files
Max-Cocos-Demo/extensions/max-studio/source/hotupdate/minio-uploader.ts
2025-10-28 21:55:41 +08:00

230 lines
6.9 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

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