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:
parent
9f8da2c063
commit
d93098638d
@ -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
|
||||
|
||||
@ -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++)
|
||||
|
||||
@ -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)
|
||||
|
||||
@ -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];
|
||||
|
||||
@ -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)
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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;
|
||||
|
||||
@ -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,
|
||||
|
||||
@ -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)
|
||||
|
||||
@ -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(
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
@ -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;
|
||||
|
||||
@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
@ -1,4 +1,7 @@
|
||||
namespace FileShare_Services.Services.QrCode
|
||||
{
|
||||
/// <summary>
|
||||
/// 二维码生成响应,包含访问 URL 和 Base64 编码的 PNG 二维码图片。
|
||||
/// </summary>
|
||||
public sealed record QrCodeResponse(string Url, string QrCodeBase64);
|
||||
}
|
||||
|
||||
@ -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()
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user