Przeglądaj źródła

feat: 😁新增项目自动更新模块

喵你个旺呀 1 rok temu
rodzic
commit
217ad93486

+ 21 - 0
Admin.NET/Admin.NET.Application/Configuration/Gitee.json

@@ -0,0 +1,21 @@
+{
+  "$schema": "https://gitee.com/dotnetchina/Furion/raw/v4/schemas/v4/furion-schema.json",
+
+  "Gitee": {
+    "Owner": "jasondom", // gitee用户名
+    "Repo": "Admin.NET", // 仓库名
+    "Branch": "next", // 分支名
+    "AccessToken": "xxxxxxxxxxxxxxxxxxxxxxxxx", // gitee用户授权码
+    "OutputDir": { // 输出目录
+      "FrontEnd": "D:\\Admin.NET\\wwwroot", // 前端输出目录
+      "BackEnd": "D:\\Admin.NET" // 后端输出目录
+    },
+    "UpdateInterval": 10, // 最小更新间隔(分钟),-1不限制
+    "BackupCount": 10, // 备份文件保留数量
+    "Publish": { // 后端发布选项
+      "Configuration": "Release", // 发布环境
+      "TargetFramework": "net8.0", // 发布.NET版本
+      "RuntimeIdentifier": "linux-x64" // 运行环境
+    }
+  }
+}

+ 5 - 0
Admin.NET/Admin.NET.Core/Const/CacheConst.cs

@@ -95,4 +95,9 @@ public class CacheConst
     /// Excel临时文件缓存
     /// </summary>
     public const string KeyExcelTemp = "sys_excel_temp:";
+
+    /// <summary>
+    /// 系统更新命令日志缓存
+    /// </summary>
+    public const string KeySysUpdateLog = "sys_update_log:";
 }

+ 111 - 0
Admin.NET/Admin.NET.Core/Option/GiteeOptions.cs

@@ -0,0 +1,111 @@
+// Admin.NET 项目的版权、商标、专利和其他相关权利均受相应法律法规的保护。使用本项目应遵守相关法律法规和许可证的要求。
+// 
+// 本项目主要遵循 MIT 许可证和 Apache 许可证(版本 2.0)进行分发和使用。许可证位于源代码树根目录中的 LICENSE-MIT 和 LICENSE-APACHE 文件。
+// 
+// 不得利用本项目从事危害国家安全、扰乱社会秩序、侵犯他人合法权益等法律法规禁止的活动!任何基于本项目二次开发而产生的一切法律纠纷和责任,我们不承担任何责任!
+
+namespace Admin.NET.Core;
+
+/// <summary>
+/// Gitee CI/CD 配置选项
+/// </summary>
+public class GiteeOptions : IConfigurableOptions
+{
+    /// <summary>
+    /// 用户名
+    /// </summary>
+    public string Owner { get; set; }
+    
+    /// <summary>
+    /// 仓库名
+    /// </summary>
+    public string Repo { get; set; }
+    
+    /// <summary>
+    /// 分支名
+    /// </summary>
+    public string Branch { get; set; }
+    
+    /// <summary>
+    /// 用户授权码
+    /// </summary>
+    public string AccessToken { get; set; }
+    
+    /// <summary>
+    /// 输出目录配置
+    /// </summary>
+    public OutputDirOptions OutputDir { get; set; }
+
+    /// <summary>
+    /// 更新间隔限制(分钟)-1 不限制
+    /// </summary>
+    public int UpdateInterval { get; set; }
+    
+    /// <summary>
+    /// 保留备份文件的数量
+    /// </summary>
+    public int BackupCount { get; set; }
+
+    /// <summary>
+    /// 发布配置选项
+    /// </summary>
+    public GiteePublishOptions Publish { get; set; }
+}
+
+/// <summary>
+/// 项目目录
+/// </summary>
+public class ProjectDirOptions
+{
+    /// <summary>
+    /// 后端目录
+    /// </summary>
+    public string BackEnd { get; set; }
+    
+    /// <summary>
+    /// 前端目录
+    /// </summary>
+    public string FrontEnd { get; set; }
+    
+    /// <summary>
+    /// 后端入口项目目录
+    /// </summary>
+    public string EntryDir { get; set; }
+}
+
+/// <summary>
+/// 输出目录
+/// </summary>
+public class OutputDirOptions
+{
+    /// <summary>
+    /// 后端目录
+    /// </summary>
+    public string BackEnd { get; set; }
+    
+    /// <summary>
+    /// 前端目录
+    /// </summary>
+    public string FrontEnd { get; set; }
+}
+
+/// <summary>
+/// Gitee发布配置选项
+/// </summary>
+public class GiteePublishOptions
+{
+    /// <summary>
+    /// 发布环境配置
+    /// </summary>
+    public string Configuration { get; set; }
+    
+    /// <summary>
+    /// 目标框架
+    /// </summary>
+    public string TargetFramework { get; set; }
+    
+    /// <summary>
+    /// 运行环境
+    /// </summary>
+    public string RuntimeIdentifier { get; set; }
+}

