网站首页 网站源码
website
站点相关全部源代码,隐藏了一些关于服务器的信息
#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();
    }
}
⚠⚠    以下内容为AI分析的结果,请根据实际情况进行判断。

上述代码是一个 ASP.NET Core Web API 控制器,名为 CodeController,主要用于管理和查看源代码。以下是代码的主要功能和结构的详细解释:

1. 控制器概述

  • 命名空间: Dpz.Core.WebApi.Controllers
  • 特性: 使用 [ApiController][Route("api/[controller]")] 特性,表明这是一个 API 控制器,并定义了路由前缀。

2. 依赖注入

控制器的构造函数接受多个依赖项:

  • IConfiguration: 用于读取应用程序配置。
  • ICodeNoteService: 用于处理代码注释的服务。
  • IHybridCachingProvider: 用于缓存的服务。
  • ILogger<CodeController>: 用于记录日志。

3. 源代码查看配置

  • 使用 Lazy<CodeViewOption> 来延迟加载源代码查看的配置,确保在需要时才读取配置。
  • _allSourceFiles 是一个静态的 Lazy<List<FileSystemInfo>>,用于存储所有源代码文件的信息。

4. 私有方法

  • GetCurrentStruct: 根据给定路径获取当前目录或文件的结构。
  • GetCodeNoteTreeAsync: 异步获取代码注释树,返回当前路径的文件和目录结构。
  • GetCodeContentAsync: 异步获取文件的内容和相关信息,支持代码预览。
  • GetDirectorySystemInfos: 获取目录下的文件和子目录的信息。
  • GetAllFilSystemInfos: 递归获取指定路径下的所有文件和目录信息。

5. API 方法

  • GetCodeNotes:

    • HTTP GET 请求,获取指定路径的代码注释树。
    • 支持缓存,使用 IHybridCachingProvider 来缓存结果。
    • 如果启用了 AI 分析,并且文件是 C# 语言且没有分析结果,则会异步触发代码分析任务。
  • Search:

    • HTTP GET 请求,支持根据关键字搜索文件和目录。
    • 返回匹配的文件和目录列表,并支持缓存。
  • SaveNote:

    • HTTP POST 请求,用于保存源代码的说明。
    • 验证路径是否存在,并确保路径在允许的目录内。

6. 错误处理

  • 在配置加载和路径验证中,使用 BusinessException 抛出错误,确保在配置错误或路径不合法时能够及时反馈。

7. 其他功能

  • 代码中使用了 LINQ 查询来处理文件和目录的过滤、排序等操作。
  • 使用 BackgroundJob.Enqueue 来异步处理代码分析任务,利用 Hangfire 进行后台作业调度。

总结

CodeController 提供了一系列 API 接口,用于管理和查看源代码,包括获取代码结构、搜索文件、保存注释等功能。它通过依赖注入和懒加载的方式提高了性能和可维护性,同时也考虑了错误处理和缓存机制。

loading