// Admin.NET 项目的版权、商标、专利和其他相关权利均受相应法律法规的保护。使用本项目应遵守相关法律法规和许可证的要求。 // // 本项目主要遵循 MIT 许可证和 Apache 许可证(版本 2.0)进行分发和使用。许可证位于源代码树根目录中的 LICENSE-MIT 和 LICENSE-APACHE 文件。 // // 不得利用本项目从事危害国家安全、扰乱社会秩序、侵犯他人合法权益等法律法规禁止的活动!任何基于本项目二次开发而产生的一切法律纠纷和责任,我们不承担任何责任! using System.IO.Compression; using DocumentFormat.OpenXml.Office2010.ExcelAc; namespace Admin.NET.Core.Service; /// /// 系统更新管理服务 🧩 /// [ApiDescriptionSettings(Order = 390)] public class SysUpdateService : IDynamicApiController, ITransient { private readonly SqlSugarRepository _sysUserRep; private readonly SysOnlineUserService _onlineUserService; private readonly SysCacheService _sysCacheService; private readonly CDConfigOptions _cdConfigOptions; private readonly UserManager _userManager; public SysUpdateService( SqlSugarRepository sysUserRep, SysOnlineUserService onlineUserService, IOptions giteeOptions, SysCacheService sysCacheService, UserManager userManager) { _sysUserRep = sysUserRep; _userManager = userManager; _cdConfigOptions = giteeOptions.Value; _sysCacheService = sysCacheService; _onlineUserService = onlineUserService; } /// /// 备份列表 /// /// [DisplayName("备份列表")] [ApiDescriptionSettings(Name = "List"), HttpPost] public Task> List() { const string backendDir = "Admin.NET"; var rootPath = Path.GetFullPath(Path.Combine(_cdConfigOptions.BackendOutput, "..")); return Task.FromResult(Directory.GetFiles(rootPath, backendDir + "*.zip", SearchOption.TopDirectoryOnly) .Select(filePath => { var file = new FileInfo(filePath); return new BackupOutput { CreateTime = file.CreationTime, FilePath = filePath, FileName = file.Name }; }) .OrderByDescending(u => u.CreateTime) .ToList()); } /// /// 还原 /// /// [DisplayName("还原")] [ApiDescriptionSettings(Name = "Restore"), HttpPost] public async Task Restore(RestoreInput input) { // 检查参数 CheckConfig(); try { var file = (await List()).FirstOrDefault(u => u.FileName.EqualIgnoreCase(input.FileName)); if (file == null) { await PrintfLog("文件不存在..."); return; } await PrintfLog("正在还原..."); using ZipArchive archive = new(File.OpenRead(file.FilePath), ZipArchiveMode.Read, leaveOpen: false); archive.ExtractToDirectory(_cdConfigOptions.BackendOutput, true); await PrintfLog("还原成功..."); } catch (Exception ex) { await PrintfLog("发生异常:" + ex.Message); throw; } } /// /// 从远端更新系统 /// /// [DisplayName("系统更新")] [ApiDescriptionSettings(Name = "Update"), HttpPost] public async Task Update() { var originColor = Console.ForegroundColor; Console.ForegroundColor = ConsoleColor.Yellow; Console.WriteLine($"【{DateTime.Now}】从远端仓库部署项目"); try { await PrintfLog("----------------------------从远端仓库部署项目-开始----------------------------"); // 检查参数 await PrintfLog("检查参数..."); CheckConfig(); // 检查操作间隔 if (_cdConfigOptions.UpdateInterval > 0) { if (_sysCacheService.Get(CacheConst.KeySysUpdateInterval)) throw Oops.Oh("请勿频繁操作"); _sysCacheService.Set(CacheConst.KeySysUpdateInterval, true, TimeSpan.FromMinutes(_cdConfigOptions.UpdateInterval)); } await PrintfLog($"客户端host:{App.HttpContext.Request.Host}"); await PrintfLog($"客户端IP:{App.HttpContext.GetRemoteIpAddressToIPv4(true)}"); await PrintfLog($"仓库地址:https://gitee.com/{_cdConfigOptions.Owner}/{_cdConfigOptions.Repo}.git"); await PrintfLog($"仓库分支:{_cdConfigOptions.Branch}"); // 获取解压后的根目录 var rootPath = Path.GetFullPath(Path.Combine(_cdConfigOptions.BackendOutput, "..")); var tempDir = Path.Combine(rootPath, $"{_cdConfigOptions.Repo}-{_cdConfigOptions.Branch}"); await PrintfLog("清理旧文件..."); FileHelper.TryDelete(tempDir); await PrintfLog("拉取远端代码..."); var stream = await GiteeHelper.DownloadRepoZip(_cdConfigOptions.Owner, _cdConfigOptions.Repo, _cdConfigOptions.AccessToken, _cdConfigOptions.Branch); await PrintfLog("文件包解压..."); using ZipArchive archive = new(stream, ZipArchiveMode.Read, leaveOpen: false); archive.ExtractToDirectory(rootPath); // 项目目录 var backendDir = "Admin.NET"; // 后端根目录 var entryProjectName = "Admin.NET.Web.Entry"; // 启动项目目录 var tempOutput = Path.Combine(rootPath, $"{_cdConfigOptions.Repo}_temp"); await PrintfLog("编译项目..."); await PrintfLog($"发布版本:{_cdConfigOptions.Publish.Configuration}"); await PrintfLog($"目标框架:{_cdConfigOptions.Publish.TargetFramework}"); await PrintfLog($"运行环境:{_cdConfigOptions.Publish.RuntimeIdentifier}"); var option = _cdConfigOptions.Publish; var adminNetDir = Path.Combine(tempDir, backendDir); var args = $"publish \"{entryProjectName}\" -c {option.Configuration} -f {option.TargetFramework} -r {option.RuntimeIdentifier} --output \"{tempOutput}\""; await RunCommandAsync("dotnet", args, adminNetDir); await PrintfLog("复制 wwwroot 目录..."); var wwwrootDir = Path.Combine(adminNetDir, entryProjectName, "wwwroot"); FileHelper.CopyDirectory(wwwrootDir, Path.Combine(tempOutput, "wwwroot"), true); // 删除排除文件 foreach (var filePath in (_cdConfigOptions.ExcludeFiles ?? new()).SelectMany(file => Directory.GetFiles(tempOutput, file, SearchOption.TopDirectoryOnly))) { await PrintfLog($"排除文件:{filePath}"); FileHelper.TryDelete(filePath); } await PrintfLog("备份原项目文件..."); string backupPath = Path.Combine(rootPath, $"{_cdConfigOptions.Repo}_{DateTime.Now:yyyy_MM_dd}.zip") ; if (File.Exists(backupPath)) File.Delete(backupPath); ZipFile.CreateFromDirectory(_cdConfigOptions.BackendOutput, backupPath); // 将临时文件移动到正式目录 FileHelper.CopyDirectory(tempOutput, _cdConfigOptions.BackendOutput, true); await PrintfLog("清理文件..."); FileHelper.TryDelete(tempOutput); FileHelper.TryDelete(tempDir); if (_cdConfigOptions.BackupCount > 0) { var fileList = await List(); if (fileList.Count > _cdConfigOptions.BackupCount) await PrintfLog("清除多余的备份文件..."); while (fileList.Count > _cdConfigOptions.BackupCount) { var last = fileList.Last(); FileHelper.TryDelete(last.FilePath); fileList.Remove(last); } } await PrintfLog("重启项目后生效..."); await PrintfLog("----------------------------从远端仓库部署项目-结束----------------------------"); } catch (Exception ex) { await PrintfLog("发生异常:" + ex.Message); throw; } finally { Console.ForegroundColor = originColor; } } /// /// 仓库WebHook接口 /// /// [AllowAnonymous] [DisplayName("仓库WebHook接口")] [ApiDescriptionSettings(Name = "WebHook"), HttpPost] public async Task WebHook(WebHookInput input) { if (CryptogramUtil.Decrypt(input.Key) != GetWebHookKeyPlainText()) throw Oops.Oh("非法密钥"); var updateInterval = _cdConfigOptions.UpdateInterval; try { _cdConfigOptions.UpdateInterval = 0; await Update(); } finally { _cdConfigOptions.UpdateInterval = updateInterval; } } /// /// 获取WebHook接口密钥 /// /// [DisplayName("获取WebHook接口密钥")] [ApiDescriptionSettings(Name = "WebHookKey"), HttpGet] public string GetWebHookKey() { return CryptogramUtil.Encrypt(GetWebHookKeyPlainText()); } /// /// 获取日志列表 /// /// [DisplayName("获取日志列表")] [ApiDescriptionSettings(Name = "Logs"), HttpGet] public List LogList() { return _sysCacheService.Get>(CacheConst.KeySysUpdateLog) ?? new(); } /// /// 清空日志 /// /// [DisplayName("清空日志")] [ApiDescriptionSettings(Name = "Clear"), HttpGet] public void ClearLog() { _sysCacheService.Remove(CacheConst.KeySysUpdateLog); } /// /// 获取密钥明文 /// /// private string GetWebHookKeyPlainText() { return $"https://gitee.com/{_cdConfigOptions.Owner}/{_cdConfigOptions.Repo}.git-{_cdConfigOptions.Branch}-{_cdConfigOptions.AccessToken}"; } /// /// 检查参数 /// /// private void CheckConfig() { if (_cdConfigOptions == null) throw Oops.Oh("CDConfig配置不能为空"); if (string.IsNullOrWhiteSpace(_cdConfigOptions.Owner)) throw Oops.Oh("仓库用户名不能为空"); if (string.IsNullOrWhiteSpace(_cdConfigOptions.Repo)) throw Oops.Oh("仓库名不能为空"); // if (string.IsNullOrWhiteSpace(_cdConfigOptions.Branch)) throw Oops.Oh("分支名不能为空"); if (string.IsNullOrWhiteSpace(_cdConfigOptions.AccessToken)) throw Oops.Oh("授权信息不能为空"); if (string.IsNullOrWhiteSpace(_cdConfigOptions.BackendOutput)) throw Oops.Oh("部署目录不能为空"); if (_cdConfigOptions.Publish == null) throw Oops.Oh("编译配置不能为空"); if (string.IsNullOrWhiteSpace(_cdConfigOptions.Publish.Configuration)) throw Oops.Oh("运行环境编译配置不能为空"); if (string.IsNullOrWhiteSpace(_cdConfigOptions.Publish.TargetFramework)) throw Oops.Oh(".NET版本编译配置不能为空"); if (string.IsNullOrWhiteSpace(_cdConfigOptions.Publish.RuntimeIdentifier)) throw Oops.Oh("运行平台配置不能为空"); } /// /// 打印日志 /// /// private Task PrintfLog(string message) { var logList = _sysCacheService.Get>(CacheConst.KeySysUpdateLog) ?? new(); var content = $"【{DateTime.Now}】 {message}"; Console.WriteLine(content); logList.Add(content); _sysCacheService.Set(CacheConst.KeySysUpdateLog, logList); return Task.CompletedTask; } /// /// 执行命令 /// /// 命令 /// 参数 /// 工作目录 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 PrintfLog(line.Trim()); } await process.WaitForExitAsync(); } }