+ 8 - 1
Admin.NET/Admin.NET.Core/Service/Common/SysCommonService.cs

@@ -4,6 +4,7 @@
 //
 // 不得利用本项目从事危害国家安全、扰乱社会秩序、侵犯他人合法权益等法律法规禁止的活动!任何基于本项目二次开发而产生的一切法律纠纷和责任,我们不承担任何责任!
 
+using System.IO.Compression;
 using Microsoft.AspNetCore.Mvc.ApiExplorer;
 using Org.BouncyCastle.Crypto.Parameters;
 using Org.BouncyCastle.Utilities.Encoders;
@@ -19,10 +20,16 @@ namespace Admin.NET.Core.Service;
 public class SysCommonService : IDynamicApiController, ITransient
 {
     private readonly IApiDescriptionGroupCollectionProvider _apiProvider;
+    private readonly SqlSugarRepository<SysUser> _sysUserRep;
+    private readonly GiteeOptions _giteeOptions;
 
-    public SysCommonService(IApiDescriptionGroupCollectionProvider apiProvider)
+    public SysCommonService(IApiDescriptionGroupCollectionProvider apiProvider,
+        SqlSugarRepository<SysUser> sysUserRep,
+        IOptions<GiteeOptions> giteeOptions)
     {
+        _sysUserRep = sysUserRep;
         _apiProvider = apiProvider;
+        _giteeOptions = giteeOptions.Value;
     }
 
     /// <summary>

+ 171 - 0
Admin.NET/Admin.NET.Core/Service/Update/SysUpdateService.cs

@@ -0,0 +1,171 @@
+// Admin.NET 项目的版权、商标、专利和其他相关权利均受相应法律法规的保护。使用本项目应遵守相关法律法规和许可证的要求。
+//
+// 本项目主要遵循 MIT 许可证和 Apache 许可证(版本 2.0)进行分发和使用。许可证位于源代码树根目录中的 LICENSE-MIT 和 LICENSE-APACHE 文件。
+//
+// 不得利用本项目从事危害国家安全、扰乱社会秩序、侵犯他人合法权益等法律法规禁止的活动!任何基于本项目二次开发而产生的一切法律纠纷和责任,我们不承担任何责任!
+
+using System.IO.Compression;
+
+namespace Admin.NET.Core.Service;
+
+/// <summary>
+/// 系统更新管理服务 🧩
+/// </summary>
+[ApiDescriptionSettings(Order = 390)]
+public class SysUpdateService : IDynamicApiController, ITransient
+{
+    private readonly SqlSugarRepository<SysUser> _sysUserRep;
+    private readonly SysOnlineUserService _onlineUserService;
+    private readonly SysCacheService _sysCacheService;
+    private readonly GiteeOptions _giteeOptions;
+    private readonly UserManager _userManager;
+
+    public SysUpdateService(
+        SqlSugarRepository<SysUser> sysUserRep,
+        SysOnlineUserService onlineUserService,
+        IOptions<GiteeOptions> giteeOptions,
+        SysCacheService sysCacheService,
+        UserManager userManager)
+    {
+        _sysUserRep = sysUserRep;
+        _userManager = userManager;
+        _giteeOptions = giteeOptions.Value;
+        _sysCacheService = sysCacheService;
+        _onlineUserService = onlineUserService;
+    }
+
+    /// <summary>
+    /// 从远端更新项目
+    /// </summary>
+    /// <returns></returns>
+    public async Task Update()
+    {
+        var originColor = Console.ForegroundColor;
+        Console.ForegroundColor = ConsoleColor.Yellow;
+        Console.WriteLine($"【{DateTime.Now}】从远端仓库部署项目");
+        try
+        {
+            await SendMessage("----------------------------从远端仓库部署项目-开始----------------------------");
+            await SendMessage($"仓库地址:https://gitee.com/{_giteeOptions.Owner}/{_giteeOptions.Repo}.git");
+            await SendMessage($"仓库分支:{_giteeOptions.Branch}");
+
+            await SendMessage("项目备份...");
+            // TODO 备份项目
+
+            // 获取解压后的根目录
+            var rootPath = Path.GetFullPath(Path.Combine(_giteeOptions.OutputDir.BackEnd, ".."));
+            var tempDir = Path.Combine(rootPath, $"{_giteeOptions.Repo}-{_giteeOptions.Branch}");
+
+            await SendMessage("清理旧文件...");
+            TryDeleteFileOrDir(tempDir);
+
+            await SendMessage("拉取远端代码...");
+            var stream = await GiteeHelper.DownloadRepoZip(_giteeOptions.Owner, _giteeOptions.Repo,
+                _giteeOptions.AccessToken, _giteeOptions.Branch);
+
+            await SendMessage("文件包解压...");
+            using ZipArchive archive = new(stream, ZipArchiveMode.Read, leaveOpen: false);
+            archive.ExtractToDirectory(rootPath);
+
+            // 项目目录
+            var backendDir = "Admin.NET";
+            var entryProjectName = "Admin.NET.Web.Entry";
+
+            await SendMessage("编译项目...");
+            await SendMessage($"发布版本:{_giteeOptions.Publish.Configuration}");
+            await SendMessage($"目标框架:{_giteeOptions.Publish.TargetFramework}");
+            await SendMessage($"运行环境:{_giteeOptions.Publish.RuntimeIdentifier}");
+            var option = _giteeOptions.Publish;
+            var adminNetDir = Path.Combine(tempDir, backendDir);
+            var args =
+                $"publish \"{entryProjectName}\" -c {option.Configuration} -f {option.TargetFramework} -r {option.RuntimeIdentifier} --output \"{_giteeOptions.OutputDir.BackEnd}\"";
+            await RunCommandAsync("dotnet", args, adminNetDir);
+
+            await SendMessage("清理文件...");
+            TryDeleteFileOrDir(tempDir);
+
+            await SendMessage("----------------------------从远端仓库部署项目-结束----------------------------");
+        }
+        catch (Exception ex)
+        {
+            await SendMessage("发生异常:" + ex.Message);
+            throw;
+        }
+        finally
+        {
+            Console.ForegroundColor = originColor;
+        }
+    }
+
+    /// <summary>
+    /// 推送消息
+    /// </summary>
+    /// <param name="message"></param>
+    public Task SendMessage(string message)
+    {
+        var logList = _sysCacheService.Get<List<string>>(CacheConst.KeySysUpdateLog) ?? new();
+
+        var content = $"【{DateTime.Now}】 {message}";
+
+        Console.WriteLine(content);
+
+        logList.Add(content);
+
+        _sysCacheService.Set(CacheConst.KeySysUpdateLog, logList);
+
+        return Task.CompletedTask;
+    }
+
+    /// <summary>
+    /// 执行命令
+    /// </summary>
+    /// <param name="command">命令</param>
+    /// <param name="arguments">参数</param>
+    /// <param name="workingDirectory">工作目录</param>
+    private async Task RunCommandAsync(string command, string arguments, string workingDirectory)
+    {
+        var processStartInfo = new ProcessStartInfo
+        {
+            FileName = command,
+            Arguments = arguments,
+            WorkingDirectory = workingDirectory,
+            RedirectStandardOutput = true,
+            RedirectStandardError = true,
+            StandardOutputEncoding = Encoding.UTF8,
+            StandardErrorEncoding = Encoding.UTF8,
+            UseShellExecute = false,
+            CreateNoWindow = true
+        };
+
+        using var process = new Process();
+        process.StartInfo = processStartInfo;
+        process.Start();
+
+        while (!process.StandardOutput.EndOfStream)
+        {
+            string line = await process.StandardOutput.ReadLineAsync();
+            if (string.IsNullOrEmpty(line)) continue;
+            await SendMessage(line.Trim());
+        }
+        await process.WaitForExitAsync();
+    }
+
+    /// <summary>
+    /// 尝试删除文件/目录
+    /// </summary>
+    /// <param name="path"></param>
+    /// <returns></returns>
+    private void TryDeleteFileOrDir(string path)
+    {
+        try
+        {
+            if (string.IsNullOrEmpty(path)) return;
+            if (Directory.Exists(path)) Directory.Delete(path, recursive: true);
+            else File.Delete(path);
+        }
+        catch (Exception)
+        {
+            // ignored
+        }
+    }
+}

+ 61 - 0
Admin.NET/Admin.NET.Core/Utils/GiteeHelper.cs

@@ -0,0 +1,61 @@
+// Admin.NET 项目的版权、商标、专利和其他相关权利均受相应法律法规的保护。使用本项目应遵守相关法律法规和许可证的要求。
+//
+// 本项目主要遵循 MIT 许可证和 Apache 许可证(版本 2.0)进行分发和使用。许可证位于源代码树根目录中的 LICENSE-MIT 和 LICENSE-APACHE 文件。
+//
+// 不得利用本项目从事危害国家安全、扰乱社会秩序、侵犯他人合法权益等法律法规禁止的活动!任何基于本项目二次开发而产生的一切法律纠纷和责任,我们不承担任何责任!
+
+using System.Diagnostics.CodeAnalysis;
+
+namespace Admin.NET.Core;
+
+/// <summary>
+/// Gitee接口帮助类
+/// </summary>
+public class GiteeHelper
+{
+    private const string BaseUrl = "https://gitee.com/api/v5/repos/";
+    private static readonly HttpClient Client = new();
+
+    /// <summary>
+    /// 下载仓库 zip
+    /// </summary>
+    /// <remarks>https://gitee.com/api/v5/swagger#/getV5ReposOwnerRepoZipball</remarks>
+    /// <returns></returns>
+    public static async Task<Stream> DownloadRepoZip(string owner, string repo, string accessToken = null, string @ref = null)
+    {
+        if (string.IsNullOrWhiteSpace(owner)) throw Oops.Bah($"参数 {nameof(owner)} 不能为空");
+        if (string.IsNullOrWhiteSpace(repo)) throw Oops.Bah($"参数 {nameof(repo)} 不能为空");
+        var query = BuilderQueryString(new
+        {
+            access_token = accessToken,
+            @ref
+        });
+        return await Client.GetStreamAsync($"{BaseUrl}{owner}/{repo}/zipball?{query}");
+    }
+
+    /// <summary>
+    /// 构建Query参数
+    /// </summary>
+    /// <returns></returns>
+    private static string BuilderQueryString([NotNull]object obj)
+    {
+        if (obj == null) return string.Empty;
+        var query = HttpUtility.ParseQueryString(string.Empty);
+        foreach (var prop in obj.GetType().GetProperties())
+        {
+            var val = prop.GetValue(obj);
+            if (val == null) continue;
+
+            // 以元组形式校验参数集
+            var name = prop.Name.Trim('@');
+            if (val is Tuple<object, string> { Item1: not null } tuple)
+            {
+                if (!tuple.Item2.Split(",").Any(x => x.Trim().Equals(tuple.Item1))) throw Oops.Oh($"参数 {name} 的值只能为:{tuple.Item2}");
+                query[name] = tuple.Item1.ToString();
+                continue;
+            }
+            query[name] = val.ToString();
+        }
+        return query.ToString();
+    }
+}

+ 1 - 0
Admin.NET/Admin.NET.Web.Core/ProjectOptions.cs

@@ -37,6 +37,7 @@ public static class ProjectOptions
         services.AddConfigurableOptions<CryptogramOptions>();
         services.AddConfigurableOptions<SMSOptions>();
         services.AddConfigurableOptions<EventBusOptions>();
+        services.AddConfigurableOptions<GiteeOptions>();
         services.Configure<IpRateLimitOptions>(App.Configuration.GetSection("IpRateLimiting"));
         services.Configure<IpRateLimitPolicies>(App.Configuration.GetSection("IpRateLimitPolicies"));
         services.Configure<ClientRateLimitOptions>(App.Configuration.GetSection("ClientRateLimiting"));