feat: 提交资源
This commit is contained in:
157
extensions/max-studio/source/hotupdate/builder.ts
Normal file
157
extensions/max-studio/source/hotupdate/builder.ts
Normal file
@@ -0,0 +1,157 @@
|
||||
/* eslint-disable @typescript-eslint/naming-convention */
|
||||
|
||||
import { BuildPlugin } from "@cocos/creator-types/editor/packages/builder/@types/public";
|
||||
|
||||
const options = {
|
||||
hotupdatePluginCheck: {
|
||||
label: 'i18n:max-framework.hotupdate.hotupdatecheck',
|
||||
description: "hotupdate checkbox",
|
||||
default: false,
|
||||
render: {
|
||||
ui: 'ui-checkbox',
|
||||
attributes: {
|
||||
}
|
||||
}
|
||||
},
|
||||
configInput: {
|
||||
label: "i18n:max-framework.hotupdate.hotupdatecfg",
|
||||
description: "hotupdate config file path",
|
||||
extensions: ['json'],
|
||||
default: 'project://hotupdate-config.json',
|
||||
render: {
|
||||
ui: 'ui-file',
|
||||
attributes: {
|
||||
protocols: 'project',
|
||||
value: 'project://hotupdate-config.json',
|
||||
}
|
||||
},
|
||||
},
|
||||
outInput: {
|
||||
label: "i18n:max-framework.hotupdate.outputcfg",
|
||||
description: "hotupdate output directory",
|
||||
default: 'project://hotupdate-packages',
|
||||
render: {
|
||||
ui: 'ui-file',
|
||||
attributes: {
|
||||
protocols: 'project',
|
||||
type: 'directory',
|
||||
value: 'project://hotupdate-packages',
|
||||
}
|
||||
},
|
||||
},
|
||||
// MinIO 配置选项
|
||||
minioAutoUpload: {
|
||||
label: 'i18n:max-framework.hotupdate.autoupload',
|
||||
description: "Enable automatic upload to MinIO after build",
|
||||
default: false,
|
||||
render: {
|
||||
ui: 'ui-checkbox',
|
||||
attributes: {}
|
||||
}
|
||||
},
|
||||
minioEndpoint: {
|
||||
label: "i18n:max-framework.hotupdate.endpoint",
|
||||
description: "MinIO server endpoint",
|
||||
default: '',
|
||||
displayCondition: {
|
||||
minioAutoUpload: true
|
||||
},
|
||||
render: {
|
||||
ui: 'ui-input',
|
||||
attributes: {
|
||||
placeholder: 'example: minio.example.com or 192.168.1.100:9000'
|
||||
}
|
||||
},
|
||||
},
|
||||
minioAccessKeyId: {
|
||||
label: "i18n:max-framework.hotupdate.accesskey",
|
||||
description: "MinIO access key ID",
|
||||
default: 'Wh32ZQrSq742n119Ta9m',
|
||||
displayCondition: {
|
||||
minioAutoUpload: true
|
||||
},
|
||||
render: {
|
||||
ui: 'ui-input',
|
||||
attributes: {
|
||||
placeholder: 'Access Key ID'
|
||||
}
|
||||
},
|
||||
},
|
||||
minioSecretAccessKey: {
|
||||
label: "i18n:max-framework.hotupdate.secretkey",
|
||||
description: "MinIO secret access key",
|
||||
default: 'OCvFbPC9wIzkP7hWywFEn3BhPiNoLjoOWKGZnAn0',
|
||||
displayCondition: {
|
||||
minioAutoUpload: true
|
||||
},
|
||||
render: {
|
||||
ui: 'ui-input',
|
||||
attributes: {
|
||||
placeholder: 'Secret Access Key',
|
||||
type: 'password'
|
||||
}
|
||||
},
|
||||
},
|
||||
minioBucketName: {
|
||||
label: "i18n:max-framework.hotupdate.bucket",
|
||||
description: "MinIO bucket name",
|
||||
default: 'hotupdate',
|
||||
displayCondition: {
|
||||
minioAutoUpload: true
|
||||
},
|
||||
render: {
|
||||
ui: 'ui-input',
|
||||
attributes: {
|
||||
placeholder: 'Bucket name'
|
||||
}
|
||||
},
|
||||
},
|
||||
minioUseSSL: {
|
||||
label: 'i18n:max-framework.hotupdate.usessl',
|
||||
description: "Use SSL for MinIO connection",
|
||||
default: false,
|
||||
displayCondition: {
|
||||
minioAutoUpload: true
|
||||
},
|
||||
render: {
|
||||
ui: 'ui-checkbox',
|
||||
attributes: {}
|
||||
}
|
||||
},
|
||||
minioPathPrefix: {
|
||||
label: "i18n:max-framework.hotupdate.pathprefix",
|
||||
description: "MinIO path prefix",
|
||||
default: 'hotupdate',
|
||||
displayCondition: {
|
||||
minioAutoUpload: true
|
||||
},
|
||||
render: {
|
||||
ui: 'ui-input',
|
||||
attributes: {
|
||||
placeholder: 'Path prefix (optional)'
|
||||
}
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
export const configs: BuildPlugin.Configs = {
|
||||
'*': {
|
||||
hooks: './hooks',
|
||||
},
|
||||
|
||||
"mac": {
|
||||
options: options,
|
||||
},
|
||||
"ios": {
|
||||
options: options,
|
||||
},
|
||||
"android": {
|
||||
options: options,
|
||||
},
|
||||
"windows": {
|
||||
options: options,
|
||||
},
|
||||
"web-mobile": {
|
||||
panel: "./panel"
|
||||
}
|
||||
};
|
||||
38
extensions/max-studio/source/hotupdate/cfgutils.ts
Normal file
38
extensions/max-studio/source/hotupdate/cfgutils.ts
Normal file
@@ -0,0 +1,38 @@
|
||||
import { existsSync, readFileSync, writeFileSync } from "fs";
|
||||
import { ensureFileSync } from "fs-extra";
|
||||
import path from "path";
|
||||
|
||||
export class CfgData {
|
||||
version: string = "1.0.0";
|
||||
md5: string = "";
|
||||
}
|
||||
|
||||
export class CfgUtils {
|
||||
private static getCfgFile(pkg: string) {
|
||||
if (!pkg || pkg.trim() === '') {
|
||||
throw new Error('包名不能为空');
|
||||
}
|
||||
return path.join(__dirname, `../../../temp/max-framework-hotupdate/${pkg}.json`);
|
||||
}
|
||||
|
||||
static getCfgData(pkg: string): CfgData {
|
||||
const cfgFile = CfgUtils.getCfgFile(pkg);
|
||||
if (existsSync(cfgFile)) {
|
||||
const data = readFileSync(cfgFile, 'utf-8');
|
||||
const cfgData = JSON.parse(data) as CfgData;
|
||||
console.log('读取配置文件', pkg, JSON.stringify(cfgData));
|
||||
return cfgData;
|
||||
} else {
|
||||
const data = new CfgData();
|
||||
CfgUtils.savaConfig(pkg, data);
|
||||
return data;
|
||||
}
|
||||
}
|
||||
|
||||
static savaConfig(pkg: string, data: CfgData) {
|
||||
console.log('保存配置文件', pkg, JSON.stringify(data));
|
||||
const cfgFile = CfgUtils.getCfgFile(pkg);
|
||||
ensureFileSync(cfgFile);
|
||||
writeFileSync(cfgFile, JSON.stringify(data), 'utf-8');
|
||||
}
|
||||
}
|
||||
31
extensions/max-studio/source/hotupdate/cli.ts
Executable file
31
extensions/max-studio/source/hotupdate/cli.ts
Executable file
@@ -0,0 +1,31 @@
|
||||
import { generator, GenPackageConfig } from "./version_generator";
|
||||
|
||||
|
||||
let configs:GenPackageConfig[] = []
|
||||
let assetRootDirPath:string = ''
|
||||
let outputPath: string = '';
|
||||
|
||||
let i = 2;
|
||||
while (i < process.argv.length) {
|
||||
let arg = process.argv[i];
|
||||
switch(arg) {
|
||||
case '-configPath':
|
||||
configs = require(process.argv[i + 1]) as GenPackageConfig[];
|
||||
break;
|
||||
case '-assetRootDirPath':
|
||||
assetRootDirPath = process.argv[i + 1];
|
||||
break;
|
||||
case '-outputDirPath':
|
||||
outputPath = process.argv[i + 1];
|
||||
break;
|
||||
default:
|
||||
console.error('unknown arg:', arg);
|
||||
break;
|
||||
}
|
||||
i += 2
|
||||
}
|
||||
|
||||
for (let item of configs) {
|
||||
let src = assetRootDirPath;
|
||||
generator(src, outputPath, item);
|
||||
}
|
||||
227
extensions/max-studio/source/hotupdate/hooks.ts
Normal file
227
extensions/max-studio/source/hotupdate/hooks.ts
Normal file
@@ -0,0 +1,227 @@
|
||||
import {
|
||||
BuildHook,
|
||||
IBuildResult,
|
||||
IBuildTaskOption,
|
||||
} from "@cocos/creator-types/editor/packages/builder/@types/public";
|
||||
import path from "path";
|
||||
import { existsSync, readFile, writeFile } from "fs";
|
||||
import { ensureDirSync } from "fs-extra";
|
||||
import { generator, GenPackageConfig } from "./version_generator";
|
||||
import { createMinIOUploader, MinIOConfig } from "./minio-uploader";
|
||||
|
||||
const injectScript = `(function () {
|
||||
const native = globalThis?.jsb || window.jsb || native;
|
||||
if (typeof native === 'object') {
|
||||
var hotUpdateSearchPaths = localStorage.getItem('HotUpdateSearchPaths');
|
||||
if (hotUpdateSearchPaths) {
|
||||
var paths = JSON.parse(hotUpdateSearchPaths);
|
||||
native.fileUtils.setSearchPaths(paths);
|
||||
|
||||
var fileList = [];
|
||||
var storagePath = paths[0] || '';
|
||||
var tempPath = storagePath + '_temp/';
|
||||
var baseOffset = tempPath.length;
|
||||
if (native.fileUtils.isDirectoryExist(tempPath) && !native.fileUtils.isFileExist(tempPath + 'project.manifest.temp')) {
|
||||
native.fileUtils.listFilesRecursively(tempPath, fileList);
|
||||
fileList.forEach(srcPath => {
|
||||
var relativePath = srcPath.substr(baseOffset);
|
||||
var dstPath = storagePath + relativePath;
|
||||
if (srcPath[srcPath.length - 1] === '/') {
|
||||
native.fileUtils.createDirectory(dstPath)
|
||||
}
|
||||
else {
|
||||
if (native.fileUtils.isFileExist(dstPath)) {
|
||||
native.fileUtils.removeFile(dstPath)
|
||||
}
|
||||
native.fileUtils.renameFile(srcPath, dstPath);
|
||||
}
|
||||
})
|
||||
native.fileUtils.removeDirectory(tempPath);
|
||||
}
|
||||
}
|
||||
}
|
||||
})();`;
|
||||
|
||||
export const onBeforeBuild: BuildHook.onBeforeBuild = async function (
|
||||
options: IBuildTaskOption,
|
||||
result: IBuildResult
|
||||
) {};
|
||||
|
||||
export const onAfterBuild: BuildHook.onAfterBuild = async function (
|
||||
options: IBuildTaskOption,
|
||||
result: IBuildResult
|
||||
) {
|
||||
console.log("onAfterBuild platform", options.platform);
|
||||
|
||||
if (!["android", "mac", "ios", "windows"].includes(options.platform)) {
|
||||
return;
|
||||
}
|
||||
|
||||
let url = path.join(result.dest, "data", "main.js");
|
||||
console.log("onAfterBuild url", url);
|
||||
if (!existsSync(url)) {
|
||||
url = path.join(result.dest, "assets", "main.js");
|
||||
}
|
||||
console.log("onAfterBuild url", url);
|
||||
|
||||
const hotupdateConfig: {
|
||||
hotupdatePluginCheck: boolean;
|
||||
configInput: string;
|
||||
outInput: string;
|
||||
// MinIO 配置
|
||||
minioAutoUpload: boolean;
|
||||
minioEndpoint: string;
|
||||
minioAccessKeyId: string;
|
||||
minioSecretAccessKey: string;
|
||||
minioBucketName: string;
|
||||
minioUseSSL: boolean;
|
||||
minioPathPrefix: string;
|
||||
} = options.packages?.["max-framework"];
|
||||
console.log(
|
||||
"onAfterBuild hotupdateConfig",
|
||||
url,
|
||||
hotupdateConfig.hotupdatePluginCheck,
|
||||
hotupdateConfig.configInput,
|
||||
hotupdateConfig.outInput,
|
||||
JSON.stringify(hotupdateConfig)
|
||||
);
|
||||
if (!!hotupdateConfig.hotupdatePluginCheck) {
|
||||
// 验证配置参数
|
||||
if (!hotupdateConfig.configInput || !hotupdateConfig.outInput) {
|
||||
console.error("热更新配置不完整:configInput 和 outInput 不能为空");
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
// configInput 是带 project:// 前缀的,需要转换为实际文件路径
|
||||
let configPath = hotupdateConfig.configInput;
|
||||
if (configPath.startsWith("project://")) {
|
||||
configPath = path.resolve(
|
||||
Editor.Project.path,
|
||||
configPath.replace("project://", "")
|
||||
);
|
||||
} else if (!path.isAbsolute(configPath)) {
|
||||
configPath = path.resolve(Editor.Project.path, configPath);
|
||||
}
|
||||
let outInput = hotupdateConfig.outInput;
|
||||
if (outInput.startsWith("project://")) {
|
||||
outInput = path.resolve(
|
||||
Editor.Project.path,
|
||||
outInput.replace("project://", "")
|
||||
);
|
||||
} else if (!path.isAbsolute(outInput)) {
|
||||
outInput = path.resolve(Editor.Project.path, outInput);
|
||||
}
|
||||
console.log("onAfterBuild configPath", configPath, outInput);
|
||||
const obj = require(configPath) as GenPackageConfig[];
|
||||
for (const item of obj) {
|
||||
// 验证包名不能为空
|
||||
if (!item.pkgName || item.pkgName.trim() === "") {
|
||||
console.error("热更新包名不能为空:", item);
|
||||
continue;
|
||||
}
|
||||
const source = path.join(result.dest, "data");
|
||||
console.log("开始生成Package: ", item.pkgName, source);
|
||||
ensureDirSync(
|
||||
path.join(outInput, item.pkgName)
|
||||
);
|
||||
generator(source, outInput, item);
|
||||
}
|
||||
|
||||
// MinIO上传功能
|
||||
if (hotupdateConfig.minioAutoUpload) {
|
||||
console.log("开始MinIO自动上传流程...");
|
||||
await uploadToMinIO(hotupdateConfig, outInput, obj);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("热更新配置文件加载失败:", error);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
return new Promise((resolve, reject) => {
|
||||
readFile(url, "utf8", (err, data) => {
|
||||
if (err) {
|
||||
reject(err);
|
||||
return;
|
||||
}
|
||||
const newStr = injectScript + data;
|
||||
writeFile(url, newStr, (err) => {
|
||||
if (err) {
|
||||
reject(err);
|
||||
return;
|
||||
}
|
||||
|
||||
resolve();
|
||||
});
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* 上传到MinIO
|
||||
* @param config 热更新配置
|
||||
* @param outputPath 输出路径
|
||||
* @param packages 包配置列表
|
||||
*/
|
||||
async function uploadToMinIO(
|
||||
config: {
|
||||
minioEndpoint: string;
|
||||
minioAccessKeyId: string;
|
||||
minioSecretAccessKey: string;
|
||||
minioBucketName: string;
|
||||
minioUseSSL: boolean;
|
||||
minioPathPrefix: string;
|
||||
},
|
||||
outputPath: string,
|
||||
packages: GenPackageConfig[]
|
||||
) {
|
||||
try {
|
||||
// 验证MinIO配置
|
||||
if (!config.minioEndpoint || !config.minioAccessKeyId ||
|
||||
!config.minioSecretAccessKey || !config.minioBucketName) {
|
||||
console.error("MinIO配置不完整,跳过上传");
|
||||
return;
|
||||
}
|
||||
|
||||
// 创建MinIO上传器
|
||||
const minioConfig: MinIOConfig = {
|
||||
endpoint: config.minioEndpoint,
|
||||
accessKeyId: config.minioAccessKeyId,
|
||||
secretAccessKey: config.minioSecretAccessKey,
|
||||
bucketName: config.minioBucketName,
|
||||
useSSL: config.minioUseSSL,
|
||||
pathPrefix: config.minioPathPrefix
|
||||
};
|
||||
|
||||
const uploader = createMinIOUploader(minioConfig);
|
||||
|
||||
// 上传每个包
|
||||
for (const pkg of packages) {
|
||||
if (!pkg.pkgName || pkg.pkgName.trim() === "") {
|
||||
continue;
|
||||
}
|
||||
|
||||
const packageDir = path.join(outputPath, pkg.pkgName);
|
||||
if (!existsSync(packageDir)) {
|
||||
console.error(`包目录不存在: ${packageDir}`);
|
||||
continue;
|
||||
}
|
||||
|
||||
console.log(`开始上传包到MinIO: ${pkg.pkgName}`);
|
||||
|
||||
const result = await uploader.uploadDirectory(packageDir, pkg.pkgName);
|
||||
|
||||
if (result.success) {
|
||||
console.log(`包 ${pkg.pkgName} 上传成功,共 ${result.uploadedFiles.length} 个文件`);
|
||||
console.log(`上传的文件: ${result.uploadedFiles.join(', ')}`);
|
||||
} else {
|
||||
console.error(`包 ${pkg.pkgName} 上传失败: ${result.error}`);
|
||||
}
|
||||
}
|
||||
|
||||
console.log("所有包MinIO上传完成");
|
||||
} catch (error) {
|
||||
console.error("MinIO上传过程中发生错误:", error);
|
||||
}
|
||||
}
|
||||
229
extensions/max-studio/source/hotupdate/minio-uploader.ts
Normal file
229
extensions/max-studio/source/hotupdate/minio-uploader.ts
Normal file
@@ -0,0 +1,229 @@
|
||||
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);
|
||||
}
|
||||
226
extensions/max-studio/source/hotupdate/panel.ts
Executable file
226
extensions/max-studio/source/hotupdate/panel.ts
Executable file
@@ -0,0 +1,226 @@
|
||||
import { IBuildTaskOption } from "@cocos/creator-types/editor/packages/builder/@types";
|
||||
|
||||
export const style = `
|
||||
.hotupdate-plugin {
|
||||
padding: 10px;
|
||||
}
|
||||
|
||||
.minio-section {
|
||||
margin-top: 15px;
|
||||
padding: 10px;
|
||||
border: 1px solid #ddd;
|
||||
border-radius: 4px;
|
||||
background-color: #f9f9f9;
|
||||
}
|
||||
|
||||
.minio-title {
|
||||
font-weight: bold;
|
||||
margin-bottom: 10px;
|
||||
color: #333;
|
||||
}
|
||||
|
||||
.minio-config-row {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
|
||||
.minio-config-row ui-label {
|
||||
width: 120px;
|
||||
min-width: 120px;
|
||||
}
|
||||
|
||||
.minio-config-row ui-input,
|
||||
.minio-config-row ui-checkbox {
|
||||
flex: 1;
|
||||
margin-left: 10px;
|
||||
}
|
||||
|
||||
.minio-test-button {
|
||||
margin-top: 10px;
|
||||
}
|
||||
`;
|
||||
|
||||
export const temple = `
|
||||
<div class="hotupdate-plugin">
|
||||
<div class="minio-section">
|
||||
<div class="minio-title">MinIO 上传配置</div>
|
||||
|
||||
<div class="minio-config-row">
|
||||
<ui-label value="自动上传"></ui-label>
|
||||
<ui-checkbox id="minio-auto-upload"></ui-checkbox>
|
||||
</div>
|
||||
|
||||
<div class="minio-config-row">
|
||||
<ui-label value="服务器地址"></ui-label>
|
||||
<ui-input id="minio-endpoint" placeholder="例: minio.example.com:9000"></ui-input>
|
||||
</div>
|
||||
|
||||
<div class="minio-config-row">
|
||||
<ui-label value="Access Key"></ui-label>
|
||||
<ui-input id="minio-access-key" placeholder="Access Key ID"></ui-input>
|
||||
</div>
|
||||
|
||||
<div class="minio-config-row">
|
||||
<ui-label value="Secret Key"></ui-label>
|
||||
<ui-input id="minio-secret-key" type="password" placeholder="Secret Access Key"></ui-input>
|
||||
</div>
|
||||
|
||||
<div class="minio-config-row">
|
||||
<ui-label value="存储桶名称"></ui-label>
|
||||
<ui-input id="minio-bucket" placeholder="hotupdate"></ui-input>
|
||||
</div>
|
||||
|
||||
<div class="minio-config-row">
|
||||
<ui-label value="使用SSL"></ui-label>
|
||||
<ui-checkbox id="minio-use-ssl"></ui-checkbox>
|
||||
</div>
|
||||
|
||||
<div class="minio-config-row">
|
||||
<ui-label value="路径前缀"></ui-label>
|
||||
<ui-input id="minio-path-prefix" placeholder="hotupdate"></ui-input>
|
||||
</div>
|
||||
|
||||
<div class="minio-test-button">
|
||||
<ui-button id="test-minio-connection">测试连接</ui-button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
|
||||
export const $ = {
|
||||
root: '.hotupdate-plugin',
|
||||
minioAutoUpload: '#minio-auto-upload',
|
||||
minioEndpoint: '#minio-endpoint',
|
||||
minioAccessKey: '#minio-access-key',
|
||||
minioSecretKey: '#minio-secret-key',
|
||||
minioBucket: '#minio-bucket',
|
||||
minioUseSSL: '#minio-use-ssl',
|
||||
minioPathPrefix: '#minio-path-prefix',
|
||||
testButton: '#test-minio-connection',
|
||||
}
|
||||
|
||||
export async function update(options: IBuildTaskOption, key: string) {
|
||||
console.log(`update options ${options} and key ${key}`);
|
||||
if (key) {
|
||||
return;
|
||||
}
|
||||
|
||||
updateMinIOConfigFromOptions(options);
|
||||
}
|
||||
|
||||
export async function ready(options: IBuildTaskOption) {
|
||||
console.log(`ready options ${options}`);
|
||||
init();
|
||||
updateMinIOConfigFromOptions(options);
|
||||
}
|
||||
|
||||
export async function close() {
|
||||
// 清理资源
|
||||
}
|
||||
|
||||
function init() {
|
||||
// 绑定测试连接按钮事件
|
||||
const testButton = document.querySelector($.testButton) as HTMLElement;
|
||||
if (testButton) {
|
||||
testButton.addEventListener('click', testMinIOConnection);
|
||||
}
|
||||
|
||||
// 绑定自动上传复选框事件
|
||||
const autoUploadCheckbox = document.querySelector($.minioAutoUpload) as HTMLInputElement;
|
||||
if (autoUploadCheckbox) {
|
||||
autoUploadCheckbox.addEventListener('change', onAutoUploadChange);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 从构建选项中更新MinIO配置
|
||||
*/
|
||||
function updateMinIOConfigFromOptions(options: IBuildTaskOption) {
|
||||
const config = options.packages?.['max-framework'];
|
||||
if (!config) return;
|
||||
|
||||
// 更新UI元素的值
|
||||
updateInputValue($.minioAutoUpload, config.minioAutoUpload);
|
||||
updateInputValue($.minioEndpoint, config.minioEndpoint);
|
||||
updateInputValue($.minioAccessKey, config.minioAccessKeyId);
|
||||
updateInputValue($.minioSecretKey, config.minioSecretAccessKey);
|
||||
updateInputValue($.minioBucket, config.minioBucketName);
|
||||
updateInputValue($.minioUseSSL, config.minioUseSSL);
|
||||
updateInputValue($.minioPathPrefix, config.minioPathPrefix);
|
||||
}
|
||||
|
||||
/**
|
||||
* 更新输入框的值
|
||||
*/
|
||||
function updateInputValue(selector: string, value: any) {
|
||||
const element = document.querySelector(selector) as HTMLInputElement;
|
||||
if (element) {
|
||||
if (element.type === 'checkbox') {
|
||||
element.checked = Boolean(value);
|
||||
} else {
|
||||
element.value = String(value || '');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 自动上传复选框变化事件
|
||||
*/
|
||||
function onAutoUploadChange(event: Event) {
|
||||
const checkbox = event.target as HTMLInputElement;
|
||||
const isEnabled = checkbox.checked;
|
||||
|
||||
// 可以在这里添加额外的逻辑,比如当启用自动上传时显示警告
|
||||
if (isEnabled) {
|
||||
console.log('已启用MinIO自动上传');
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 测试MinIO连接
|
||||
*/
|
||||
async function testMinIOConnection() {
|
||||
try {
|
||||
// 获取当前配置
|
||||
const config = {
|
||||
endpoint: (document.querySelector($.minioEndpoint) as HTMLInputElement)?.value || '',
|
||||
accessKeyId: (document.querySelector($.minioAccessKey) as HTMLInputElement)?.value || '',
|
||||
secretAccessKey: (document.querySelector($.minioSecretKey) as HTMLInputElement)?.value || '',
|
||||
bucketName: (document.querySelector($.minioBucket) as HTMLInputElement)?.value || '',
|
||||
useSSL: (document.querySelector($.minioUseSSL) as HTMLInputElement)?.checked || false,
|
||||
pathPrefix: (document.querySelector($.minioPathPrefix) as HTMLInputElement)?.value || ''
|
||||
};
|
||||
|
||||
// 验证必填字段
|
||||
if (!config.endpoint || !config.accessKeyId || !config.secretAccessKey || !config.bucketName) {
|
||||
alert('请填写完整的MinIO配置信息');
|
||||
return;
|
||||
}
|
||||
|
||||
// 添加加载状态
|
||||
const testButton = document.querySelector($.testButton) as HTMLElement;
|
||||
const originalText = testButton.textContent;
|
||||
testButton.textContent = '测试中...';
|
||||
testButton.setAttribute('disabled', 'true');
|
||||
|
||||
// 这里可以添加实际的连接测试逻辑
|
||||
// 由于在面板环境中,我们只做基本验证
|
||||
await new Promise(resolve => setTimeout(resolve, 1000)); // 模拟网络请求
|
||||
|
||||
alert('MinIO配置验证通过!\n注意:实际连接测试将在构建时进行。');
|
||||
|
||||
// 恢复按钮状态
|
||||
testButton.textContent = originalText;
|
||||
testButton.removeAttribute('disabled');
|
||||
|
||||
} catch (error) {
|
||||
console.error('测试MinIO连接失败:', error);
|
||||
alert(`测试失败: ${error}`);
|
||||
|
||||
// 恢复按钮状态
|
||||
const testButton = document.querySelector($.testButton) as HTMLElement;
|
||||
testButton.textContent = '测试连接';
|
||||
testButton.removeAttribute('disabled');
|
||||
}
|
||||
}
|
||||
184
extensions/max-studio/source/hotupdate/version_generator.ts
Normal file
184
extensions/max-studio/source/hotupdate/version_generator.ts
Normal file
@@ -0,0 +1,184 @@
|
||||
import fs, { existsSync } from "fs";
|
||||
import { ensureDirSync, copyFileSync } from "fs-extra";
|
||||
import { createHash } from "crypto";
|
||||
import path from "path";
|
||||
import { CfgUtils } from "./cfgutils";
|
||||
|
||||
|
||||
|
||||
export interface GenPackageConfig {
|
||||
pkgName: string;
|
||||
files: string[];
|
||||
version?: string;
|
||||
}
|
||||
|
||||
class Manifest {
|
||||
packageUrl: string = "";
|
||||
remoteManifestUrl: string = "";
|
||||
remoteVersionUrl: string = "";
|
||||
version: string = "";
|
||||
assets: object = {};
|
||||
searchPaths: string[] = [];
|
||||
}
|
||||
|
||||
class VersionManifest {
|
||||
packageUrl: string = "";
|
||||
remoteManifestUrl: string = "";
|
||||
remoteVersionUrl: string = "";
|
||||
version: string = "";
|
||||
}
|
||||
|
||||
function readDir(buildDir: string, destination: string, dir: string, obj: any) {
|
||||
try {
|
||||
const stat = fs.statSync(dir);
|
||||
if (stat.isFile()) {
|
||||
const subpath = dir;
|
||||
const size = stat.size;
|
||||
const md5 = createHash('md5').update(fs.readFileSync(subpath)).digest('hex');
|
||||
const compressed = path.extname(subpath).toLowerCase() === '.zip';
|
||||
|
||||
let relv = path.relative(buildDir, subpath);
|
||||
relv = relv.replace(/\\/g, '/');
|
||||
relv = encodeURI(relv);
|
||||
obj[relv] = {
|
||||
"size": size,
|
||||
"md5": md5
|
||||
};
|
||||
if (compressed) {
|
||||
obj[relv].compressed = true;
|
||||
}
|
||||
const index = relv.lastIndexOf("/");
|
||||
const subdir = relv.substring(0, index);
|
||||
mkdirSync(path.join(destination, subdir));
|
||||
copyFileSync(subpath, path.join(destination, relv));
|
||||
} else {
|
||||
const subpaths = fs.readdirSync(dir);
|
||||
for (let i = 0; i < subpaths.length; i++) {
|
||||
if (subpaths[i][0] === '.') {
|
||||
continue;
|
||||
}
|
||||
const subpath = path.join(dir, subpaths[i]);
|
||||
const stat = fs.statSync(subpath);
|
||||
if (stat.isDirectory()) {
|
||||
readDir(buildDir, destination, subpath, obj);
|
||||
} else if (stat.isFile()) {
|
||||
const size = stat.size;
|
||||
const md5 = createHash('md5').update(fs.readFileSync(subpath)).digest('hex');
|
||||
const compressed = path.extname(subpath).toLowerCase() === '.zip';
|
||||
|
||||
let relv = path.relative(buildDir, subpath);
|
||||
relv = relv.replace(/\\/g, '/');
|
||||
relv = encodeURI(relv);
|
||||
obj[relv] = {
|
||||
"size": size,
|
||||
"md5": md5
|
||||
};
|
||||
if (compressed) {
|
||||
obj[relv].compressed = true;
|
||||
}
|
||||
const index = relv.lastIndexOf("/");
|
||||
const subdir = relv.substring(0, index);
|
||||
mkdirSync(path.join(destination, subdir));
|
||||
copyFileSync(subpath, path.join(destination, relv));
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
}
|
||||
}
|
||||
|
||||
function mkdirSync(path: string) {
|
||||
try {
|
||||
ensureDirSync(path);
|
||||
} catch (error: any) {
|
||||
if (error.code != 'EEXIST') throw error;
|
||||
}
|
||||
}
|
||||
|
||||
function versionCompare(versionA: string, versionB: string) {
|
||||
const vA = versionA.split('.');
|
||||
const vB = versionB.split('.');
|
||||
for (let i = 0; i < vA.length; ++i) {
|
||||
const a = parseInt(vA[i]);
|
||||
const b = parseInt(vB[i] || '0');
|
||||
if (a === b) {
|
||||
continue;
|
||||
} else {
|
||||
return a - b;
|
||||
}
|
||||
}
|
||||
return vB.length > vA.length ? -1 : 0;
|
||||
}
|
||||
|
||||
function clearDir(dir: string) {
|
||||
try {
|
||||
if (existsSync(dir)) {
|
||||
const files = fs.readdirSync(dir);
|
||||
for (const file of files) {
|
||||
const filePath = path.join(dir, file);
|
||||
const stat = fs.statSync(filePath);
|
||||
if (stat.isDirectory()) {
|
||||
clearDir(filePath);
|
||||
fs.rmdirSync(filePath);
|
||||
} else {
|
||||
fs.unlinkSync(filePath);
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch (err) {
|
||||
console.error('清理目录失败:', err);
|
||||
}
|
||||
}
|
||||
|
||||
export function generator(buildDir: string, output: string, cfg: GenPackageConfig) {
|
||||
const destination = path.join(output,cfg.pkgName);
|
||||
const manifest = new Manifest();
|
||||
const version = new VersionManifest();
|
||||
|
||||
const cfgData = CfgUtils.getCfgData(cfg.pkgName);
|
||||
|
||||
if (cfg.version) {
|
||||
cfgData.version = cfg.version;
|
||||
} else {
|
||||
const versionParts = cfgData.version.split('.');
|
||||
const patch = parseInt(versionParts[2] || '0') + 1;
|
||||
cfgData.version = `${versionParts[0]}.${versionParts[1]}.${patch}`;
|
||||
}
|
||||
|
||||
manifest.version = cfgData.version;
|
||||
manifest.packageUrl = `${cfg.pkgName}/${cfgData.version}`;
|
||||
manifest.remoteManifestUrl = `${cfg.pkgName}/project.manifest`;
|
||||
manifest.remoteVersionUrl = `${cfg.pkgName}/version.manifest`;
|
||||
version.version = cfgData.version;
|
||||
|
||||
// 清理输出目录
|
||||
clearDir(destination);
|
||||
ensureDirSync(destination);
|
||||
|
||||
// 处理文件
|
||||
for (const file of cfg.files) {
|
||||
const filePath = path.join(buildDir, file);
|
||||
if (existsSync(filePath)) {
|
||||
readDir(buildDir, destination, filePath, manifest.assets);
|
||||
}
|
||||
}
|
||||
|
||||
// 生成 manifest 文件到输出目录
|
||||
fs.writeFileSync(path.join(destination, 'project.manifest'), JSON.stringify(manifest, null, 2));
|
||||
fs.writeFileSync(path.join(destination, 'version.manifest'), JSON.stringify(version, null, 2));
|
||||
|
||||
// 将 manifest 和 version 文件也存储到 buildDir + "/assets/" + pkgName 目录下
|
||||
const buildAssetsDir = path.join(buildDir, "assets", cfg.pkgName);
|
||||
ensureDirSync(buildAssetsDir);
|
||||
fs.writeFileSync(path.join(buildAssetsDir, 'project.manifest'), JSON.stringify(manifest, null, 2));
|
||||
fs.writeFileSync(path.join(buildAssetsDir, 'version.manifest'), JSON.stringify(version, null, 2));
|
||||
|
||||
console.log(`Manifest文件已生成到构建目录: ${buildAssetsDir}`);
|
||||
console.log(`Manifest文件已生成到输出目录: ${destination}`);
|
||||
|
||||
// 保存配置
|
||||
CfgUtils.savaConfig(cfg.pkgName, cfgData);
|
||||
|
||||
console.log(`热更新包生成完成: ${cfg.pkgName} v${cfgData.version}`);
|
||||
}
|
||||
Reference in New Issue
Block a user