diff --git a/FileShare-API/Services/FileLibraryScanHostedService.cs b/FileShare-API/Services/FileLibraryScanHostedService.cs index 0431c33..2e5b9f0 100644 --- a/FileShare-API/Services/FileLibraryScanHostedService.cs +++ b/FileShare-API/Services/FileLibraryScanHostedService.cs @@ -2,11 +2,21 @@ using FileShare_Services.Services.FileLibrary; namespace FileShare_API.Services { + /// + /// 文件库定时扫描后台服务,每分钟执行一次扫描,检查是否有需要扫描的文件库目录。 + /// public sealed class FileLibraryScanHostedService(IServiceScopeFactory scopeFactory, ILogger logger) : BackgroundService { + /// + /// 扫描间隔,固定为 1 分钟。 + /// private static readonly TimeSpan Interval = TimeSpan.FromMinutes(1); + /// + /// 启动扫描循环,首次立即执行,之后按 周期重复执行。 + /// + /// 应用关闭时触发的取消令牌。 protected override async Task ExecuteAsync(CancellationToken stoppingToken) { await ScanAsync(stoppingToken); @@ -18,6 +28,10 @@ namespace FileShare_API.Services } } + /// + /// 创建临时作用域并调用 执行一次到期扫描。 + /// + /// 取消令牌。 private async Task ScanAsync(CancellationToken cancellationToken) { try diff --git a/FileShare-EFCore/Database/AppDataContextFactory.cs b/FileShare-EFCore/Database/AppDataContextFactory.cs index 6b3da08..5172243 100644 --- a/FileShare-EFCore/Database/AppDataContextFactory.cs +++ b/FileShare-EFCore/Database/AppDataContextFactory.cs @@ -58,8 +58,18 @@ namespace FileShare_EFCore.Database => new(DesignTimeDatabaseConfiguration.Create(args, DatabaseProvider.MySQL)); } + /// + /// 为设计时工具(dotnet ef migrations)提供数据库连接配置。 + /// internal static class DesignTimeDatabaseConfiguration { + /// + /// 解析命令行参数中的 --provider 选项,创建对应的 。 + /// 未指定提供程序时默认使用 SQLite。 + /// + /// 命令行参数。 + /// 未指定时的默认数据库提供程序。 + /// 对应数据库提供程序的配置。 public static DatabaseConfiguration Create(string[] args, DatabaseProvider defaultProvider = DatabaseProvider.SQLite) { DatabaseProviderRegistry.RegisterDefaults(); @@ -75,6 +85,12 @@ namespace FileShare_EFCore.Database }; } + /// + /// 从命令行参数中解析 --provider 选项的值。 + /// 支持 --provider sqlite--provider=sqlite 两种格式。 + /// + /// 命令行参数。 + /// 解析到的数据库提供程序,未指定或无法识别时返回 null。 private static DatabaseProvider? GetProvider(string[] args) { for (var i = 0; i < args.Length; i++) diff --git a/FileShare-EFCore/Database/DatabaseExtensions.cs b/FileShare-EFCore/Database/DatabaseExtensions.cs index 0c7dce0..41aebac 100644 --- a/FileShare-EFCore/Database/DatabaseExtensions.cs +++ b/FileShare-EFCore/Database/DatabaseExtensions.cs @@ -40,6 +40,12 @@ namespace FileShare_EFCore.Database return services; } + /// + /// 根据 注册对应具体类型的 实现。 + /// + /// 服务集合。 + /// 数据库配置。 + /// 数据库提供程序未注册时抛出。 private static void AddProviderAppDataContext(this IServiceCollection services, DatabaseConfiguration config) { switch (config.Provider) diff --git a/FileShare-PC/Views/MainWindow.axaml.cs b/FileShare-PC/Views/MainWindow.axaml.cs index 1a3ba3e..895d573 100644 --- a/FileShare-PC/Views/MainWindow.axaml.cs +++ b/FileShare-PC/Views/MainWindow.axaml.cs @@ -684,6 +684,10 @@ namespace FileShare_PC.Views return true; } + /// + /// 为媒体流响应添加 CORS 和 Range 请求头,允许浏览器跨域访问和分段请求。 + /// + /// HTTP 响应对象。 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"; } + /// + /// 解析 HTTP Range 请求头中的 bytes=start-end 格式,支持两端省略和后缀长度(如 bytes=-500)。 + /// + /// Range 请求头值。 + /// 资源总字节数。 + /// 解析出的起始字节偏移。 + /// 解析出的结束字节偏移。 + /// 解析成功返回 true,否则 false。 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; } + /// + /// 从输入流读取指定字节数并写入输出流,用于实现 HTTP Range 分段响应。 + /// + /// 源文件流。 + /// HTTP 响应输出流。 + /// 需要传输的剩余字节数。 private static async Task CopyRangeAsync(Stream input, Stream output, long bytesRemaining) { var buffer = new byte[64 * 1024]; diff --git a/FileShare-Services/Core/ServiceEndpointPatternMatcher.cs b/FileShare-Services/Core/ServiceEndpointPatternMatcher.cs index d6cbca2..064a0a5 100644 --- a/FileShare-Services/Core/ServiceEndpointPatternMatcher.cs +++ b/FileShare-Services/Core/ServiceEndpointPatternMatcher.cs @@ -47,11 +47,22 @@ namespace FileShare_Services.Core return true; } + /// + /// 将路径按 / 拆分为非空片段数组。 + /// + /// 路径字符串。 + /// 过滤空条目后的片段数组。 private static string[] SplitSegments(string value) { return value.Split('/', StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries); } + /// + /// 尝试从 {name}{name:constraint} 格式的片段中提取参数名称。 + /// + /// 模式片段。 + /// 提取出的参数名称。 + /// 该片段是路由参数时返回 true,否则 false。 private static bool TryGetParameterName(string segment, out string parameterName) { parameterName = string.Empty; @@ -66,6 +77,12 @@ namespace FileShare_Services.Core return !string.IsNullOrWhiteSpace(parameterName); } + /// + /// 检查路由值是否满足片段中的类型约束(目前仅支持 :int)。 + /// + /// 包含约束的模式片段。 + /// 实际路径值。 + /// 满足约束时返回 true。 private static bool MatchesConstraint(string segment, string value) { return !segment.EndsWith(":int}", StringComparison.OrdinalIgnoreCase) diff --git a/FileShare-Services/Core/ServiceRequestBinder.cs b/FileShare-Services/Core/ServiceRequestBinder.cs index 6e1dab3..7ff9f71 100644 --- a/FileShare-Services/Core/ServiceRequestBinder.cs +++ b/FileShare-Services/Core/ServiceRequestBinder.cs @@ -37,6 +37,14 @@ namespace FileShare_Services.Core return Deserialize(json, "query"); } + /// + /// 将 JSON 反序列化为目标类型,反序列化失败或结果为 null 时抛出 。 + /// + /// 目标类型。 + /// JSON 字符串。 + /// 来源标识(body 或 query),用于异常消息。 + /// 反序列化后的实例。 + /// JSON 无法绑定到目标类型时抛出。 private static T Deserialize(string json, string source) { try diff --git a/FileShare-Services/Endpoints/AppEndpoints.cs b/FileShare-Services/Endpoints/AppEndpoints.cs index 8a3b717..4080f4b 100644 --- a/FileShare-Services/Endpoints/AppEndpoints.cs +++ b/FileShare-Services/Endpoints/AppEndpoints.cs @@ -98,6 +98,12 @@ namespace FileShare_Services.Endpoints #region 业务处理方法 + /// + /// 从 中解析 , + /// 读取查询参数中的文件 ID,返回文件流响应。 + /// + /// 端点上下文。 + /// 文件流响应对象,服务不可用或 ID 无效时返回 null。 private static async Task GetFileStreamAsync(ServiceEndpointContext ctx) { var sp = ctx.Items["ServiceProvider"] as IServiceProvider; diff --git a/FileShare-Services/Services/FileLibrary/FileLibraryContracts.cs b/FileShare-Services/Services/FileLibrary/FileLibraryContracts.cs index 665b2b1..a45b321 100644 --- a/FileShare-Services/Services/FileLibrary/FileLibraryContracts.cs +++ b/FileShare-Services/Services/FileLibrary/FileLibraryContracts.cs @@ -2,27 +2,53 @@ using System.Text.Json.Serialization; namespace FileShare_Services.Services.FileLibrary { + /// + /// 添加文件库根目录的请求。 + /// 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); + /// + /// 更新文件库根目录启用状态的请求。 + /// public sealed record UpdateLibraryRootRequest( [property: JsonPropertyName("id")] int Id, [property: JsonPropertyName("isEnabled")] bool IsEnabled); + /// + /// 触发文件库根目录立即扫描的请求。 + /// public sealed record ScanLibraryRootRequest( [property: JsonPropertyName("id")] int Id); + /// + /// 删除文件库根目录的请求。 + /// public sealed record DeleteLibraryRootRequest( [property: JsonPropertyName("id")] int Id); + /// + /// 查询服务器子目录的请求。 + /// public sealed record DirectoryQueryRequest( [property: JsonPropertyName("path")] string? Path); + /// + /// 根据 ID 查询文件的请求。 + /// public sealed record FileQueryRequest( [property: JsonPropertyName("id")] int Id); + /// + /// 分页搜索已扫描文件的请求。 + /// 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); + /// + /// 磁盘驱动器信息。 + /// public sealed record DriveDto( string Name, string DisplayName, @@ -39,10 +68,16 @@ namespace FileShare_Services.Services.FileLibrary long? AvailableFreeSpace, bool IsReady); + /// + /// 服务器子目录信息。 + /// public sealed record DirectoryDto( string Name, string FullPath); + /// + /// 文件库根目录信息,包含扫描状态与文件数量。 + /// public sealed record LibraryRootDto( int Id, string Path, @@ -55,6 +90,9 @@ namespace FileShare_Services.Services.FileLibrary string? LastScanError, int FileCount); + /// + /// 已扫描文件的记录信息,包含媒体类型与流式访问 URL。 + /// public sealed record FileRecordDto( int Id, int LibraryRootId, @@ -69,15 +107,24 @@ namespace FileShare_Services.Services.FileLibrary string? TextUrl, bool BrowserPlayable); + /// + /// 浏览文件库目录结构的请求。 + /// public sealed record BrowseDirectoryRequest( [property: JsonPropertyName("rootId")] int RootId = 0, [property: JsonPropertyName("path")] string? Path = null); + /// + /// 浏览文件库目录的响应,包含当前路径、子目录列表和文件列表。 + /// public sealed record BrowseDirectoryResponse( string CurrentPath, List Subdirectories, List Files); + /// + /// 文本文件预览内容,支持截断标记。 + /// public sealed record TextPreviewDto( int Id, string FileName, diff --git a/FileShare-Services/Services/FileLibrary/FileLibraryEndpointService.cs b/FileShare-Services/Services/FileLibrary/FileLibraryEndpointService.cs index 3bc0bd1..e553796 100644 --- a/FileShare-Services/Services/FileLibrary/FileLibraryEndpointService.cs +++ b/FileShare-Services/Services/FileLibrary/FileLibraryEndpointService.cs @@ -3,49 +3,61 @@ using FileShare_Services.Core; namespace FileShare_Services.Services.FileLibrary { + /// + /// 文件库 HTTP 端点服务,将请求适配到 并包装为 。 + /// public sealed class FileLibraryEndpointService(IFileLibraryService fileLibrary) : IFileLibraryEndpointService { + /// public async Task GetDrivesAsync(ServiceEndpointContext ctx) { return ResponseHelper.Ok(await fileLibrary.GetDrivesAsync()); } + /// public async Task GetDirectoriesAsync(DirectoryQueryRequest request) { return ResponseHelper.Ok(await fileLibrary.GetDirectoriesAsync(request.Path)); } + /// public async Task GetRootsAsync(ServiceEndpointContext ctx) { return ResponseHelper.Ok(await fileLibrary.GetRootsAsync()); } + /// public async Task AddRootAsync(AddLibraryRootRequest request) { return ResponseHelper.Ok(await fileLibrary.AddRootAsync(request), "文件库目录已添加并完成扫描。"); } + /// public async Task SetRootEnabledAsync(UpdateLibraryRootRequest request) { return ResponseHelper.Ok(await fileLibrary.SetRootEnabledAsync(request), "文件库目录状态已更新。"); } + /// public async Task DeleteRootAsync(DeleteLibraryRootRequest request) { await fileLibrary.DeleteRootAsync(request); return ResponseHelper.Succeed("文件库目录已删除。"); } + /// public async Task ScanRootAsync(ScanLibraryRootRequest request) { return ResponseHelper.Ok(await fileLibrary.ScanRootAsync(request.Id), "文件库目录扫描完成。"); } + /// public async Task SearchFilesAsync(SearchFilesRequest request) { return await fileLibrary.SearchFilesAsync(request); } + /// public async Task GetFileAsync(FileQueryRequest request) { ValidateFileId(request.Id); @@ -55,6 +67,7 @@ namespace FileShare_Services.Services.FileLibrary : ResponseHelper.Ok(file); } + /// public async Task GetTextPreviewAsync(FileQueryRequest request) { ValidateFileId(request.Id); @@ -64,6 +77,7 @@ namespace FileShare_Services.Services.FileLibrary : ResponseHelper.Ok(preview); } + /// public async Task BrowseDirectoryAsync(BrowseDirectoryRequest request) { if (request.RootId <= 0) @@ -73,6 +87,11 @@ namespace FileShare_Services.Services.FileLibrary return ResponseHelper.Ok(result); } + /// + /// 验证文件 ID 是否有效,无效时抛出 。 + /// + /// 文件记录 ID。 + /// ID 小于等于 0 时抛出。 private static void ValidateFileId(int id) { if (id > 0) diff --git a/FileShare-Services/Services/FileLibrary/FileLibraryService.cs b/FileShare-Services/Services/FileLibrary/FileLibraryService.cs index d8b4eff..8481baf 100644 --- a/FileShare-Services/Services/FileLibrary/FileLibraryService.cs +++ b/FileShare-Services/Services/FileLibrary/FileLibraryService.cs @@ -7,11 +7,21 @@ using System.Text; namespace FileShare_Services.Services.FileLibrary { + /// + /// 文件库核心业务服务,实现磁盘枚举、目录管理、文件扫描与检索。 + /// public sealed class FileLibraryService(AppDataContext db) : IFileLibraryService { + /// + /// 默认扫描间隔(分钟),当请求未指定间隔时使用。 + /// private const int DefaultScanIntervalMinutes = 5; + /// + /// 文本预览最大读取字节数(1 MB)。 + /// private const int MaxTextPreviewBytes = 1024 * 1024; + /// public Task> GetDrivesAsync(CancellationToken cancellationToken = default) { var drives = DriveInfo.GetDrives() @@ -29,6 +39,7 @@ namespace FileShare_Services.Services.FileLibrary return Task.FromResult(drives); } + /// public Task> GetDirectoriesAsync(string? path, CancellationToken cancellationToken = default) { var normalized = NormalizeExistingDirectory(path); @@ -41,6 +52,7 @@ namespace FileShare_Services.Services.FileLibrary return Task.FromResult(directories); } + /// public async Task> 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(); } + /// public async Task 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); } + /// public async Task 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); } + /// 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); } + /// public async Task 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); } + /// public async Task ScanDueRootsAsync(CancellationToken cancellationToken = default) { var now = DateTime.UtcNow; @@ -213,6 +230,7 @@ namespace FileShare_Services.Services.FileLibrary } } + /// public async Task> 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.From(items, total, page, pageSize); } + /// public async Task GetFileAsync(int id, CancellationToken cancellationToken = default) { return await db.ManagedFileRecords @@ -261,6 +280,7 @@ namespace FileShare_Services.Services.FileLibrary .FirstOrDefaultAsync(cancellationToken); } + /// public async Task BrowseDirectoryAsync(BrowseDirectoryRequest request, CancellationToken cancellationToken = default) { var rootId = request.RootId; @@ -309,6 +329,7 @@ namespace FileShare_Services.Services.FileLibrary currentFiles); } + /// public async Task 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); } + /// + /// 深度优先遍历目录树,枚举所有被 支持的媒体文件路径。 + /// 遇到无权限的目录时跳过该分支继续遍历。 + /// + /// 根目录路径。 + /// 支持的文件完整路径枚举。 private static IEnumerable EnumerateSupportedFiles(string rootPath) { var pending = new Stack(); @@ -371,6 +398,13 @@ namespace FileShare_Services.Services.FileLibrary } } + /// + /// 规范化目录路径并验证目录存在。 + /// + /// 原始路径。 + /// 规范化后的完整路径。 + /// 路径为空时抛出。 + /// 目录不存在时抛出。 private static string NormalizeExistingDirectory(string? path) { if (string.IsNullOrWhiteSpace(path)) @@ -387,6 +421,12 @@ namespace FileShare_Services.Services.FileLibrary return new DirectoryInfo(fullPath).FullName; } + /// + /// 解析根目录的显示名称,优先使用用户指定名称,否则使用目录名。 + /// + /// 目录路径。 + /// 用户指定的显示名称。 + /// 最终显示名称。 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; } + /// + /// 规范化扫描间隔,限制在 1 到 1440 分钟范围内,未指定时使用默认值。 + /// + /// 用户指定的间隔。 + /// 规范化后的间隔分钟数。 private static int NormalizeInterval(int? interval) { return Math.Clamp(interval ?? DefaultScanIntervalMinutes, 1, 1440); } + /// + /// 安全获取驱动器属性值,驱动器未就绪或访问异常时返回 null。 + /// + /// 驱动器信息。 + /// 属性选择器。 + /// 属性值,不可用时返回 null。 private static long? SafeDriveValue(DriveInfo drive, Func selector) { try @@ -415,6 +466,12 @@ namespace FileShare_Services.Services.FileLibrary } } + /// + /// 将 实体映射为 。 + /// + /// 数据库实体。 + /// 关联的文件记录数。 + /// DTO 对象。 private static LibraryRootDto ToRootDto(ManagedLibraryRoot root, int fileCount) { return new LibraryRootDto( @@ -430,6 +487,11 @@ namespace FileShare_Services.Services.FileLibrary fileCount); } + /// + /// 将 实体映射为 ,并生成流式访问和文本预览 URL。 + /// + /// 数据库实体。 + /// DTO 对象。 private static FileRecordDto ToFileDto(ManagedFileRecord file) { return new FileRecordDto( diff --git a/FileShare-Services/Services/FileLibrary/FileStreamService.cs b/FileShare-Services/Services/FileLibrary/FileStreamService.cs index ec7931e..f91cd5a 100644 --- a/FileShare-Services/Services/FileLibrary/FileStreamService.cs +++ b/FileShare-Services/Services/FileLibrary/FileStreamService.cs @@ -5,13 +5,26 @@ using Microsoft.EntityFrameworkCore; namespace FileShare_Services.Services.FileLibrary { + /// + /// 文件流服务接口,根据文件记录 ID 返回物理文件流信息。 + /// public interface IFileStreamService { + /// + /// 获取指定文件的流式传输响应,包含文件路径、名称、Content-Type 和修改时间。 + /// + /// 文件记录 ID。 + /// 取消令牌。 + /// 文件流响应,文件不存在时返回 null。 Task GetFileStreamAsync(int id, CancellationToken cancellationToken = default); } + /// + /// 文件流服务实现,从数据库查询文件记录并构建 。 + /// public sealed class FileStreamService(AppDataContext db) : IFileStreamService { + /// public async Task GetFileStreamAsync(int id, CancellationToken cancellationToken = default) { var file = await db.ManagedFileRecords diff --git a/FileShare-Services/Services/FileLibrary/IFileLibraryEndpointService.cs b/FileShare-Services/Services/FileLibrary/IFileLibraryEndpointService.cs index 4b0a6d6..21e3b23 100644 --- a/FileShare-Services/Services/FileLibrary/IFileLibraryEndpointService.cs +++ b/FileShare-Services/Services/FileLibrary/IFileLibraryEndpointService.cs @@ -3,28 +3,86 @@ using FileShare_Services.Core; namespace FileShare_Services.Services.FileLibrary { + /// + /// 文件库 HTTP 端点服务接口,将 HTTP 请求适配到 业务层。 + /// public interface IFileLibraryEndpointService { + /// + /// 获取服务器所有磁盘驱动器。 + /// + /// 端点上下文。 + /// API 响应。 Task GetDrivesAsync(ServiceEndpointContext ctx); + /// + /// 查询指定路径下的子目录。 + /// + /// 包含路径的查询请求。 + /// API 响应。 Task GetDirectoriesAsync(DirectoryQueryRequest request); + /// + /// 获取所有已注册的文件库根目录。 + /// + /// 端点上下文。 + /// API 响应。 Task GetRootsAsync(ServiceEndpointContext ctx); + /// + /// 添加新的文件库根目录。 + /// + /// 包含路径和配置的请求。 + /// API 响应。 Task AddRootAsync(AddLibraryRootRequest request); + /// + /// 启用或禁用指定的文件库根目录。 + /// + /// 包含根目录 ID 和目标状态的请求。 + /// API 响应。 Task SetRootEnabledAsync(UpdateLibraryRootRequest request); + /// + /// 删除指定的文件库根目录。 + /// + /// 包含根目录 ID 的请求。 + /// API 响应。 Task DeleteRootAsync(DeleteLibraryRootRequest request); + /// + /// 立即扫描指定文件库根目录。 + /// + /// 包含根目录 ID 的请求。 + /// API 响应。 Task ScanRootAsync(ScanLibraryRootRequest request); + /// + /// 分页搜索已扫描的文件记录。 + /// + /// 包含分页和过滤条件的请求。 + /// API 响应。 Task SearchFilesAsync(SearchFilesRequest request); + /// + /// 根据 ID 获取文件详情。 + /// + /// 包含文件 ID 的请求。 + /// API 响应。 Task GetFileAsync(FileQueryRequest request); + /// + /// 获取文本文件预览内容。 + /// + /// 包含文件 ID 的请求。 + /// API 响应。 Task GetTextPreviewAsync(FileQueryRequest request); + /// + /// 浏览文件库目录结构。 + /// + /// 包含根目录 ID 和路径的请求。 + /// API 响应。 Task BrowseDirectoryAsync(BrowseDirectoryRequest request); } } diff --git a/FileShare-Services/Services/FileLibrary/IFileLibraryService.cs b/FileShare-Services/Services/FileLibrary/IFileLibraryService.cs index 8485e83..6ea9da5 100644 --- a/FileShare-Services/Services/FileLibrary/IFileLibraryService.cs +++ b/FileShare-Services/Services/FileLibrary/IFileLibraryService.cs @@ -2,30 +2,100 @@ using FileShare_Common.Core; namespace FileShare_Services.Services.FileLibrary { + /// + /// 文件库核心业务服务接口,提供磁盘枚举、目录管理、文件扫描与检索功能。 + /// public interface IFileLibraryService { + /// + /// 获取服务器所有磁盘驱动器信息。 + /// + /// 取消令牌。 + /// 驱动器信息列表。 Task> GetDrivesAsync(CancellationToken cancellationToken = default); + /// + /// 获取指定路径下的子目录列表。 + /// + /// 父目录路径,为空时返回根目录。 + /// 取消令牌。 + /// 子目录信息列表。 Task> GetDirectoriesAsync(string? path, CancellationToken cancellationToken = default); + /// + /// 获取所有已注册的文件库根目录及其文件数量。 + /// + /// 取消令牌。 + /// 文件库根目录列表。 Task> GetRootsAsync(CancellationToken cancellationToken = default); + /// + /// 添加新的文件库根目录,如已存在则激活并重新扫描。 + /// + /// 包含路径、显示名称和扫描间隔的请求。 + /// 取消令牌。 + /// 新添加或已存在的根目录信息。 Task AddRootAsync(AddLibraryRootRequest request, CancellationToken cancellationToken = default); + /// + /// 启用或禁用指定的文件库根目录。 + /// + /// 包含根目录 ID 和目标启用状态的请求。 + /// 取消令牌。 + /// 更新后的根目录信息。 Task SetRootEnabledAsync(UpdateLibraryRootRequest request, CancellationToken cancellationToken = default); + /// + /// 删除指定的文件库根目录。 + /// + /// 包含根目录 ID 的请求。 + /// 取消令牌。 Task DeleteRootAsync(DeleteLibraryRootRequest request, CancellationToken cancellationToken = default); + /// + /// 立即扫描指定根目录,枚举所有支持的媒体文件并更新数据库。 + /// + /// 根目录 ID。 + /// 取消令牌。 + /// 扫描后的根目录信息。 Task ScanRootAsync(int rootId, CancellationToken cancellationToken = default); + /// + /// 扫描所有已到期(距上次扫描超过间隔时间)的根目录。 + /// + /// 取消令牌。 Task ScanDueRootsAsync(CancellationToken cancellationToken = default); + /// + /// 分页搜索已扫描的文件记录,支持按媒体类型、关键词和根目录过滤。 + /// + /// 包含分页和过滤条件的请求。 + /// 取消令牌。 + /// 分页文件记录响应。 Task> SearchFilesAsync(SearchFilesRequest request, CancellationToken cancellationToken = default); + /// + /// 根据 ID 获取单个文件的记录信息。 + /// + /// 文件记录 ID。 + /// 取消令牌。 + /// 文件记录 DTO,不存在时返回 null。 Task GetFileAsync(int id, CancellationToken cancellationToken = default); + /// + /// 获取文本文件的预览内容(最多 1 MB)。 + /// + /// 文件记录 ID。 + /// 取消令牌。 + /// 文本预览 DTO,文件不存在或非文本类型时返回 null。 Task GetTextPreviewAsync(int id, CancellationToken cancellationToken = default); + /// + /// 浏览指定根目录下的目录结构和文件列表。 + /// + /// 包含根目录 ID 和路径的请求。 + /// 取消令牌。 + /// 目录浏览响应,包含子目录和文件列表。 Task BrowseDirectoryAsync(BrowseDirectoryRequest request, CancellationToken cancellationToken = default); } } diff --git a/FileShare-Services/Services/FileLibrary/MediaFileTypes.cs b/FileShare-Services/Services/FileLibrary/MediaFileTypes.cs index 64e4cc0..3469564 100644 --- a/FileShare-Services/Services/FileLibrary/MediaFileTypes.cs +++ b/FileShare-Services/Services/FileLibrary/MediaFileTypes.cs @@ -1,5 +1,8 @@ namespace FileShare_Services.Services.FileLibrary { + /// + /// 受支持的媒体文件类型注册表,提供扩展名到媒体类型、Content-Type 和浏览器可播放性的查询。 + /// public static class MediaFileTypes { private static readonly Dictionary Types = @@ -27,6 +30,14 @@ namespace FileShare_Services.Services.FileLibrary [".oga"] = ("audio", "audio/ogg", true), }; + /// + /// 根据文件扩展名查询媒体类型信息。 + /// + /// 文件扩展名(含点号)。 + /// 输出:媒体分类(text、video、audio)。 + /// 输出:HTTP Content-Type。 + /// 输出:浏览器是否可直接播放。 + /// 扩展名已注册时返回 true,否则 false。 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; } + /// + /// 判断指定扩展名对应的媒体是否可在浏览器中直接播放。 + /// + /// 文件扩展名(含点号)。 + /// 浏览器可播放时返回 true。 public static bool IsBrowserPlayable(string extension) { return Types.TryGetValue(extension, out var value) && value.BrowserPlayable; diff --git a/FileShare-Services/Services/QrCode/IQrCodeService.cs b/FileShare-Services/Services/QrCode/IQrCodeService.cs index af33c86..8c92825 100644 --- a/FileShare-Services/Services/QrCode/IQrCodeService.cs +++ b/FileShare-Services/Services/QrCode/IQrCodeService.cs @@ -2,8 +2,16 @@ using FileShare_Services.Core; namespace FileShare_Services.Services.QrCode { + /// + /// 二维码生成服务接口,根据当前局域网地址生成访问二维码。 + /// public interface IQrCodeService { + /// + /// 获取当前服务器的局域网 IP 地址并生成对应的访问二维码。 + /// + /// 端点上下文。 + /// 包含 URL 和 Base64 PNG 二维码的响应。 Task GenerateQrCodeAsync(ServiceEndpointContext ctx); } } diff --git a/FileShare-Services/Services/QrCode/QrCodeContracts.cs b/FileShare-Services/Services/QrCode/QrCodeContracts.cs index 5d306e5..95758c8 100644 --- a/FileShare-Services/Services/QrCode/QrCodeContracts.cs +++ b/FileShare-Services/Services/QrCode/QrCodeContracts.cs @@ -1,4 +1,7 @@ namespace FileShare_Services.Services.QrCode { + /// + /// 二维码生成响应,包含访问 URL 和 Base64 编码的 PNG 二维码图片。 + /// public sealed record QrCodeResponse(string Url, string QrCodeBase64); } diff --git a/FileShare-Services/Services/QrCode/QrCodeService.cs b/FileShare-Services/Services/QrCode/QrCodeService.cs index 95bb853..c103cb2 100644 --- a/FileShare-Services/Services/QrCode/QrCodeService.cs +++ b/FileShare-Services/Services/QrCode/QrCodeService.cs @@ -7,8 +7,12 @@ using System.Net.Sockets; namespace FileShare_Services.Services.QrCode { + /// + /// 二维码生成服务,获取局域网 IP 并生成 PNG 格式的访问二维码。 + /// public sealed class QrCodeService : IQrCodeService { + /// public Task GenerateQrCodeAsync(ServiceEndpointContext ctx) { var ip = GetLanIpAddress(); @@ -20,6 +24,11 @@ namespace FileShare_Services.Services.QrCode return Task.FromResult(ResponseHelper.Ok(new QrCodeResponse(url, base64))); } + /// + /// 使用 QRCoder 库生成指定内容的二维码,输出为 Base64 编码的 PNG data URI。 + /// + /// 二维码编码内容。 + /// data URI 格式的 Base64 PNG 字符串。 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)}"; } + /// + /// 获取本机第一个可用的局域网 IPv4 地址,排除回环地址和 APIPA 地址(169.254.x.x)。 + /// + /// 局域网 IP 地址字符串,无可用地址时返回 null。 private static string? GetLanIpAddress() { return NetworkInterface.GetAllNetworkInterfaces()