网站首页 网站源码
#nullable enable
using Dpz.Core.Hangfire;
using Dpz.Core.WebApi.Hangfire;
using Hangfire;
namespace Dpz.Core.WebApi.Controllers;
/// <summary>
/// 源码管理
/// </summary>
[ApiController, Route("api/[controller]")]
public class CodeController : ControllerBase
{
private readonly IConfiguration _configuration;
private readonly ICodeNoteService _codeNoteService;
private readonly IHybridCachingProvider _cachingProvider;
/// <summary>
/// 代码查看配置
/// </summary>
private readonly Lazy<CodeViewOption> _codeViewCfg;
/// <summary>
/// 过滤后的所有文件
/// </summary>
private static Lazy<List<FileSystemInfo>>? _allSourceFiles;
/// <summary>
/// ctor
/// </summary>
public CodeController(
IConfiguration configuration,
ICodeNoteService codeNoteService,
IHybridCachingProvider cachingProvider,
ILogger<CodeController> logger
)
{
_configuration = configuration;
_codeNoteService = codeNoteService;
_cachingProvider = cachingProvider;
if (_codeViewCfg is not { IsValueCreated: true })
{
_codeViewCfg = new Lazy<CodeViewOption>(() =>
{
var codeView = configuration.GetSection("CodeView").Get<CodeViewOption>();
if (
codeView == null
|| string.IsNullOrEmpty(codeView.SourceCodeRoot)
|| !Directory.Exists(codeView.SourceCodeRoot)
)
{
logger.LogError("code view cfg:{Cfg}", JsonSerializer.Serialize(codeView));
throw new BusinessException(
"appsettings.json configuration error, source code directory not exists. "
);
}
if (_allSourceFiles is not { IsValueCreated: true })
{
_allSourceFiles = new Lazy<List<FileSystemInfo>>(() =>
{
return GetAllFilSystemInfos(codeView.SourceCodeRoot)
.OrderBy(x => x.FullName)
.ToList();
});
}
return codeView;
});
}
}
#region private method
[NonAction]
private FileSystemInfo[] GetCurrentStruct(string?[]? paths, string currentPath)
{
var result = IsRoot(paths)
? _allSourceFiles
?.Value
//.AsParallel()
.Where(x =>
{
if (x is FileInfo file)
{
return file.Directory?.FullName
== new DirectoryInfo(_codeViewCfg.Value.SourceCodeRoot).FullName;
}
if (x is DirectoryInfo directoryInfo)
{
return directoryInfo.Parent?.FullName
== new DirectoryInfo(_codeViewCfg.Value.SourceCodeRoot).FullName;
}
return false;
})
.ToArray()
: _allSourceFiles
?.Value
//.AsParallel()
.Where(x =>
{
if (x is DirectoryInfo directoryInfo)
{
return directoryInfo.Parent?.FullName.Equals(
currentPath,
StringComparison.OrdinalIgnoreCase
) == true;
}
if (x is FileInfo fileInfo)
{
return fileInfo.Directory?.FullName.Equals(
currentPath,
StringComparison.OrdinalIgnoreCase
) == true;
}
return false;
})
// .Where(x => x.FullName.StartsWith(currentPath, StringComparison.CurrentCultureIgnoreCase))
// .Where(x => !x.FullName.Equals(currentPath, StringComparison.CurrentCultureIgnoreCase))
.ToArray();
return result ?? [];
}
[NonAction]
private async Task<CodeNoteTree> GetCodeNoteTreeAsync(string?[]? paths)
{
paths = paths?.Where(x => !string.IsNullOrEmpty(x)).ToArray();
//get current path
var currentPath = IsRoot(paths)
? _codeViewCfg.Value.SourceCodeRoot
: Path.Combine(_codeViewCfg.Value.SourceCodeRoot, Path.Combine(paths!));
if (
!IsRoot(paths)
&& _allSourceFiles?.Value.Any(x =>
x.FullName.Equals(currentPath, StringComparison.OrdinalIgnoreCase)
) == false
)
{
return new CodeNoteTree
{
IsRoot = false,
IsDirectory = false,
Directories = [],
Files = [],
ParentPaths = [],
CurrentPaths = paths?.ToList() ?? [],
FileName = null,
Type = FileSystemType.NoExists,
};
}
// get current directory all parent path.
var parentPaths = IsRoot(paths) ? null : paths!.ToList();
if (parentPaths is { Count: > 0 })
{
parentPaths.RemoveAt(parentPaths.Count - 1);
}
var currentStruct = GetCurrentStruct(paths, currentPath);
if (currentStruct.Length == 0 && Directory.Exists(currentPath))
{
return new CodeNoteTree
{
IsRoot = IsRoot(paths),
IsDirectory = true,
Directories = new List<ChildrenTree>(),
Files = new List<ChildrenTree>(),
ParentPaths = parentPaths,
CurrentPaths = paths?.ToList() ?? [],
Type = FileSystemType.FileSystem,
ReadmeContent = null,
};
}
// get current directory all file system info.
if (currentStruct.Any())
{
var notes = await _codeNoteService.GetNoteAsync(paths!);
//get current path all directory
var directories = GetDirectorySystemInfos(
currentStruct,
_codeViewCfg.Value,
notes,
x => x.Attributes.HasFlag(FileAttributes.Directory)
);
var files = GetDirectorySystemInfos(
currentStruct,
_codeViewCfg.Value,
notes,
x => !x.Attributes.HasFlag(FileAttributes.Directory)
);
var readmeContent = "";
var readme = currentStruct.FirstOrDefault(x => x.Name.ToLower() == "readme.md");
if (readme?.Exists == true)
{
using var reader = new StreamReader(readme.FullName);
readmeContent = await reader.ReadToEndAsync();
}
return new CodeNoteTree
{
IsRoot = IsRoot(paths),
IsDirectory = true,
Directories = directories.ToList(),
Files = files.ToList(),
ParentPaths = parentPaths,
CurrentPaths = paths?.ToList() ?? [],
Type = FileSystemType.FileSystem,
ReadmeContent = readmeContent,
};
}
// get current file info.
var fileInfo = (FileInfo?)
_allSourceFiles?.Value.FirstOrDefault(x =>
x.FullName.Equals(currentPath, StringComparison.OrdinalIgnoreCase)
);
//var fileInfo = new FileInfo(fullPath);
var tree = new CodeNoteTree
{
IsRoot = false,
IsDirectory = false,
Directories = new(),
Files = new(),
ParentPaths = parentPaths,
CurrentPaths = paths?.ToList() ?? new(),
FileName = fileInfo?.Name,
};
VmCodeNote? codeNote = null;
if (fileInfo != null)
{
var filePath = (parentPaths ?? []).Select(x => x!).ToArray();
codeNote = await _codeNoteService.FindAsync(filePath, fileInfo.Name);
}
var codeContainer = await GetCodeContentAsync(fileInfo, codeNote);
tree.Type =
codeContainer?.IsPreview == true ? FileSystemType.FileSystem : FileSystemType.NoSupport;
tree.CodeContainer = codeContainer;
return tree;
}
[NonAction]
private async Task<CodeContainer?> GetCodeContentAsync(FileInfo? fileInfo, VmCodeNote? note)
{
if (fileInfo is not { Exists: true })
return null;
if (_codeViewCfg.Value.Filters?.Any(x => x.WildCardMatch(fileInfo.Name)) == true)
{
return null;
}
var model = new CodeContainer
{
Language = "",
CodeContent = "该文件不支持预览",
IsPreview = false,
};
var extension = fileInfo.Extension;
extension = (
string.IsNullOrEmpty(extension) && fileInfo.Attributes.HasFlag(FileAttributes.Archive)
? fileInfo.Name
: extension
).ToLower();
if (
_codeViewCfg.Value.ExtensionToLanguage?.TryGetValue(extension, out var language) == true
)
{
model.IsPreview = true;
model.Language = language;
using var fileReader = fileInfo.OpenText();
var code = await fileReader.ReadToEndAsync();
model.CodeContent = code;
model.AiAnalyzeResult = note?.AiAnalyzeResult;
}
return model;
}
[NonAction]
private static bool IsRoot(IReadOnlyCollection<string?>? paths)
{
return paths is null || paths.Count == 0;
}
[NonAction]
private IEnumerable<ChildrenTree> GetDirectorySystemInfos(
FileSystemInfo[] fileSystemInfos,
CodeViewOption codeView,
List<VmCodeNote> notes,
Func<FileSystemInfo, bool> func
)
{
return from x in fileSystemInfos
where (_codeViewCfg.Value.Filters ?? []).All(y => !y.WildCardMatch(x.Name))
orderby x.Name
where func(x)
let allDir = x
.FullName.ReplaceOne(codeView.SourceCodeRoot, "")
.Split(Path.DirectorySeparatorChar)
.Where(s => !string.IsNullOrEmpty(s))
.ToList()
let note = notes.FirstOrDefault(y => y.Name == x.Name)
select new ChildrenTree
{
Name = x.Name,
LastUpdateTime = x.LastWriteTime,
CurrentPath = allDir.ToList(),
Note = string.IsNullOrEmpty(note?.Note) ? "" : note.Note,
};
}
private List<FileSystemInfo> GetAllFilSystemInfos(string? path)
{
if (string.IsNullOrEmpty(path))
{
return [];
}
var dir = new DirectoryInfo(path);
if (!dir.Exists)
return new List<FileSystemInfo>();
var files = new List<FileSystemInfo>();
var fileSystemInfos = dir.GetFileSystemInfos();
foreach (var item in fileSystemInfos)
{
if (_codeViewCfg.Value.Filters?.Any(x => x.WildCardMatch(item.Name)) == true)
continue;
if (item.Attributes == FileAttributes.Directory)
{
files.Add(item);
files.AddRange(GetAllFilSystemInfos(item.FullName));
}
else
{
files.Add(item);
}
}
return files;
}
#endregion
/// <summary>
/// 获取源码树节点
/// </summary>
/// <returns></returns>
[HttpGet]
[ProducesResponseType<CodeNoteTree>(StatusCodes.Status200OK)]
public async Task<IActionResult> GetCodeNotes([FromQuery] params string[] path)
{
var key = CacheKey.CodeViewKey(path, _codeViewCfg.Value.ToString());
var cache = await _cachingProvider.GetAsync<CodeNoteTree>(key);
if (!cache.IsNull && cache.HasValue)
{
return Ok(cache.Value);
}
var tree = await GetCodeNoteTreeAsync(path);
var useAiAnalyze = _configuration.GetValue("CodeUseAIAnalyze", false);
if (
useAiAnalyze
&& tree.CodeContainer is { Language: "csharp" }
&& string.IsNullOrWhiteSpace(tree.CodeContainer.AiAnalyzeResult)
)
{
var parentPath = (tree.ParentPaths ?? []).ToArray();
BackgroundJob.Enqueue<AnalyzeCodeActivator>(x =>
x.AnalyzeAsync(tree.CodeContainer.CodeContent, parentPath, tree.FileName)
);
}
await _cachingProvider.SetAsync(key, tree, TimeSpan.FromDays(30));
return Ok(tree);
}
/// <summary>
/// 搜索
/// </summary>
/// <param name="keyword"></param>
/// <returns></returns>
[HttpGet("search")]
[ProducesResponseType<CodeNoteTree>(StatusCodes.Status200OK)]
public async Task<IActionResult> Search([FromQuery] [Required] string keyword)
{
var model = new CodeNoteTree
{
IsRoot = true,
IsDirectory = true,
Directories = [],
Files = [],
ParentPaths = [],
CurrentPaths = [],
Type = FileSystemType.FileSystem,
ReadmeContent = null,
};
if (string.IsNullOrWhiteSpace(keyword))
{
return Ok(model);
}
var key = CacheKey.CodeViewSearchKey(keyword);
var cache = await _cachingProvider.GetAsync<CodeNoteTree>(key);
if (!cache.IsNull && cache.HasValue)
{
return Ok(cache.Value);
}
var matchList = _allSourceFiles
?.Value.Where(x => x.Name.Contains(keyword, StringComparison.CurrentCultureIgnoreCase))
.OrderBy(x => x.FullName)
.ToList();
Func<FileSystemInfo, ChildrenTree> func = x => new ChildrenTree
{
Name = x.Name,
LastUpdateTime = x.LastWriteTime,
CurrentPath = x
.FullName.ReplaceOne(_codeViewCfg.Value.SourceCodeRoot, "")
.Split(Path.DirectorySeparatorChar)
.Where(s => !string.IsNullOrEmpty(s))
.ToList(),
Note = "",
};
model.Directories = matchList
?.Where(x => x.Attributes == FileAttributes.Directory)
.Select(func)
.ToList();
model.Files = matchList
?.Where(x => x.Attributes != FileAttributes.Directory)
.Select(func)
.ToList();
await _cachingProvider.SetAsync(key, model, TimeSpan.FromDays(1));
return Ok(model);
}
/// <summary>
/// 保存源码说明
/// </summary>
/// <returns></returns>
[HttpPost, Authorize(Policy = "System")]
[ProducesResponseType<CodeNoteTree>(StatusCodes.Status204NoContent)]
[ProducesResponseType<CodeNoteTree>(StatusCodes.Status401Unauthorized)]
public async Task<IActionResult> SaveNote([FromBody] CodeSaveDto model)
{
var absolutePath = Path.Combine(
_codeViewCfg.Value.SourceCodeRoot,
Path.Combine(model.Path),
model.Name
);
if (!Directory.Exists(absolutePath) && !System.IO.File.Exists(absolutePath))
{
return BadRequest("path not exists");
}
// 验证路径是否在允许的目录内
if (
!absolutePath.StartsWith(
_codeViewCfg.Value.SourceCodeRoot,
StringComparison.OrdinalIgnoreCase
)
)
{
return BadRequest("invalid path");
}
await _codeNoteService.SaveNoteAsync(model.Path, model.Name, model.Note);
return NoContent();
}
}
上述代码是一个 ASP.NET Core Web API 控制器,名为 CodeController
,主要用于管理和查看源代码。以下是代码的主要功能和结构的详细解释:
Dpz.Core.WebApi.Controllers
[ApiController]
和 [Route("api/[controller]")]
特性,表明这是一个 API 控制器,并定义了路由前缀。控制器的构造函数接受多个依赖项:
IConfiguration
: 用于读取应用程序配置。ICodeNoteService
: 用于处理代码注释的服务。IHybridCachingProvider
: 用于缓存的服务。ILogger<CodeController>
: 用于记录日志。Lazy<CodeViewOption>
来延迟加载源代码查看的配置,确保在需要时才读取配置。_allSourceFiles
是一个静态的 Lazy<List<FileSystemInfo>>
,用于存储所有源代码文件的信息。GetCodeNotes:
IHybridCachingProvider
来缓存结果。Search:
SaveNote:
BusinessException
抛出错误,确保在配置错误或路径不合法时能够及时反馈。BackgroundJob.Enqueue
来异步处理代码分析任务,利用 Hangfire 进行后台作业调度。CodeController
提供了一系列 API 接口,用于管理和查看源代码,包括获取代码结构、搜索文件、保存注释等功能。它通过依赖注入和懒加载的方式提高了性能和可维护性,同时也考虑了错误处理和缓存机制。