// Copyright (c) 2025 EFramework Innovation. All rights reserved. // Use of this source code is governed by a MIT-style // license that can be found in the LICENSE file. using System; using System.IO; using System.Collections.Generic; using UnityEngine; using UnityEditor; using EFramework.Unity.Editor; using EFramework.Unity.Utility; namespace EFramework.Unity.Asset.Editor { public partial class XAsset { /// /// XAsset.Publisher 实现了资源包的发布工作流,用于将打包好的资源发布至存储服务中。 /// /// /// /// 功能特性 /// - 首选项配置:提供首选项配置以自定义发布流程 /// - 自动化流程:提供资源包发布任务的自动化执行 /// /// 使用手册 /// /// 1. 首选项配置 /// /// | 配置项 | 配置键 | 默认值 | /// | -------- | --------------------------- | ------------------ | /// | 存储服务地址 | `Asset/Publisher/Endpoint@Editor` | `${Environment.StorageEndpoint}` | /// | 存储分区名称 | `Asset/Publisher/Bucket@Editor` | `${Environment.StorageBucket}` | /// | 存储服务凭证 | `Asset/Publisher/Access@Editor` | `${Environment.StorageAccess}` | /// | 存储服务密钥 | `Asset/Publisher/Secret@Editor` | `${Environment.StorageSecret}` | /// /// 关联配置项:`Asset/LocalUri`、`Asset/RemoteUri` /// /// 以上配置项均可在 `Tools/EFramework/Preferences/Asset/Publisher` 首选项编辑器中进行可视化配置。 /// /// 2. 自动化流程 /// /// 2.1 本地环境 /// /// 本地开发环境可以使用 MinIO 作为文件存储服务: /// /// 1. 安装服务: /// /// ```bash /// # 启动 MinIO 容器 /// docker run -d --name minio -p 9000:9000 -p 9090:9090 --restart=always \ /// -e "MINIO_ACCESS_KEY=admin" -e "MINIO_SECRET_KEY=adminadmin" \ /// minio/minio server /data --console-address ":9090" --address ":9000" /// ``` /// /// 2. 服务配置: /// - 控制台:http://localhost:9090 /// - API:http://localhost:9000 /// - 凭证: /// - Access Key:admin /// - Secret Key:adminadmin /// - 存储:创建 `default` 存储桶并设置公开访问权限 /// /// 3. 首选项配置: /// ``` /// Asset/Publisher/Endpoint@Editor = http://localhost:9000 /// Asset/Publisher/Bucket@Editor = default /// Asset/Publisher/Access@Editor = admin /// Asset/Publisher/Secret@Editor = adminadmin /// ``` /// /// 2.2 发布流程 /// /// ```mermaid /// stateDiagram-v2 /// direction LR /// 读取发布配置 --> 获取远端清单 /// 获取远端清单 --> 对比本地清单 /// 对比本地清单 --> 发布差异文件 /// ``` /// /// 发布时根据清单对比结果进行增量上传: /// - 新增文件:`文件名@MD5` /// - 修改文件:`文件名@MD5` /// - 清单文件:`Manifest.db` 和 `Manifest.db@MD5`(用于版本记录) /// /// /// 更多信息请参考模块文档。 /// [XEditor.Tasks.Worker(name: "Publish Assets", group: "XAsset", runasync: true, priority: 102)] public class Publisher : XEditor.MinIO, XEditor.Tasks.Panel.IOnGUI { /// /// Preferences 是发布流程首选项设置类,包含存储服务相关的配置选项。 /// public class Preferences : Builder.Preferences { /// /// Endpoint 是存储服务地址的键名。 /// public const string Endpoint = "XAsset/Publisher/Endpoint@Editor"; /// /// EndpointDefault 是存储服务地址的默认值。 /// public const string EndpointDefault = "${Environment.StorageEndpoint}"; /// /// Bucket 是存储分区名称的键名。 /// public const string Bucket = "XAsset/Publisher/Bucket@Editor"; /// /// BucketDefault 是存储分区名称的默认值。 /// public const string BucketDefault = "${Environment.StorageBucket}"; /// /// Access 是存储服务凭证的键名。 /// public const string Access = "XAsset/Publisher/Access@Editor"; /// /// AccessDefault 是存储服务凭证的默认值。 /// public const string AccessDefault = "${Environment.StorageAccess}"; /// /// Secret 是存储服务密钥的键名。 /// public const string Secret = "XAsset/Publisher/Secret@Editor"; /// /// SecretDefault 是存储服务密钥的默认值。 /// public const string SecretDefault = "${Environment.StorageSecret}"; /// /// Section 获取面板章节的名称。 /// public override string Section => "XAsset"; /// /// Priority 获取面板显示的优先级。 /// public override int Priority => 102; /// /// 初始化的新实例。 /// public Preferences() { foldout = false; } [NonSerialized] SerializedObject serialized; public override void OnVisualize(string searchContext, XPrefs.IBase context) { var taskPanel = searchContext == "Task Runner"; serialized ??= new SerializedObject(this); serialized.Update(); var ocolor = GUI.color; var bundleMode = context.GetBool(BundleMode, BundleModeDefault); if (!taskPanel) { if (!bundleMode) GUI.color = Color.gray; EditorGUILayout.BeginVertical(EditorStyles.helpBox); foldout = EditorGUILayout.Foldout(foldout, new GUIContent("Publisher", "XAsset Publisher Options.")); } else foldout = true; if (foldout && bundleMode) { EditorGUILayout.BeginVertical(EditorStyles.helpBox); EditorGUILayout.BeginHorizontal(); GUILayout.Label(new GUIContent("Endpoint", "Storage Service Endpoint."), GUILayout.Width(60)); context.Set(Endpoint, EditorGUILayout.TextField("", context.GetString(Endpoint, EndpointDefault))); EditorGUILayout.EndHorizontal(); EditorGUILayout.BeginHorizontal(); GUILayout.Label(new GUIContent("Bucket", "Storage Bucket Name."), GUILayout.Width(60)); context.Set(Bucket, EditorGUILayout.TextField("", context.GetString(Bucket, BucketDefault))); EditorGUILayout.EndHorizontal(); EditorGUILayout.BeginHorizontal(); GUILayout.Label(new GUIContent("Access", "Storage Access Key."), GUILayout.Width(60)); context.Set(Access, EditorGUILayout.TextField("", context.GetString(Access, AccessDefault))); EditorGUILayout.EndHorizontal(); EditorGUILayout.BeginHorizontal(); GUILayout.Label(new GUIContent("Secret", "Storage Secret Key."), GUILayout.Width(60)); context.Set(Secret, EditorGUILayout.TextField("", context.GetString(Secret, SecretDefault))); EditorGUILayout.EndHorizontal(); EditorGUILayout.EndVertical(); } else if (foldout && !bundleMode) EditorGUILayout.HelpBox("Bundle Mode is Disabled.", MessageType.None); GUI.color = ocolor; if (!taskPanel) EditorGUILayout.EndVertical(); } } internal Preferences preferencesPanel; void XEditor.Tasks.Panel.IOnGUI.OnGUI() { preferencesPanel = preferencesPanel != null ? preferencesPanel : ScriptableObject.CreateInstance(); preferencesPanel.OnVisualize("Task Runner", XPrefs.Asset); } public override void Preprocess(XEditor.Tasks.Report report) { Endpoint = XPrefs.GetString(Preferences.Endpoint, Preferences.EndpointDefault).Eval(XEnv.Instance); Bucket = XPrefs.GetString(Preferences.Bucket, Preferences.BucketDefault).Eval(XEnv.Instance); Access = XPrefs.GetString(Preferences.Access, Preferences.AccessDefault).Eval(XEnv.Instance); Secret = XPrefs.GetString(Preferences.Secret, Preferences.SecretDefault).Eval(XEnv.Instance); base.Preprocess(report); Local = XFile.PathJoin(Temp, XPrefs.GetString(Asset.XAsset.Preferences.LocalUri, Asset.XAsset.Preferences.LocalUriDefault)); Remote = XPrefs.GetString(Asset.XAsset.Preferences.RemoteUri, Asset.XAsset.Preferences.RemoteUriDefault).Eval(XEnv.Instance); } public override void Process(XEditor.Tasks.Report report) { var root = XFile.NormalizePath(XPrefs.GetString(Builder.Preferences.Output, Builder.Preferences.OutputDefault).Eval(XEnv.Instance)); var remoteMani = new XMani.Manifest(); var tempFile = Path.GetTempFileName(); var task = XEditor.Command.Run(bin: Bin, args: new string[] { "get", $"\"{Alias}/{Bucket}/{Remote}/{XMani.Default}\"", tempFile }); task.Wait(); if (task.Result.Code != 0) { XLog.Warn("XAsset.Publisher.Process: get remote mainifest failed: {0}", task.Result.Error); } else { remoteMani.Read(tempFile); if (!string.IsNullOrEmpty(remoteMani.Error)) XLog.Warn("XAsset.Publisher.Process: parse remote mainifest failed: {0}", remoteMani.Error); } var localMani = new XMani.Manifest(); localMani.Read(XFile.PathJoin(root, XMani.Default)); if (!string.IsNullOrEmpty(localMani.Error)) XLog.Warn("XAsset.Publisher.Process: parse local mainifest failed: {0}", remoteMani.Error); else { var diff = remoteMani.Compare(localMani); var files = new List(); for (var i = 0; i < diff.Added.Count; i++) { files.Add(new string[] { XFile.PathJoin(root, diff.Added[i].Name), diff.Added[i].MD5 }); } for (var i = 0; i < diff.Modified.Count; i++) { files.Add(new string[] { XFile.PathJoin(root, diff.Modified[i].Name), diff.Modified[i].MD5 }); } if (diff.Added.Count > 0 || diff.Modified.Count > 0) { var maniFile = XFile.PathJoin(root, XMani.Default); files.Add(new string[] { maniFile, "" }); files.Add(new string[] { maniFile, XFile.FileMD5(maniFile) }); } if (files.Count == 0) { XLog.Debug("XAsset.Publisher.Process: diff files is zero, no need to publish."); return; } else { foreach (var kvp in files) { var file = kvp[0]; var md5 = kvp[1]; var src = file; var dst = XFile.PathJoin(Local, Path.GetRelativePath(root, file)); if (!string.IsNullOrEmpty(md5)) dst += "@" + md5; // file@md5 var dir = Path.GetDirectoryName(dst); if (!XFile.HasDirectory(dir)) XFile.CreateDirectory(dir); XFile.CopyFile(src, dst); } } base.Process(report); } } } } }