docs: 补全 C# XML 文档注释,覆盖所有公开与内部成员

为 14 个项目中缺少 XML 注释的类、接口、方法、属性、字段、record、
枚举等成员补全中文文档注释。接口方法在接口层定义完整注释,实现类
使用 <inheritdoc /> 引用。私有辅助方法结合业务语义编写注释。

扫描结果:missing-csharp-docs.txt 缺失项归零。
构建结果:0 警告,0 错误。

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
luoqian 2026-05-22 14:45:07 +08:00
parent 9f8da2c063
commit d93098638d
17 changed files with 397 additions and 3 deletions

View File

@ -2,11 +2,21 @@ using FileShare_Services.Services.FileLibrary;
namespace FileShare_API.Services
{
/// <summary>
/// 文件库定时扫描后台服务,每分钟执行一次扫描,检查是否有需要扫描的文件库目录。
/// </summary>
public sealed class FileLibraryScanHostedService(IServiceScopeFactory scopeFactory, ILogger<FileLibraryScanHostedService> logger)
: BackgroundService
{
/// <summary>
/// 扫描间隔,固定为 1 分钟。
/// </summary>
private static readonly TimeSpan Interval = TimeSpan.FromMinutes(1);
/// <summary>
/// 启动扫描循环,首次立即执行,之后按 <see cref="Interval"/> 周期重复执行。
/// </summary>
/// <param name="stoppingToken">应用关闭时触发的取消令牌。</param>
protected override async Task ExecuteAsync(CancellationToken stoppingToken)
{
await ScanAsync(stoppingToken);
@ -18,6 +28,10 @@ namespace FileShare_API.Services
}
}
/// <summary>
/// 创建临时作用域并调用 <see cref="IFileLibraryService.ScanDueRootsAsync"/> 执行一次到期扫描。
/// </summary>
/// <param name="cancellationToken">取消令牌。</param>
private async Task ScanAsync(CancellationToken cancellationToken)
{
try

View File

@ -58,8 +58,18 @@ namespace FileShare_EFCore.Database
=> new(DesignTimeDatabaseConfiguration.Create(args, DatabaseProvider.MySQL));
}
/// <summary>
/// 为设计时工具dotnet ef migrations提供数据库连接配置。
/// </summary>
internal static class DesignTimeDatabaseConfiguration
{
/// <summary>
/// 解析命令行参数中的 --provider 选项,创建对应的 <see cref="DatabaseConfiguration"/>。
/// 未指定提供程序时默认使用 SQLite。
/// </summary>
/// <param name="args">命令行参数。</param>
/// <param name="defaultProvider">未指定时的默认数据库提供程序。</param>
/// <returns>对应数据库提供程序的配置。</returns>
public static DatabaseConfiguration Create(string[] args, DatabaseProvider defaultProvider = DatabaseProvider.SQLite)
{
DatabaseProviderRegistry.RegisterDefaults();
@ -75,6 +85,12 @@ namespace FileShare_EFCore.Database
};
}
/// <summary>
/// 从命令行参数中解析 --provider 选项的值。
/// 支持 <c>--provider sqlite</c> 和 <c>--provider=sqlite</c> 两种格式。
/// </summary>
/// <param name="args">命令行参数。</param>
/// <returns>解析到的数据库提供程序,未指定或无法识别时返回 null。</returns>
private static DatabaseProvider? GetProvider(string[] args)
{
for (var i = 0; i < args.Length; i++)

View File

@ -40,6 +40,12 @@ namespace FileShare_EFCore.Database
return services;
}
/// <summary>
/// 根据 <see cref="DatabaseConfiguration.Provider"/> 注册对应具体类型的 <see cref="AppDataContext"/> 实现。
/// </summary>
/// <param name="services">服务集合。</param>
/// <param name="config">数据库配置。</param>
/// <exception cref="NotSupportedException">数据库提供程序未注册时抛出。</exception>
private static void AddProviderAppDataContext(this IServiceCollection services, DatabaseConfiguration config)
{
switch (config.Provider)

View File

@ -684,6 +684,10 @@ namespace FileShare_PC.Views
return true;
}
/// <summary>
/// 为媒体流响应添加 CORS 和 Range 请求头,允许浏览器跨域访问和分段请求。
/// </summary>
/// <param name="response">HTTP 响应对象。</param>
private static void AddLocalMediaHeaders(HttpListenerResponse response)
{
response.Headers["Access-Control-Allow-Origin"] = "*";
@ -692,6 +696,14 @@ namespace FileShare_PC.Views
response.Headers["Access-Control-Expose-Headers"] = "Accept-Ranges, Content-Length, Content-Range";
}
/// <summary>
/// 解析 HTTP Range 请求头中的 <c>bytes=start-end</c> 格式,支持两端省略和后缀长度(如 <c>bytes=-500</c>)。
/// </summary>
/// <param name="value">Range 请求头值。</param>
/// <param name="length">资源总字节数。</param>
/// <param name="start">解析出的起始字节偏移。</param>
/// <param name="end">解析出的结束字节偏移。</param>
/// <returns>解析成功返回 true否则 false。</returns>
private static bool TryParseByteRange(string? value, long length, out long start, out long end)
{
start = 0;
@ -737,6 +749,12 @@ namespace FileShare_PC.Views
return true;
}
/// <summary>
/// 从输入流读取指定字节数并写入输出流,用于实现 HTTP Range 分段响应。
/// </summary>
/// <param name="input">源文件流。</param>
/// <param name="output">HTTP 响应输出流。</param>
/// <param name="bytesRemaining">需要传输的剩余字节数。</param>
private static async Task CopyRangeAsync(Stream input, Stream output, long bytesRemaining)
{
var buffer = new byte[64 * 1024];

View File

@ -47,11 +47,22 @@ namespace FileShare_Services.Core
return true;
}
/// <summary>
/// 将路径按 <c>/</c> 拆分为非空片段数组。
/// </summary>
/// <param name="value">路径字符串。</param>
/// <returns>过滤空条目后的片段数组。</returns>
private static string[] SplitSegments(string value)
{
return value.Split('/', StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries);
}
/// <summary>
/// 尝试从 <c>{name}</c> 或 <c>{name:constraint}</c> 格式的片段中提取参数名称。
/// </summary>
/// <param name="segment">模式片段。</param>
/// <param name="parameterName">提取出的参数名称。</param>
/// <returns>该片段是路由参数时返回 true否则 false。</returns>
private static bool TryGetParameterName(string segment, out string parameterName)
{
parameterName = string.Empty;
@ -66,6 +77,12 @@ namespace FileShare_Services.Core
return !string.IsNullOrWhiteSpace(parameterName);
}
/// <summary>
/// 检查路由值是否满足片段中的类型约束(目前仅支持 <c>:int</c>)。
/// </summary>
/// <param name="segment">包含约束的模式片段。</param>
/// <param name="value">实际路径值。</param>
/// <returns>满足约束时返回 true。</returns>
private static bool MatchesConstraint(string segment, string value)
{
return !segment.EndsWith(":int}", StringComparison.OrdinalIgnoreCase)

View File

@ -37,6 +37,14 @@ namespace FileShare_Services.Core
return Deserialize<T>(json, "query");
}
/// <summary>
/// 将 JSON 反序列化为目标类型,反序列化失败或结果为 null 时抛出 <see cref="ArgumentException"/>。
/// </summary>
/// <typeparam name="T">目标类型。</typeparam>
/// <param name="json">JSON 字符串。</param>
/// <param name="source">来源标识body 或 query用于异常消息。</param>
/// <returns>反序列化后的实例。</returns>
/// <exception cref="ArgumentException">JSON 无法绑定到目标类型时抛出。</exception>
private static T Deserialize<T>(string json, string source)
{
try

View File

@ -98,6 +98,12 @@ namespace FileShare_Services.Endpoints
#region
/// <summary>
/// 从 <see cref="ServiceEndpointContext.Items"/> 中解析 <see cref="IFileStreamService"/>
/// 读取查询参数中的文件 ID返回文件流响应。
/// </summary>
/// <param name="ctx">端点上下文。</param>
/// <returns>文件流响应对象,服务不可用或 ID 无效时返回 null。</returns>
private static async Task<object?> GetFileStreamAsync(ServiceEndpointContext ctx)
{
var sp = ctx.Items["ServiceProvider"] as IServiceProvider;

View File

@ -2,27 +2,53 @@ using System.Text.Json.Serialization;
namespace FileShare_Services.Services.FileLibrary
{
/// <summary>
/// 添加文件库根目录的请求。
/// </summary>
public sealed record AddLibraryRootRequest(
[property: JsonPropertyName("path")] string? Path,
[property: JsonPropertyName("displayName")] string? DisplayName = null,
[property: JsonPropertyName("scanIntervalMinutes")] int? ScanIntervalMinutes = null);
[property: JsonPropertyName("path")]
string? Path,
[property: JsonPropertyName("displayName")]
string? DisplayName = null,
[property: JsonPropertyName("scanIntervalMinutes")]
int? ScanIntervalMinutes = null);
/// <summary>
/// 更新文件库根目录启用状态的请求。
/// </summary>
public sealed record UpdateLibraryRootRequest(
[property: JsonPropertyName("id")] int Id,
[property: JsonPropertyName("isEnabled")] bool IsEnabled);
/// <summary>
/// 触发文件库根目录立即扫描的请求。
/// </summary>
public sealed record ScanLibraryRootRequest(
[property: JsonPropertyName("id")] int Id);
/// <summary>
/// 删除文件库根目录的请求。
/// </summary>
public sealed record DeleteLibraryRootRequest(
[property: JsonPropertyName("id")] int Id);
/// <summary>
/// 查询服务器子目录的请求。
/// </summary>
public sealed record DirectoryQueryRequest(
[property: JsonPropertyName("path")] string? Path);
/// <summary>
/// 根据 ID 查询文件的请求。
/// </summary>
public sealed record FileQueryRequest(
[property: JsonPropertyName("id")] int Id);
/// <summary>
/// 分页搜索已扫描文件的请求。
/// </summary>
public sealed record SearchFilesRequest(
[property: JsonPropertyName("page")] int Page = 1,
[property: JsonPropertyName("pageSize")] int PageSize = 24,
@ -30,6 +56,9 @@ namespace FileShare_Services.Services.FileLibrary
[property: JsonPropertyName("keyword")] string? Keyword = null,
[property: JsonPropertyName("rootId")] int RootId = 0);
/// <summary>
/// 磁盘驱动器信息。
/// </summary>
public sealed record DriveDto(
string Name,
string DisplayName,
@ -39,10 +68,16 @@ namespace FileShare_Services.Services.FileLibrary
long? AvailableFreeSpace,
bool IsReady);
/// <summary>
/// 服务器子目录信息。
/// </summary>
public sealed record DirectoryDto(
string Name,
string FullPath);
/// <summary>
/// 文件库根目录信息,包含扫描状态与文件数量。
/// </summary>
public sealed record LibraryRootDto(
int Id,
string Path,
@ -55,6 +90,9 @@ namespace FileShare_Services.Services.FileLibrary
string? LastScanError,
int FileCount);
/// <summary>
/// 已扫描文件的记录信息,包含媒体类型与流式访问 URL。
/// </summary>
public sealed record FileRecordDto(
int Id,
int LibraryRootId,
@ -69,15 +107,24 @@ namespace FileShare_Services.Services.FileLibrary
string? TextUrl,
bool BrowserPlayable);
/// <summary>
/// 浏览文件库目录结构的请求。
/// </summary>
public sealed record BrowseDirectoryRequest(
[property: JsonPropertyName("rootId")] int RootId = 0,
[property: JsonPropertyName("path")] string? Path = null);
/// <summary>
/// 浏览文件库目录的响应,包含当前路径、子目录列表和文件列表。
/// </summary>
public sealed record BrowseDirectoryResponse(
string CurrentPath,
List<string> Subdirectories,
List<FileRecordDto> Files);
/// <summary>
/// 文本文件预览内容,支持截断标记。
/// </summary>
public sealed record TextPreviewDto(
int Id,
string FileName,

View File

@ -3,49 +3,61 @@ using FileShare_Services.Core;
namespace FileShare_Services.Services.FileLibrary
{
/// <summary>
/// 文件库 HTTP 端点服务,将请求适配到 <see cref="IFileLibraryService"/> 并包装为 <see cref="IApiResponse"/>。
/// </summary>
public sealed class FileLibraryEndpointService(IFileLibraryService fileLibrary) : IFileLibraryEndpointService
{
/// <inheritdoc />
public async Task<IApiResponse> GetDrivesAsync(ServiceEndpointContext ctx)
{
return ResponseHelper.Ok(await fileLibrary.GetDrivesAsync());
}
/// <inheritdoc />
public async Task<IApiResponse> GetDirectoriesAsync(DirectoryQueryRequest request)
{
return ResponseHelper.Ok(await fileLibrary.GetDirectoriesAsync(request.Path));
}
/// <inheritdoc />
public async Task<IApiResponse> GetRootsAsync(ServiceEndpointContext ctx)
{
return ResponseHelper.Ok(await fileLibrary.GetRootsAsync());
}
/// <inheritdoc />
public async Task<IApiResponse> AddRootAsync(AddLibraryRootRequest request)
{
return ResponseHelper.Ok(await fileLibrary.AddRootAsync(request), "文件库目录已添加并完成扫描。");
}
/// <inheritdoc />
public async Task<IApiResponse> SetRootEnabledAsync(UpdateLibraryRootRequest request)
{
return ResponseHelper.Ok(await fileLibrary.SetRootEnabledAsync(request), "文件库目录状态已更新。");
}
/// <inheritdoc />
public async Task<IApiResponse> DeleteRootAsync(DeleteLibraryRootRequest request)
{
await fileLibrary.DeleteRootAsync(request);
return ResponseHelper.Succeed("文件库目录已删除。");
}
/// <inheritdoc />
public async Task<IApiResponse> ScanRootAsync(ScanLibraryRootRequest request)
{
return ResponseHelper.Ok(await fileLibrary.ScanRootAsync(request.Id), "文件库目录扫描完成。");
}
/// <inheritdoc />
public async Task<IApiResponse> SearchFilesAsync(SearchFilesRequest request)
{
return await fileLibrary.SearchFilesAsync(request);
}
/// <inheritdoc />
public async Task<IApiResponse> GetFileAsync(FileQueryRequest request)
{
ValidateFileId(request.Id);
@ -55,6 +67,7 @@ namespace FileShare_Services.Services.FileLibrary
: ResponseHelper.Ok(file);
}
/// <inheritdoc />
public async Task<IApiResponse> GetTextPreviewAsync(FileQueryRequest request)
{
ValidateFileId(request.Id);
@ -64,6 +77,7 @@ namespace FileShare_Services.Services.FileLibrary
: ResponseHelper.Ok(preview);
}
/// <inheritdoc />
public async Task<IApiResponse> BrowseDirectoryAsync(BrowseDirectoryRequest request)
{
if (request.RootId <= 0)
@ -73,6 +87,11 @@ namespace FileShare_Services.Services.FileLibrary
return ResponseHelper.Ok(result);
}
/// <summary>
/// 验证文件 ID 是否有效,无效时抛出 <see cref="ArgumentException"/>。
/// </summary>
/// <param name="id">文件记录 ID。</param>
/// <exception cref="ArgumentException">ID 小于等于 0 时抛出。</exception>
private static void ValidateFileId(int id)
{
if (id > 0)

View File

@ -7,11 +7,21 @@ using System.Text;
namespace FileShare_Services.Services.FileLibrary
{
/// <summary>
/// 文件库核心业务服务,实现磁盘枚举、目录管理、文件扫描与检索。
/// </summary>
public sealed class FileLibraryService(AppDataContext db) : IFileLibraryService
{
/// <summary>
/// 默认扫描间隔(分钟),当请求未指定间隔时使用。
/// </summary>
private const int DefaultScanIntervalMinutes = 5;
/// <summary>
/// 文本预览最大读取字节数1 MB
/// </summary>
private const int MaxTextPreviewBytes = 1024 * 1024;
/// <inheritdoc />
public Task<List<DriveDto>> GetDrivesAsync(CancellationToken cancellationToken = default)
{
var drives = DriveInfo.GetDrives()
@ -29,6 +39,7 @@ namespace FileShare_Services.Services.FileLibrary
return Task.FromResult(drives);
}
/// <inheritdoc />
public Task<List<DirectoryDto>> GetDirectoriesAsync(string? path, CancellationToken cancellationToken = default)
{
var normalized = NormalizeExistingDirectory(path);
@ -41,6 +52,7 @@ namespace FileShare_Services.Services.FileLibrary
return Task.FromResult(directories);
}
/// <inheritdoc />
public async Task<List<LibraryRootDto>> GetRootsAsync(CancellationToken cancellationToken = default)
{
var counts = await db.ManagedFileRecords
@ -56,6 +68,7 @@ namespace FileShare_Services.Services.FileLibrary
return roots.Select(root => ToRootDto(root, counts.GetValueOrDefault(root.Id))).ToList();
}
/// <inheritdoc />
public async Task<LibraryRootDto> AddRootAsync(AddLibraryRootRequest request, CancellationToken cancellationToken = default)
{
var normalized = NormalizeExistingDirectory(request.Path);
@ -85,6 +98,7 @@ namespace FileShare_Services.Services.FileLibrary
return await ScanRootAsync(root.Id, cancellationToken);
}
/// <inheritdoc />
public async Task<LibraryRootDto> SetRootEnabledAsync(UpdateLibraryRootRequest request, CancellationToken cancellationToken = default)
{
var root = await db.ManagedLibraryRoots.FirstOrDefaultAsync(item => item.Id == request.Id, cancellationToken)
@ -97,6 +111,7 @@ namespace FileShare_Services.Services.FileLibrary
return ToRootDto(root, count);
}
/// <inheritdoc />
public async Task DeleteRootAsync(DeleteLibraryRootRequest request, CancellationToken cancellationToken = default)
{
var root = await db.ManagedLibraryRoots.FirstOrDefaultAsync(item => item.Id == request.Id, cancellationToken)
@ -106,6 +121,7 @@ namespace FileShare_Services.Services.FileLibrary
await db.SaveChangesAsync(cancellationToken);
}
/// <inheritdoc />
public async Task<LibraryRootDto> ScanRootAsync(int rootId, CancellationToken cancellationToken = default)
{
var root = await db.ManagedLibraryRoots.FirstOrDefaultAsync(item => item.Id == rootId, cancellationToken)
@ -186,6 +202,7 @@ namespace FileShare_Services.Services.FileLibrary
return ToRootDto(root, count);
}
/// <inheritdoc />
public async Task ScanDueRootsAsync(CancellationToken cancellationToken = default)
{
var now = DateTime.UtcNow;
@ -213,6 +230,7 @@ namespace FileShare_Services.Services.FileLibrary
}
}
/// <inheritdoc />
public async Task<PagedResponse<FileRecordDto>> SearchFilesAsync(SearchFilesRequest request, CancellationToken cancellationToken = default)
{
var page = Math.Clamp(request.Page, 1, 100000);
@ -252,6 +270,7 @@ namespace FileShare_Services.Services.FileLibrary
return PagedResponse<FileRecordDto>.From(items, total, page, pageSize);
}
/// <inheritdoc />
public async Task<FileRecordDto?> GetFileAsync(int id, CancellationToken cancellationToken = default)
{
return await db.ManagedFileRecords
@ -261,6 +280,7 @@ namespace FileShare_Services.Services.FileLibrary
.FirstOrDefaultAsync(cancellationToken);
}
/// <inheritdoc />
public async Task<BrowseDirectoryResponse> BrowseDirectoryAsync(BrowseDirectoryRequest request, CancellationToken cancellationToken = default)
{
var rootId = request.RootId;
@ -309,6 +329,7 @@ namespace FileShare_Services.Services.FileLibrary
currentFiles);
}
/// <inheritdoc />
public async Task<TextPreviewDto?> GetTextPreviewAsync(int id, CancellationToken cancellationToken = default)
{
var file = await db.ManagedFileRecords
@ -335,6 +356,12 @@ namespace FileShare_Services.Services.FileLibrary
return new TextPreviewDto(file.Id, file.FileName, content, stream.Length > MaxTextPreviewBytes);
}
/// <summary>
/// 深度优先遍历目录树,枚举所有被 <see cref="MediaFileTypes"/> 支持的媒体文件路径。
/// 遇到无权限的目录时跳过该分支继续遍历。
/// </summary>
/// <param name="rootPath">根目录路径。</param>
/// <returns>支持的文件完整路径枚举。</returns>
private static IEnumerable<string> EnumerateSupportedFiles(string rootPath)
{
var pending = new Stack<string>();
@ -371,6 +398,13 @@ namespace FileShare_Services.Services.FileLibrary
}
}
/// <summary>
/// 规范化目录路径并验证目录存在。
/// </summary>
/// <param name="path">原始路径。</param>
/// <returns>规范化后的完整路径。</returns>
/// <exception cref="InvalidOperationException">路径为空时抛出。</exception>
/// <exception cref="DirectoryNotFoundException">目录不存在时抛出。</exception>
private static string NormalizeExistingDirectory(string? path)
{
if (string.IsNullOrWhiteSpace(path))
@ -387,6 +421,12 @@ namespace FileShare_Services.Services.FileLibrary
return new DirectoryInfo(fullPath).FullName;
}
/// <summary>
/// 解析根目录的显示名称,优先使用用户指定名称,否则使用目录名。
/// </summary>
/// <param name="path">目录路径。</param>
/// <param name="displayName">用户指定的显示名称。</param>
/// <returns>最终显示名称。</returns>
private static string ResolveDisplayName(string path, string? displayName)
{
if (!string.IsNullOrWhiteSpace(displayName))
@ -398,11 +438,22 @@ namespace FileShare_Services.Services.FileLibrary
return string.IsNullOrWhiteSpace(directory.Name) ? directory.FullName : directory.Name;
}
/// <summary>
/// 规范化扫描间隔,限制在 1 到 1440 分钟范围内,未指定时使用默认值。
/// </summary>
/// <param name="interval">用户指定的间隔。</param>
/// <returns>规范化后的间隔分钟数。</returns>
private static int NormalizeInterval(int? interval)
{
return Math.Clamp(interval ?? DefaultScanIntervalMinutes, 1, 1440);
}
/// <summary>
/// 安全获取驱动器属性值,驱动器未就绪或访问异常时返回 null。
/// </summary>
/// <param name="drive">驱动器信息。</param>
/// <param name="selector">属性选择器。</param>
/// <returns>属性值,不可用时返回 null。</returns>
private static long? SafeDriveValue(DriveInfo drive, Func<DriveInfo, long> selector)
{
try
@ -415,6 +466,12 @@ namespace FileShare_Services.Services.FileLibrary
}
}
/// <summary>
/// 将 <see cref="ManagedLibraryRoot"/> 实体映射为 <see cref="LibraryRootDto"/>。
/// </summary>
/// <param name="root">数据库实体。</param>
/// <param name="fileCount">关联的文件记录数。</param>
/// <returns>DTO 对象。</returns>
private static LibraryRootDto ToRootDto(ManagedLibraryRoot root, int fileCount)
{
return new LibraryRootDto(
@ -430,6 +487,11 @@ namespace FileShare_Services.Services.FileLibrary
fileCount);
}
/// <summary>
/// 将 <see cref="ManagedFileRecord"/> 实体映射为 <see cref="FileRecordDto"/>,并生成流式访问和文本预览 URL。
/// </summary>
/// <param name="file">数据库实体。</param>
/// <returns>DTO 对象。</returns>
private static FileRecordDto ToFileDto(ManagedFileRecord file)
{
return new FileRecordDto(

View File

@ -5,13 +5,26 @@ using Microsoft.EntityFrameworkCore;
namespace FileShare_Services.Services.FileLibrary
{
/// <summary>
/// 文件流服务接口,根据文件记录 ID 返回物理文件流信息。
/// </summary>
public interface IFileStreamService
{
/// <summary>
/// 获取指定文件的流式传输响应包含文件路径、名称、Content-Type 和修改时间。
/// </summary>
/// <param name="id">文件记录 ID。</param>
/// <param name="cancellationToken">取消令牌。</param>
/// <returns>文件流响应,文件不存在时返回 null。</returns>
Task<FileStreamResponse?> GetFileStreamAsync(int id, CancellationToken cancellationToken = default);
}
/// <summary>
/// 文件流服务实现,从数据库查询文件记录并构建 <see cref="FileStreamResponse"/>。
/// </summary>
public sealed class FileStreamService(AppDataContext db) : IFileStreamService
{
/// <inheritdoc />
public async Task<FileStreamResponse?> GetFileStreamAsync(int id, CancellationToken cancellationToken = default)
{
var file = await db.ManagedFileRecords

View File

@ -3,28 +3,86 @@ using FileShare_Services.Core;
namespace FileShare_Services.Services.FileLibrary
{
/// <summary>
/// 文件库 HTTP 端点服务接口,将 HTTP 请求适配到 <see cref="IFileLibraryService"/> 业务层。
/// </summary>
public interface IFileLibraryEndpointService
{
/// <summary>
/// 获取服务器所有磁盘驱动器。
/// </summary>
/// <param name="ctx">端点上下文。</param>
/// <returns>API 响应。</returns>
Task<IApiResponse> GetDrivesAsync(ServiceEndpointContext ctx);
/// <summary>
/// 查询指定路径下的子目录。
/// </summary>
/// <param name="request">包含路径的查询请求。</param>
/// <returns>API 响应。</returns>
Task<IApiResponse> GetDirectoriesAsync(DirectoryQueryRequest request);
/// <summary>
/// 获取所有已注册的文件库根目录。
/// </summary>
/// <param name="ctx">端点上下文。</param>
/// <returns>API 响应。</returns>
Task<IApiResponse> GetRootsAsync(ServiceEndpointContext ctx);
/// <summary>
/// 添加新的文件库根目录。
/// </summary>
/// <param name="request">包含路径和配置的请求。</param>
/// <returns>API 响应。</returns>
Task<IApiResponse> AddRootAsync(AddLibraryRootRequest request);
/// <summary>
/// 启用或禁用指定的文件库根目录。
/// </summary>
/// <param name="request">包含根目录 ID 和目标状态的请求。</param>
/// <returns>API 响应。</returns>
Task<IApiResponse> SetRootEnabledAsync(UpdateLibraryRootRequest request);
/// <summary>
/// 删除指定的文件库根目录。
/// </summary>
/// <param name="request">包含根目录 ID 的请求。</param>
/// <returns>API 响应。</returns>
Task<IApiResponse> DeleteRootAsync(DeleteLibraryRootRequest request);
/// <summary>
/// 立即扫描指定文件库根目录。
/// </summary>
/// <param name="request">包含根目录 ID 的请求。</param>
/// <returns>API 响应。</returns>
Task<IApiResponse> ScanRootAsync(ScanLibraryRootRequest request);
/// <summary>
/// 分页搜索已扫描的文件记录。
/// </summary>
/// <param name="request">包含分页和过滤条件的请求。</param>
/// <returns>API 响应。</returns>
Task<IApiResponse> SearchFilesAsync(SearchFilesRequest request);
/// <summary>
/// 根据 ID 获取文件详情。
/// </summary>
/// <param name="request">包含文件 ID 的请求。</param>
/// <returns>API 响应。</returns>
Task<IApiResponse> GetFileAsync(FileQueryRequest request);
/// <summary>
/// 获取文本文件预览内容。
/// </summary>
/// <param name="request">包含文件 ID 的请求。</param>
/// <returns>API 响应。</returns>
Task<IApiResponse> GetTextPreviewAsync(FileQueryRequest request);
/// <summary>
/// 浏览文件库目录结构。
/// </summary>
/// <param name="request">包含根目录 ID 和路径的请求。</param>
/// <returns>API 响应。</returns>
Task<IApiResponse> BrowseDirectoryAsync(BrowseDirectoryRequest request);
}
}

View File

@ -2,30 +2,100 @@ using FileShare_Common.Core;
namespace FileShare_Services.Services.FileLibrary
{
/// <summary>
/// 文件库核心业务服务接口,提供磁盘枚举、目录管理、文件扫描与检索功能。
/// </summary>
public interface IFileLibraryService
{
/// <summary>
/// 获取服务器所有磁盘驱动器信息。
/// </summary>
/// <param name="cancellationToken">取消令牌。</param>
/// <returns>驱动器信息列表。</returns>
Task<List<DriveDto>> GetDrivesAsync(CancellationToken cancellationToken = default);
/// <summary>
/// 获取指定路径下的子目录列表。
/// </summary>
/// <param name="path">父目录路径,为空时返回根目录。</param>
/// <param name="cancellationToken">取消令牌。</param>
/// <returns>子目录信息列表。</returns>
Task<List<DirectoryDto>> GetDirectoriesAsync(string? path, CancellationToken cancellationToken = default);
/// <summary>
/// 获取所有已注册的文件库根目录及其文件数量。
/// </summary>
/// <param name="cancellationToken">取消令牌。</param>
/// <returns>文件库根目录列表。</returns>
Task<List<LibraryRootDto>> GetRootsAsync(CancellationToken cancellationToken = default);
/// <summary>
/// 添加新的文件库根目录,如已存在则激活并重新扫描。
/// </summary>
/// <param name="request">包含路径、显示名称和扫描间隔的请求。</param>
/// <param name="cancellationToken">取消令牌。</param>
/// <returns>新添加或已存在的根目录信息。</returns>
Task<LibraryRootDto> AddRootAsync(AddLibraryRootRequest request, CancellationToken cancellationToken = default);
/// <summary>
/// 启用或禁用指定的文件库根目录。
/// </summary>
/// <param name="request">包含根目录 ID 和目标启用状态的请求。</param>
/// <param name="cancellationToken">取消令牌。</param>
/// <returns>更新后的根目录信息。</returns>
Task<LibraryRootDto> SetRootEnabledAsync(UpdateLibraryRootRequest request, CancellationToken cancellationToken = default);
/// <summary>
/// 删除指定的文件库根目录。
/// </summary>
/// <param name="request">包含根目录 ID 的请求。</param>
/// <param name="cancellationToken">取消令牌。</param>
Task DeleteRootAsync(DeleteLibraryRootRequest request, CancellationToken cancellationToken = default);
/// <summary>
/// 立即扫描指定根目录,枚举所有支持的媒体文件并更新数据库。
/// </summary>
/// <param name="rootId">根目录 ID。</param>
/// <param name="cancellationToken">取消令牌。</param>
/// <returns>扫描后的根目录信息。</returns>
Task<LibraryRootDto> ScanRootAsync(int rootId, CancellationToken cancellationToken = default);
/// <summary>
/// 扫描所有已到期(距上次扫描超过间隔时间)的根目录。
/// </summary>
/// <param name="cancellationToken">取消令牌。</param>
Task ScanDueRootsAsync(CancellationToken cancellationToken = default);
/// <summary>
/// 分页搜索已扫描的文件记录,支持按媒体类型、关键词和根目录过滤。
/// </summary>
/// <param name="request">包含分页和过滤条件的请求。</param>
/// <param name="cancellationToken">取消令牌。</param>
/// <returns>分页文件记录响应。</returns>
Task<PagedResponse<FileRecordDto>> SearchFilesAsync(SearchFilesRequest request, CancellationToken cancellationToken = default);
/// <summary>
/// 根据 ID 获取单个文件的记录信息。
/// </summary>
/// <param name="id">文件记录 ID。</param>
/// <param name="cancellationToken">取消令牌。</param>
/// <returns>文件记录 DTO不存在时返回 null。</returns>
Task<FileRecordDto?> GetFileAsync(int id, CancellationToken cancellationToken = default);
/// <summary>
/// 获取文本文件的预览内容(最多 1 MB
/// </summary>
/// <param name="id">文件记录 ID。</param>
/// <param name="cancellationToken">取消令牌。</param>
/// <returns>文本预览 DTO文件不存在或非文本类型时返回 null。</returns>
Task<TextPreviewDto?> GetTextPreviewAsync(int id, CancellationToken cancellationToken = default);
/// <summary>
/// 浏览指定根目录下的目录结构和文件列表。
/// </summary>
/// <param name="request">包含根目录 ID 和路径的请求。</param>
/// <param name="cancellationToken">取消令牌。</param>
/// <returns>目录浏览响应,包含子目录和文件列表。</returns>
Task<BrowseDirectoryResponse> BrowseDirectoryAsync(BrowseDirectoryRequest request, CancellationToken cancellationToken = default);
}
}

View File

@ -1,5 +1,8 @@
namespace FileShare_Services.Services.FileLibrary
{
/// <summary>
/// 受支持的媒体文件类型注册表提供扩展名到媒体类型、Content-Type 和浏览器可播放性的查询。
/// </summary>
public static class MediaFileTypes
{
private static readonly Dictionary<string, (string MediaType, string ContentType, bool BrowserPlayable)> Types =
@ -27,6 +30,14 @@ namespace FileShare_Services.Services.FileLibrary
[".oga"] = ("audio", "audio/ogg", true),
};
/// <summary>
/// 根据文件扩展名查询媒体类型信息。
/// </summary>
/// <param name="extension">文件扩展名(含点号)。</param>
/// <param name="mediaType">输出媒体分类text、video、audio。</param>
/// <param name="contentType">输出HTTP Content-Type。</param>
/// <param name="browserPlayable">输出:浏览器是否可直接播放。</param>
/// <returns>扩展名已注册时返回 true否则 false。</returns>
public static bool TryGet(string extension, out string mediaType, out string contentType, out bool browserPlayable)
{
if (Types.TryGetValue(extension, out var value))
@ -43,6 +54,11 @@ namespace FileShare_Services.Services.FileLibrary
return false;
}
/// <summary>
/// 判断指定扩展名对应的媒体是否可在浏览器中直接播放。
/// </summary>
/// <param name="extension">文件扩展名(含点号)。</param>
/// <returns>浏览器可播放时返回 true。</returns>
public static bool IsBrowserPlayable(string extension)
{
return Types.TryGetValue(extension, out var value) && value.BrowserPlayable;

View File

@ -2,8 +2,16 @@ using FileShare_Services.Core;
namespace FileShare_Services.Services.QrCode
{
/// <summary>
/// 二维码生成服务接口,根据当前局域网地址生成访问二维码。
/// </summary>
public interface IQrCodeService
{
/// <summary>
/// 获取当前服务器的局域网 IP 地址并生成对应的访问二维码。
/// </summary>
/// <param name="ctx">端点上下文。</param>
/// <returns>包含 URL 和 Base64 PNG 二维码的响应。</returns>
Task<object?> GenerateQrCodeAsync(ServiceEndpointContext ctx);
}
}

View File

@ -1,4 +1,7 @@
namespace FileShare_Services.Services.QrCode
{
/// <summary>
/// 二维码生成响应,包含访问 URL 和 Base64 编码的 PNG 二维码图片。
/// </summary>
public sealed record QrCodeResponse(string Url, string QrCodeBase64);
}

View File

@ -7,8 +7,12 @@ using System.Net.Sockets;
namespace FileShare_Services.Services.QrCode
{
/// <summary>
/// 二维码生成服务,获取局域网 IP 并生成 PNG 格式的访问二维码。
/// </summary>
public sealed class QrCodeService : IQrCodeService
{
/// <inheritdoc />
public Task<object?> GenerateQrCodeAsync(ServiceEndpointContext ctx)
{
var ip = GetLanIpAddress();
@ -20,6 +24,11 @@ namespace FileShare_Services.Services.QrCode
return Task.FromResult<object?>(ResponseHelper.Ok(new QrCodeResponse(url, base64)));
}
/// <summary>
/// 使用 QRCoder 库生成指定内容的二维码,输出为 Base64 编码的 PNG data URI。
/// </summary>
/// <param name="content">二维码编码内容。</param>
/// <returns>data URI 格式的 Base64 PNG 字符串。</returns>
private static string GeneratePngBase64(string content)
{
using var generator = new QRCodeGenerator();
@ -29,6 +38,10 @@ namespace FileShare_Services.Services.QrCode
return $"data:image/png;base64,{Convert.ToBase64String(bytes)}";
}
/// <summary>
/// 获取本机第一个可用的局域网 IPv4 地址,排除回环地址和 APIPA 地址169.254.x.x
/// </summary>
/// <returns>局域网 IP 地址字符串,无可用地址时返回 null。</returns>
private static string? GetLanIpAddress()
{
return NetworkInterface.GetAllNetworkInterfaces()