UNIVPLMDataIntegration/Admin.NET/Plugins/Admin.NET.Plugin.PluginCore/Controllers/PluginsController.cs
2025-03-09 01:21:30 +08:00

709 lines
26 KiB
C#
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

// Admin.NET 项目的版权、商标、专利和其他相关权利均受相应法律法规的保护。使用本项目应遵守相关法律法规和许可证的要求。
//
// 本项目主要遵循 MIT 许可证和 Apache 许可证(版本 2.0)进行分发和使用。许可证位于源代码树根目录中的 LICENSE-MIT 和 LICENSE-APACHE 文件。
//
// 不得利用本项目从事危害国家安全、扰乱社会秩序、侵犯他人合法权益等法律法规禁止的活动!任何基于本项目二次开发而产生的一切法律纠纷和责任,我们不承担任何责任!
using PluginCore.AspNetCore.Interfaces;
using PluginCore.AspNetCore.ResponseModel;
using PluginCore.Infrastructure;
using PluginCore.Interfaces;
using PluginCore.IPlugins;
using PluginCore.Models;
namespace PluginCore.AspNetCore.Controllers;
[Route("api/plugincore/admin/[controller]/[action]")]
// [PluginCoreAdminAuthorize]
[ApiController]
[NonUnify]
public class PluginsController : ControllerBase
{
#region Fields
private readonly IPluginManager _pluginManager;
private readonly IPluginFinder _pluginFinder;
private readonly IPluginApplicationBuilderManager _pluginApplicationBuilderManager;
#endregion Fields
#region Ctor
public PluginsController(IPluginManager pluginManager, IPluginFinder pluginFinder, IPluginApplicationBuilderManager pluginApplicationBuilderManager)
{
_pluginManager = pluginManager;
_pluginFinder = pluginFinder;
_pluginApplicationBuilderManager = pluginApplicationBuilderManager;
}
#endregion Ctor
#region Actions
#region
/// <summary>
/// 加载插件列表
/// </summary>
/// <param name="status">插件状态</param>
/// <returns></returns>
[HttpGet, HttpPost]
public async Task<ActionResult<BaseResponseModel>> List(string status = "all")
{
BaseResponseModel responseData = new ResponseModel.BaseResponseModel();
var pluginConfigModel = PluginConfigModelFactory.Create();
// 获取所有插件信息
IList<PluginInfoModel> pluginInfoModels = PluginInfoModelFactory.CreateAll();
IList<PluginInfoResponseModel> responseModels = new List<PluginInfoResponseModel>();
string[] enablePluginIds = _pluginFinder.EnablePluginIds().ToArray();
// 添加插件状态
responseModels = PluginInfoModelToResponseModel(pluginInfoModels, pluginConfigModel, enablePluginIds);
#region
switch (status.ToLower())
{
case "all":
break;
case "enabled":
responseModels = responseModels.Where(m => m.Status == PluginStatus.Enabled).ToList();
break;
case "disabled":
responseModels = responseModels.Where(m => m.Status == PluginStatus.Disabled).ToList();
break;
default:
break;
}
#endregion
responseData.Code = 1;
responseData.Message = "加载插件列表成功";
responseData.Data = responseModels;
return await Task.FromResult(responseData);
}
#endregion
#region
[HttpGet, HttpPost]
public async Task<ActionResult<BaseResponseModel>> Uninstall(string pluginId)
{
BaseResponseModel responseData = new BaseResponseModel();
pluginId = pluginId.Trim();
var pluginConfigModel = PluginConfigModelFactory.Create();
// 卸载插件 必须 先禁用插件
#region
if (pluginConfigModel.EnabledPlugins.Contains(pluginId))
{
responseData.Code = -1;
responseData.Message = "卸载失败: 请先禁用此插件";
return await Task.FromResult(responseData);
}
string pluginDirStr = Path.Combine(PluginPathProvider.PluginsRootPath(), pluginId);
string pluginWwwrootDirStr = Path.Combine(PluginPathProvider.PluginsWwwRootDir(), pluginId);
if (!Directory.Exists(pluginDirStr) && !Directory.Exists(pluginWwwrootDirStr))
{
responseData.Code = -2;
responseData.Message = "卸载失败: 此插件不存在";
return await Task.FromResult(responseData);
}
#endregion
try
{
// PS:卸载插件必须先禁用插件所以此时插件LoadContext已被移除释放(插件Assemblies已被释放), 此处不需移除LoadContext
// 1.删除物理文件
var pluginDir = new DirectoryInfo(pluginDirStr);
if (pluginDir.Exists)
{
pluginDir.Delete(true);
}
// 虽然 已禁用 时 pluginWwwrootDirStr/pluginId 已删除, 但为确保, 还是再删除一次
var pluginWwwrootDir = new DirectoryInfo(pluginWwwrootDirStr);
if (pluginWwwrootDir.Exists)
{
pluginWwwrootDir.Delete(true);
}
responseData.Code = 1;
responseData.Message = "卸载成功";
}
catch (Exception ex)
{
responseData.Code = -1;
responseData.Message = "卸载失败: " + ex.Message;
}
return await Task.FromResult(responseData);
}
#endregion
#region
[HttpGet, HttpPost]
public async Task<ActionResult<BaseResponseModel>> Enable(string pluginId)
{
BaseResponseModel responseData = new BaseResponseModel();
var pluginConfigModel = PluginConfigModelFactory.Create();
// 效验是否存在于 已禁用插件列表
#region
pluginId = pluginId.Trim();
var pluginDir = new DirectoryInfo(Path.Combine(PluginPathProvider.PluginsRootPath(), pluginId));
if (pluginDir != null && !pluginDir.Exists)
{
responseData.Code = -1;
responseData.Message = "启用失败: 此插件不存在";
return await Task.FromResult(responseData);
}
string[] enablePluginIds = _pluginFinder.EnablePluginIds().ToArray();
if (enablePluginIds.Contains(pluginId))
{
responseData.Code = -2;
responseData.Message = "启用失败: 此插件已启用";
return await Task.FromResult(responseData);
}
#endregion
try
{
// 1. 创建插件程序集加载上下文, 添加到 PluginsLoadContexts
_pluginManager.LoadPlugin(pluginId);
// 2. 添加到 pluginConfigModel.EnabledPlugins
pluginConfigModel.EnabledPlugins.Add(pluginId);
// 4.保存到 plugin.config.json
PluginConfigModelFactory.Save(pluginConfigModel);
// 5. 找到此插件实例
IPlugin plugin = _pluginFinder.Plugin(pluginId);
if (plugin == null)
{
// 7.启用不成功, 回滚插件状态: (1)释放插件上下文 (2)更新 plugin.config.json
try
{
_pluginManager.UnloadPlugin(pluginId);
}
catch (Exception ex)
{ }
// 从 pluginConfigModel.EnabledPlugins 移除
pluginConfigModel.EnabledPlugins.Remove(pluginId);
// 保存到 plugin.config.json
PluginConfigModelFactory.Save(pluginConfigModel);
responseData.Code = -1;
responseData.Message = "启用失败: 此插件不存在";
return await Task.FromResult(responseData);
}
// 6.调取插件的 AfterEnable(), 插件开发者可在此回收资源
var pluginEnableResult = plugin.AfterEnable();
if (!pluginEnableResult.IsSuccess)
{
// 7.启用不成功, 回滚插件状态: (1)释放插件上下文 (2)更新 plugin.config.json
try
{
_pluginManager.UnloadPlugin(pluginId);
}
catch (Exception ex)
{ }
// 从 pluginConfigModel.EnabledPlugins 移除
pluginConfigModel.EnabledPlugins.Remove(pluginId);
// 保存到 plugin.config.json
PluginConfigModelFactory.Save(pluginConfigModel);
responseData.Code = -1;
responseData.Message = "启用失败: 来自插件的错误信息: " + pluginEnableResult.Message;
return await Task.FromResult(responseData);
}
// 7. ReBuild
this._pluginApplicationBuilderManager.ReBuild();
// 8. 尝试复制 插件下的 wwwroot 到 Plugins_wwwroot
string wwwRootDir = PluginPathProvider.WwwRootDir(pluginId);
if (Directory.Exists(wwwRootDir))
{
string targetDir = PluginPathProvider.PluginWwwRootDir(pluginId);
Utils.FileUtil.CopyFolder(wwwRootDir, targetDir);
}
responseData.Code = 1;
responseData.Message = "启用成功";
}
catch (Exception ex)
{
responseData.Code = -2;
responseData.Message = "启用失败: " + ex.Message;
}
return await Task.FromResult(responseData);
}
#endregion
#region
[HttpGet, HttpPost]
public async Task<ActionResult<BaseResponseModel>> Disable(string pluginId)
{
BaseResponseModel responseData = new BaseResponseModel();
#region
pluginId = pluginId.Trim();
var pluginConfigModel = PluginConfigModelFactory.Create();
// string[] enablePluginIds = _pluginFinder.EnablePluginIds().ToArray();
// // 效验是否存在于 已启用插件列表
// if (!enablePluginIds.Contains(pluginId))
// {
// responseData.Code = -1;
// responseData.Message = "禁用失败: 此插件不存在, 或未启用";
// return await Task.FromResult(responseData);
// }
#endregion
try
{
// 1. 找到此插件实例
IPlugin plugin = _pluginFinder.Plugin(pluginId);
if (plugin == null)
{
responseData.Code = -1;
responseData.Message = "禁用失败: 此插件不存在, 或未启用";
return await Task.FromResult(responseData);
}
try
{
// 2.调取插件的 BeforeDisable(), 插件开发者可在此回收资源
var pluginDisableResult = plugin.BeforeDisable();
if (!pluginDisableResult.IsSuccess)
{
responseData.Code = -1;
responseData.Message = "禁用失败: 来自插件的错误信息: " + pluginDisableResult.Message;
return await Task.FromResult(responseData);
}
// 3.移除插件对应的程序集加载上下文
_pluginManager.UnloadPlugin(pluginId);
// 3.1. ReBuild
this._pluginApplicationBuilderManager.ReBuild();
if (pluginConfigModel.EnabledPlugins.Contains(pluginId))
{
// 4.从 pluginConfigModel.EnabledPlugins 移除
pluginConfigModel.EnabledPlugins.Remove(pluginId);
// 5.保存到 plugin.config.json
PluginConfigModelFactory.Save(pluginConfigModel);
}
}
catch (Exception ex)
{
//Utils.LogUtil.Error(ex.ToString());
responseData.Code = -1;
responseData.Message = "禁用失败: 此插件不存在, 或未启用";
return await Task.FromResult(responseData);
}
// 7. 尝试移除 Plugins_wwwroot/PluginId
string pluginWwwRootDir = PluginPathProvider.PluginWwwRootDir(pluginId);
if (Directory.Exists(pluginWwwRootDir))
{
Directory.Delete(pluginWwwRootDir, true);
}
responseData.Code = 1;
responseData.Message = "禁用成功";
}
catch (Exception ex)
{
responseData.Code = -2;
responseData.Message = "禁用失败: " + ex.Message;
}
return await Task.FromResult(responseData);
}
#endregion
#region
/// <summary>
/// 上传插件
/// </summary>
/// <param name="file">注意: 参数名一定为 file 对应前端传过来时以 file 为名</param>
/// <returns></returns>
[HttpGet, HttpPost]
public async Task<ActionResult<BaseResponseModel>> Upload(IFormFile file)
{
BaseResponseModel responseData = new BaseResponseModel();
#region
if (file == null)
{
responseData.Code = -1;
responseData.Message = "上传的文件不能为空";
return responseData;
}
//文件后缀
string fileExtension = Path.GetExtension(file.FileName);//获取文件格式,拓展名
// 类型标记
UploadFileType uploadFileType = UploadFileType.NoAllowedType;
switch (fileExtension)
{
case ".zip":
uploadFileType = UploadFileType.Zip;
break;
case ".nupkg":
uploadFileType = UploadFileType.Nupkg;
break;
}
if (fileExtension != ".zip" && fileExtension != ".nupkg")
{
responseData.Code = -1;
// nupkg 其实就是 zip
responseData.Message = "只能上传 zip 或 nupkg 格式文件";
return responseData;
}
// PluginCore.AspNetCore-v1.0.2 起 不再限制插件上传大小
//判断文件大小
//var fileSize = file.Length;
//if (fileSize > 1024 * 1024 * 5) // 5M
//{
// responseData.Code = -1;
// responseData.Message = "上传的文件不能大于5MB";
// return responseData;
//}
#endregion
try
{
// 1.先上传到 临时插件上传目录, 用Guid.zip作为保存文件名
string tempZipFilePath = Path.Combine(PluginPathProvider.TempPluginUploadDir(), Guid.NewGuid() + ".zip");
using (var fs = System.IO.File.Create(tempZipFilePath))
{
file.CopyTo(fs); //将上传的文件文件流复制到fs中
fs.Flush();//清空文件流
}
// 2.解压
bool isDecomparessSuccess = false;
if (uploadFileType == UploadFileType.Zip)
{
isDecomparessSuccess = Utils.ZipHelper.DecomparessFile(tempZipFilePath, tempZipFilePath.Replace(".zip", ""));
}
else if (uploadFileType == UploadFileType.Nupkg)
{
isDecomparessSuccess = NupkgService.DecomparessFile(tempZipFilePath, tempZipFilePath.Replace(".zip", ""));
}
// 3.删除原压缩包
System.IO.File.Delete(tempZipFilePath);
if (!isDecomparessSuccess)
{
responseData.Code = -1;
responseData.Message = "解压插件压缩包失败";
return responseData;
}
// 4.读取其中的info.json, 获取 PluginId 值
PluginInfoModel pluginInfoModel = PluginInfoModelFactory.ReadPluginDir(tempZipFilePath.Replace(".zip", ""));
if (pluginInfoModel == null || string.IsNullOrEmpty(pluginInfoModel.PluginId))
{
// 记得删除已不再需要的临时插件文件夹
Directory.Delete(tempZipFilePath.Replace(".zip", ""), true);
responseData.Code = -1;
responseData.Message = "不合法的插件";
return responseData;
}
string pluginId = pluginInfoModel.PluginId;
// 5.检索 此 PluginId 是否本地插件已存在
var pluginConfigModel = PluginConfigModelFactory.Create();
// 本地已经存在的 PluginId
IList<string> localExistPluginIds = PluginPathProvider.AllPluginFolderName();
if (localExistPluginIds.Contains(pluginId))
{
// 记得删除已不再需要的临时插件文件夹
Directory.Delete(tempZipFilePath.Replace(".zip", ""), true);
responseData.Code = -1;
responseData.Message = $"本地已有此插件 (PluginId: {pluginId}), 请前往插件列表删除后, 再上传";
return responseData;
}
// 6.本地无此插件 -> 移动插件文件夹到 Plugins 下, 并以 PluginId 为插件文件夹名
string pluginsRootPath = PluginPathProvider.PluginsRootPath();
string newPluginDir = Path.Combine(pluginsRootPath, pluginId);
Directory.Move(tempZipFilePath.Replace(".zip", ""), newPluginDir);
// 7. 放入 Plugins 中, 默认为 已禁用
responseData.Code = 1;
responseData.Message = $"上传插件成功 (PluginId: {pluginId})";
}
catch (Exception ex)
{
responseData.Code = -1;
responseData.Message = "上传插件失败: " + ex.Message;
ex = ex.InnerException;
while (ex != null)
{
responseData.Message += " - " + ex.InnerException.Message;
ex = ex.InnerException;
}
}
return await Task.FromResult(responseData);
}
#endregion
#region
[HttpGet, HttpPost]
public async Task<ActionResult<BaseResponseModel>> Details(string pluginId)
{
BaseResponseModel responseData = new BaseResponseModel();
try
{
#region
pluginId = pluginId.Trim();
var pluginConfigModel = PluginConfigModelFactory.Create();
string[] localPluginIds = PluginPathProvider.AllPluginFolderName().ToArray();
if (!localPluginIds.Contains(pluginId))
{
responseData.Code = -1;
responseData.Message = $"查看详细失败: 不存在 {pluginId} 插件";
return await Task.FromResult(responseData);
}
#endregion
PluginInfoModel pluginInfoModel = PluginInfoModelFactory.Create(pluginId);
string[] enablePluginIds = _pluginFinder.EnablePluginIds().ToArray();
PluginInfoResponseModel pluginInfoResponseModel = PluginInfoModelToResponseModel(new List<PluginInfoModel>() { pluginInfoModel }, pluginConfigModel, enablePluginIds).FirstOrDefault();
responseData.Code = 1;
responseData.Message = "查看详细成功";
responseData.Data = pluginInfoResponseModel;
}
catch (Exception ex)
{
responseData.Code = -1;
responseData.Message = "查看详细失败: " + ex.Message;
}
return await Task.FromResult(responseData);
}
#endregion
#region
[HttpGet, HttpPost]
public async Task<ActionResult<BaseResponseModel>> Readme(string pluginId)
{
BaseResponseModel responseData = new BaseResponseModel();
try
{
#region
pluginId = pluginId.Trim();
// var pluginConfigModel = PluginConfigModelFactory.Create();
string[] localPluginIds = PluginPathProvider.AllPluginFolderName().ToArray();
if (!localPluginIds.Contains(pluginId))
{
responseData.Code = -1;
responseData.Message = $"查看文档失败: 不存在 {pluginId} 插件";
return await Task.FromResult(responseData);
}
#endregion
PluginReadmeModel readmeModel = PluginReadmeModelFactory.Create(pluginId);
PluginReadmeResponseModel readmeResponseModel = new PluginReadmeResponseModel();
readmeResponseModel.Content = readmeModel?.Content ?? "";
readmeResponseModel.PluginId = pluginId;
responseData.Code = 1;
responseData.Message = "查看文档成功";
responseData.Data = readmeResponseModel;
}
catch (Exception ex)
{
responseData.Code = -1;
responseData.Message = "查看文档失败: " + ex.Message;
}
return await Task.FromResult(responseData);
}
#endregion
#region
[HttpGet]
public async Task<ActionResult<BaseResponseModel>> Settings(string pluginId)
{
BaseResponseModel responseData = new BaseResponseModel();
try
{
#region
pluginId = pluginId.Trim();
// var pluginConfigModel = PluginConfigModelFactory.Create();
string[] localPluginIds = PluginPathProvider.AllPluginFolderName().ToArray();
if (!localPluginIds.Contains(pluginId))
{
responseData.Code = -1;
responseData.Message = $"查看设置失败: 不存在 {pluginId} 插件";
return await Task.FromResult(responseData);
}
#endregion
string settingsJsonStr = PluginSettingsModelFactory.Create(pluginId);
responseData.Code = 1;
responseData.Message = "查看设置成功";
responseData.Data = settingsJsonStr ?? "无设置项";
}
catch (Exception ex)
{
responseData.Code = -1;
responseData.Message = "查看设置失败: " + ex.Message;
}
return await Task.FromResult(responseData);
}
[HttpPost]
public async Task<ActionResult<BaseResponseModel>> Settings(PluginSettingsInputModel inputModel)
{
BaseResponseModel responseData = new BaseResponseModel();
try
{
#region
inputModel.PluginId = inputModel.PluginId.Trim();
// var pluginConfigModel = PluginConfigModelFactory.Create();
string[] localPluginIds = PluginPathProvider.AllPluginFolderName().ToArray();
if (!localPluginIds.Contains(inputModel.PluginId))
{
responseData.Code = -1;
responseData.Message = $"设置失败: 不存在 {inputModel.PluginId} 插件";
return await Task.FromResult(responseData);
}
#endregion
inputModel.Data = inputModel.Data ?? "";
PluginSettingsModelFactory.Save(pluginSettingsJsonStr: inputModel.Data, pluginId: inputModel.PluginId);
responseData.Code = 1;
responseData.Message = "设置成功";
responseData.Data = inputModel.Data;
}
catch (Exception ex)
{
responseData.Code = -1;
responseData.Message = "设置失败: " + ex.Message;
}
return await Task.FromResult(responseData);
}
#endregion
#endregion Actions
#region Helpers
[NonAction]
private IList<PluginInfoResponseModel> PluginInfoModelToResponseModel(IList<PluginInfoModel> pluginInfoModels, PluginConfigModel pluginConfigModel, string[] enablePluginIds)
{
// 获取 Plugins 下所有插件
// DirectoryInfo pluginsDir = new DirectoryInfo(PluginPathProvider.PluginsRootPath());
// List<string> pluginIds = pluginsDir?.GetDirectories()?.Select(m => m.Name)?.ToList() ?? new List<string>();
IList<PluginInfoResponseModel> responseModels = new List<PluginInfoResponseModel>();
#region
foreach (var model in pluginInfoModels)
{
PluginInfoResponseModel responseModel = new PluginInfoResponseModel();
responseModel.Author = model.Author;
responseModel.Description = model.Description;
responseModel.DisplayName = model.DisplayName;
responseModel.PluginId = model.PluginId;
responseModel.SupportedVersions = model.SupportedVersions;
responseModel.Version = model.Version;
responseModel.DependPlugins = model.DependPlugins;
if (pluginConfigModel.EnabledPlugins.Contains(model.PluginId) && !enablePluginIds.Contains(model.PluginId))
{
// 错误情况: 配置 标识 已启用, 但实际没有启用成功
pluginConfigModel.EnabledPlugins.Remove(model.PluginId);
PluginConfigModelFactory.Save(pluginConfigModel);
responseModel.Status = PluginStatus.Disabled;
}
else if (!pluginConfigModel.EnabledPlugins.Contains(model.PluginId) && enablePluginIds.Contains(model.PluginId))
{
// 错误情况: 配置没有标识 已启用, 但实际 已启用
pluginConfigModel.EnabledPlugins.Add(model.PluginId);
PluginConfigModelFactory.Save(pluginConfigModel);
responseModel.Status = PluginStatus.Enabled;
}
else if (pluginConfigModel.EnabledPlugins.Contains(model.PluginId) && enablePluginIds.Contains(model.PluginId))
{
responseModel.Status = PluginStatus.Enabled;
}
else
{
responseModel.Status = PluginStatus.Disabled;
}
responseModels.Add(responseModel);
}
#endregion
return responseModels;
}
public enum UploadFileType
{
NoAllowedType = 0,
Zip = 1,
Nupkg = 2
}
#endregion Helpers
}