SysUpdateService.cs 13 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347
  1. // Admin.NET 项目的版权、商标、专利和其他相关权利均受相应法律法规的保护。使用本项目应遵守相关法律法规和许可证的要求。
  2. //
  3. // 本项目主要遵循 MIT 许可证和 Apache 许可证(版本 2.0)进行分发和使用。许可证位于源代码树根目录中的 LICENSE-MIT 和 LICENSE-APACHE 文件。
  4. //
  5. // 不得利用本项目从事危害国家安全、扰乱社会秩序、侵犯他人合法权益等法律法规禁止的活动!任何基于本项目二次开发而产生的一切法律纠纷和责任,我们不承担任何责任!
  6. using System.IO.Compression;
  7. using DocumentFormat.OpenXml.Office2010.ExcelAc;
  8. namespace Admin.NET.Core.Service;
  9. /// <summary>
  10. /// 系统更新管理服务 🧩
  11. /// </summary>
  12. [ApiDescriptionSettings(Order = 390)]
  13. public class SysUpdateService : IDynamicApiController, ITransient
  14. {
  15. private readonly SqlSugarRepository<SysUser> _sysUserRep;
  16. private readonly SysOnlineUserService _onlineUserService;
  17. private readonly SysCacheService _sysCacheService;
  18. private readonly CDConfigOptions _cdConfigOptions;
  19. private readonly UserManager _userManager;
  20. public SysUpdateService(
  21. SqlSugarRepository<SysUser> sysUserRep,
  22. SysOnlineUserService onlineUserService,
  23. IOptions<CDConfigOptions> giteeOptions,
  24. SysCacheService sysCacheService,
  25. UserManager userManager)
  26. {
  27. _sysUserRep = sysUserRep;
  28. _userManager = userManager;
  29. _cdConfigOptions = giteeOptions.Value;
  30. _sysCacheService = sysCacheService;
  31. _onlineUserService = onlineUserService;
  32. }
  33. /// <summary>
  34. /// 备份列表
  35. /// </summary>
  36. /// <returns></returns>
  37. [DisplayName("备份列表")]
  38. [ApiDescriptionSettings(Name = "List"), HttpPost]
  39. public Task<List<BackupOutput>> List()
  40. {
  41. const string backendDir = "Admin.NET";
  42. var rootPath = Path.GetFullPath(Path.Combine(_cdConfigOptions.BackendOutput, ".."));
  43. return Task.FromResult(Directory.GetFiles(rootPath, backendDir + "*.zip", SearchOption.TopDirectoryOnly)
  44. .Select(filePath =>
  45. {
  46. var file = new FileInfo(filePath);
  47. return new BackupOutput
  48. {
  49. CreateTime = file.CreationTime,
  50. FilePath = filePath,
  51. FileName = file.Name
  52. };
  53. })
  54. .OrderByDescending(u => u.CreateTime)
  55. .ToList());
  56. }
  57. /// <summary>
  58. /// 还原
  59. /// </summary>
  60. /// <returns></returns>
  61. [DisplayName("还原")]
  62. [ApiDescriptionSettings(Name = "Restore"), HttpPost]
  63. public async Task Restore(RestoreInput input)
  64. {
  65. // 检查参数
  66. CheckConfig();
  67. try
  68. {
  69. var file = (await List()).FirstOrDefault(u => u.FileName.EqualIgnoreCase(input.FileName));
  70. if (file == null)
  71. {
  72. await PrintfLog("文件不存在...");
  73. return;
  74. }
  75. await PrintfLog("正在还原...");
  76. using ZipArchive archive = new(File.OpenRead(file.FilePath), ZipArchiveMode.Read, leaveOpen: false);
  77. archive.ExtractToDirectory(_cdConfigOptions.BackendOutput, true);
  78. await PrintfLog("还原成功...");
  79. }
  80. catch (Exception ex)
  81. {
  82. await PrintfLog("发生异常:" + ex.Message);
  83. throw;
  84. }
  85. }
  86. /// <summary>
  87. /// 从远端更新系统
  88. /// </summary>
  89. /// <returns></returns>
  90. [DisplayName("系统更新")]
  91. [ApiDescriptionSettings(Name = "Update"), HttpPost]
  92. public async Task Update()
  93. {
  94. var originColor = Console.ForegroundColor;
  95. Console.ForegroundColor = ConsoleColor.Yellow;
  96. Console.WriteLine($"【{DateTime.Now}】从远端仓库部署项目");
  97. try
  98. {
  99. await PrintfLog("----------------------------从远端仓库部署项目-开始----------------------------");
  100. // 检查参数
  101. await PrintfLog("检查参数...");
  102. CheckConfig();
  103. // 检查操作间隔
  104. if (_cdConfigOptions.UpdateInterval > 0)
  105. {
  106. if (_sysCacheService.Get<bool>(CacheConst.KeySysUpdateInterval)) throw Oops.Oh("请勿频繁操作");
  107. _sysCacheService.Set(CacheConst.KeySysUpdateInterval, true, TimeSpan.FromMinutes(_cdConfigOptions.UpdateInterval));
  108. }
  109. await PrintfLog($"客户端host:{App.HttpContext.Request.Host}");
  110. await PrintfLog($"客户端IP:{App.HttpContext.GetRemoteIpAddressToIPv4(true)}");
  111. await PrintfLog($"仓库地址:https://gitee.com/{_cdConfigOptions.Owner}/{_cdConfigOptions.Repo}.git");
  112. await PrintfLog($"仓库分支:{_cdConfigOptions.Branch}");
  113. // 获取解压后的根目录
  114. var rootPath = Path.GetFullPath(Path.Combine(_cdConfigOptions.BackendOutput, ".."));
  115. var tempDir = Path.Combine(rootPath, $"{_cdConfigOptions.Repo}-{_cdConfigOptions.Branch}");
  116. await PrintfLog("清理旧文件...");
  117. FileHelper.TryDelete(tempDir);
  118. await PrintfLog("拉取远端代码...");
  119. var stream = await GiteeHelper.DownloadRepoZip(_cdConfigOptions.Owner, _cdConfigOptions.Repo,
  120. _cdConfigOptions.AccessToken, _cdConfigOptions.Branch);
  121. await PrintfLog("文件包解压...");
  122. using ZipArchive archive = new(stream, ZipArchiveMode.Read, leaveOpen: false);
  123. archive.ExtractToDirectory(rootPath);
  124. // 项目目录
  125. var backendDir = "Admin.NET"; // 后端根目录
  126. var entryProjectName = "Admin.NET.Web.Entry"; // 启动项目目录
  127. var tempOutput = Path.Combine(rootPath, $"{_cdConfigOptions.Repo}_temp");
  128. await PrintfLog("编译项目...");
  129. await PrintfLog($"发布版本:{_cdConfigOptions.Publish.Configuration}");
  130. await PrintfLog($"目标框架:{_cdConfigOptions.Publish.TargetFramework}");
  131. await PrintfLog($"运行环境:{_cdConfigOptions.Publish.RuntimeIdentifier}");
  132. var option = _cdConfigOptions.Publish;
  133. var adminNetDir = Path.Combine(tempDir, backendDir);
  134. var args = $"publish \"{entryProjectName}\" -c {option.Configuration} -f {option.TargetFramework} -r {option.RuntimeIdentifier} --output \"{tempOutput}\"";
  135. await RunCommandAsync("dotnet", args, adminNetDir);
  136. await PrintfLog("复制 wwwroot 目录...");
  137. var wwwrootDir = Path.Combine(adminNetDir, entryProjectName, "wwwroot");
  138. FileHelper.CopyDirectory(wwwrootDir, Path.Combine(tempOutput, "wwwroot"), true);
  139. // 删除排除文件
  140. foreach (var filePath in (_cdConfigOptions.ExcludeFiles ?? new()).SelectMany(file => Directory.GetFiles(tempOutput, file, SearchOption.TopDirectoryOnly)))
  141. {
  142. await PrintfLog($"排除文件:{filePath}");
  143. FileHelper.TryDelete(filePath);
  144. }
  145. await PrintfLog("备份原项目文件...");
  146. string backupPath = Path.Combine(rootPath, $"{_cdConfigOptions.Repo}_{DateTime.Now:yyyy_MM_dd}.zip") ;
  147. if (File.Exists(backupPath)) File.Delete(backupPath);
  148. ZipFile.CreateFromDirectory(_cdConfigOptions.BackendOutput, backupPath);
  149. // 将临时文件移动到正式目录
  150. FileHelper.CopyDirectory(tempOutput, _cdConfigOptions.BackendOutput, true);
  151. await PrintfLog("清理文件...");
  152. FileHelper.TryDelete(tempOutput);
  153. FileHelper.TryDelete(tempDir);
  154. if (_cdConfigOptions.BackupCount > 0)
  155. {
  156. var fileList = await List();
  157. if (fileList.Count > _cdConfigOptions.BackupCount)
  158. await PrintfLog("清除多余的备份文件...");
  159. while (fileList.Count > _cdConfigOptions.BackupCount)
  160. {
  161. var last = fileList.Last();
  162. FileHelper.TryDelete(last.FilePath);
  163. fileList.Remove(last);
  164. }
  165. }
  166. await PrintfLog("重启项目后生效...");
  167. await PrintfLog("----------------------------从远端仓库部署项目-结束----------------------------");
  168. }
  169. catch (Exception ex)
  170. {
  171. await PrintfLog("发生异常:" + ex.Message);
  172. throw;
  173. }
  174. finally
  175. {
  176. Console.ForegroundColor = originColor;
  177. }
  178. }
  179. /// <summary>
  180. /// 仓库WebHook接口
  181. /// </summary>
  182. /// <returns></returns>
  183. [AllowAnonymous]
  184. [DisplayName("仓库WebHook接口")]
  185. [ApiDescriptionSettings(Name = "WebHook"), HttpPost]
  186. public async Task WebHook(WebHookInput input)
  187. {
  188. if (CryptogramUtil.Decrypt(input.Key) != GetWebHookKeyPlainText()) throw Oops.Oh("非法密钥");
  189. var updateInterval = _cdConfigOptions.UpdateInterval;
  190. try
  191. {
  192. _cdConfigOptions.UpdateInterval = 0;
  193. await Update();
  194. }
  195. finally
  196. {
  197. _cdConfigOptions.UpdateInterval = updateInterval;
  198. }
  199. }
  200. /// <summary>
  201. /// 获取WebHook接口密钥
  202. /// </summary>
  203. /// <returns></returns>
  204. [DisplayName("获取WebHook接口密钥")]
  205. [ApiDescriptionSettings(Name = "WebHookKey"), HttpGet]
  206. public string GetWebHookKey()
  207. {
  208. return CryptogramUtil.Encrypt(GetWebHookKeyPlainText());
  209. }
  210. /// <summary>
  211. /// 获取日志列表
  212. /// </summary>
  213. /// <returns></returns>
  214. [DisplayName("获取日志列表")]
  215. [ApiDescriptionSettings(Name = "Logs"), HttpGet]
  216. public List<string> LogList()
  217. {
  218. return _sysCacheService.Get<List<string>>(CacheConst.KeySysUpdateLog) ?? new();
  219. }
  220. /// <summary>
  221. /// 清空日志
  222. /// </summary>
  223. /// <returns></returns>
  224. [DisplayName("清空日志")]
  225. [ApiDescriptionSettings(Name = "Clear"), HttpGet]
  226. public void ClearLog()
  227. {
  228. _sysCacheService.Remove(CacheConst.KeySysUpdateLog);
  229. }
  230. /// <summary>
  231. /// 获取密钥明文
  232. /// </summary>
  233. /// <returns></returns>
  234. private string GetWebHookKeyPlainText()
  235. {
  236. return $"https://gitee.com/{_cdConfigOptions.Owner}/{_cdConfigOptions.Repo}.git-{_cdConfigOptions.Branch}-{_cdConfigOptions.AccessToken}";
  237. }
  238. /// <summary>
  239. /// 检查参数
  240. /// </summary>
  241. /// <returns></returns>
  242. private void CheckConfig()
  243. {
  244. if (_cdConfigOptions == null) throw Oops.Oh("CDConfig配置不能为空");
  245. if (string.IsNullOrWhiteSpace(_cdConfigOptions.Owner)) throw Oops.Oh("仓库用户名不能为空");
  246. if (string.IsNullOrWhiteSpace(_cdConfigOptions.Repo)) throw Oops.Oh("仓库名不能为空");
  247. // if (string.IsNullOrWhiteSpace(_cdConfigOptions.Branch)) throw Oops.Oh("分支名不能为空");
  248. if (string.IsNullOrWhiteSpace(_cdConfigOptions.AccessToken)) throw Oops.Oh("授权信息不能为空");
  249. if (string.IsNullOrWhiteSpace(_cdConfigOptions.BackendOutput)) throw Oops.Oh("部署目录不能为空");
  250. if (_cdConfigOptions.Publish == null) throw Oops.Oh("编译配置不能为空");
  251. if (string.IsNullOrWhiteSpace(_cdConfigOptions.Publish.Configuration)) throw Oops.Oh("运行环境编译配置不能为空");
  252. if (string.IsNullOrWhiteSpace(_cdConfigOptions.Publish.TargetFramework)) throw Oops.Oh(".NET版本编译配置不能为空");
  253. if (string.IsNullOrWhiteSpace(_cdConfigOptions.Publish.RuntimeIdentifier)) throw Oops.Oh("运行平台配置不能为空");
  254. }
  255. /// <summary>
  256. /// 打印日志
  257. /// </summary>
  258. /// <param name="message"></param>
  259. private Task PrintfLog(string message)
  260. {
  261. var logList = _sysCacheService.Get<List<string>>(CacheConst.KeySysUpdateLog) ?? new();
  262. var content = $"【{DateTime.Now}】 {message}";
  263. Console.WriteLine(content);
  264. logList.Add(content);
  265. _sysCacheService.Set(CacheConst.KeySysUpdateLog, logList);
  266. return Task.CompletedTask;
  267. }
  268. /// <summary>
  269. /// 执行命令
  270. /// </summary>
  271. /// <param name="command">命令</param>
  272. /// <param name="arguments">参数</param>
  273. /// <param name="workingDirectory">工作目录</param>
  274. private async Task RunCommandAsync(string command, string arguments, string workingDirectory)
  275. {
  276. var processStartInfo = new ProcessStartInfo
  277. {
  278. FileName = command,
  279. Arguments = arguments,
  280. WorkingDirectory = workingDirectory,
  281. RedirectStandardOutput = true,
  282. RedirectStandardError = true,
  283. StandardOutputEncoding = Encoding.UTF8,
  284. StandardErrorEncoding = Encoding.UTF8,
  285. UseShellExecute = false,
  286. CreateNoWindow = true
  287. };
  288. using var process = new Process();
  289. process.StartInfo = processStartInfo;
  290. process.Start();
  291. while (!process.StandardOutput.EndOfStream)
  292. {
  293. string line = await process.StandardOutput.ReadLineAsync();
  294. if (string.IsNullOrEmpty(line)) continue;
  295. await PrintfLog(line.Trim());
  296. }
  297. await process.WaitForExitAsync();
  298. }
  299. }