// 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();
}
}