using Microsoft.AspNetCore.Mvc; using Microsoft.AspNetCore.StaticFiles; using System.Runtime.Versioning; using System.Text.Json; using System.Windows.Forms; namespace YD_TrainingCompetition_Api.Controllers; // LibraryController 支持桌面端"用资源管理器选取任意赛事文件夹": // - scan:按约定子目录扫描所选文件夹,返回其中的背景/口令/LOGO/秩序册资源 // - file:把磁盘上任意已存在的文件通过 http 提供给前端(图片/音频/秩序册) [ApiController] [Route("api/[controller]")] public sealed class LibraryController : ControllerBase { private static readonly string[] BackgroundDirs = { "背景", "背景图" }; private static readonly string[] PasswordDirs = { "口令" }; private static readonly string[] LogoDirs = { "赛事LOGO", "赛事logo" }; private static readonly string[] RosterDirs = { "秩序册" }; private static readonly HashSet ImageExt = new(StringComparer.OrdinalIgnoreCase) { ".jpg", ".jpeg", ".png", ".webp", ".gif", ".bmp", ".svg" }; private static readonly HashSet RosterExt = new(StringComparer.OrdinalIgnoreCase) { ".txt", ".csv", ".xlsx", ".xls" }; private static readonly HashSet PasswordExt = new(StringComparer.OrdinalIgnoreCase) { ".mp3" }; private static readonly FileExtensionContentTypeProvider ContentTypeProvider = new(); // 在本机弹出系统文件夹选择对话框(供浏览器开发环境使用;桌面端优先走 Electron)。 [HttpPost("pick-folder")] [SupportedOSPlatform("windows")] public IActionResult PickFolder() { string? selectedPath = null; var thread = new Thread(() => { using var dialog = new FolderBrowserDialog { Description = "选择赛事文件夹", UseDescriptionForTitle = true, ShowNewFolderButton = false, }; if (dialog.ShowDialog() == DialogResult.OK) { selectedPath = dialog.SelectedPath; } }); thread.SetApartmentState(ApartmentState.STA); thread.Start(); thread.Join(); if (string.IsNullOrWhiteSpace(selectedPath)) { return Ok(new { canceled = true, path = (string?)null }); } return Ok(new { canceled = false, path = selectedPath }); } // 扫描用户选取的赛事文件夹,结构与原 static/manifest.json 中单个赛事一致。 [HttpGet("scan")] public IActionResult Scan([FromQuery] string path) { if (string.IsNullOrWhiteSpace(path)) { return BadRequest(new { message = "缺少文件夹路径" }); } var folderPath = path.Trim(); if (!Directory.Exists(folderPath)) { return NotFound(new { message = $"文件夹不存在:{folderPath}" }); } var folder = new DirectoryInfo(folderPath).Name; var displayName = ReadDisplayName(folderPath) ?? folder; var result = new { id = folderPath, name = displayName, folder, folderPath, backgrounds = ListFiles(folderPath, BackgroundDirs, ImageExt), passwords = ListFiles(folderPath, PasswordDirs, PasswordExt), logos = ListFiles(folderPath, LogoDirs, ImageExt), rosters = ListFiles(folderPath, RosterDirs, RosterExt), }; return Ok(result); } // 提供磁盘上任意已存在文件(仅限本机桌面端调用)。支持音视频拖动定位。 [HttpGet("file")] public IActionResult GetFile([FromQuery] string path) { if (string.IsNullOrWhiteSpace(path)) { return BadRequest(new { message = "缺少文件路径" }); } var filePath = path.Trim(); if (!System.IO.File.Exists(filePath)) { return NotFound(new { message = $"文件不存在:{filePath}" }); } if (!ContentTypeProvider.TryGetContentType(filePath, out var contentType)) { contentType = "application/octet-stream"; } return PhysicalFile(filePath, contentType, enableRangeProcessing: true); } private List ListFiles(string folderPath, string[] subDirCandidates, HashSet allowedExt) { var subDir = ResolveSubDir(folderPath, subDirCandidates); var dir = Path.Combine(folderPath, subDir); if (!Directory.Exists(dir)) { return new List(); } return Directory .EnumerateFiles(dir) .Where(file => { var name = Path.GetFileName(file); return !name.StartsWith("~$", StringComparison.Ordinal) && allowedExt.Contains(Path.GetExtension(file)); }) .OrderBy(file => Path.GetFileName(file), StringComparer.CurrentCulture) .Select(file => (object)new { file = Path.GetFileName(file), path = BuildFileUrl(file), }) .ToList(); } private static string ResolveSubDir(string folderPath, string[] candidates) { foreach (var name in candidates) { if (Directory.Exists(Path.Combine(folderPath, name))) { return name; } } return candidates[0]; } // 返回前端可直接请求的绝对地址,避免 resolveStaticUrl 误编码查询串。 private string BuildFileUrl(string absolutePath) { var encoded = Uri.EscapeDataString(absolutePath); return $"{Request.Scheme}://{Request.Host}/api/library/file?path={encoded}"; } private static string? ReadDisplayName(string folderPath) { var metaPath = Path.Combine(folderPath, "event.json"); if (!System.IO.File.Exists(metaPath)) { return null; } try { using var doc = JsonDocument.Parse(System.IO.File.ReadAllText(metaPath)); if (doc.RootElement.TryGetProperty("displayName", out var prop) && prop.ValueKind == JsonValueKind.String) { return prop.GetString(); } } catch { // 忽略损坏的 event.json } return null; } }