190 lines
6.3 KiB
C#
Raw Normal View History

2026-06-22 09:20:20 +08:00
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<string> ImageExt = new(StringComparer.OrdinalIgnoreCase)
{ ".jpg", ".jpeg", ".png", ".webp", ".gif", ".bmp", ".svg" };
private static readonly HashSet<string> RosterExt = new(StringComparer.OrdinalIgnoreCase)
{ ".txt", ".csv", ".xlsx", ".xls" };
private static readonly HashSet<string> 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<object> ListFiles(string folderPath, string[] subDirCandidates, HashSet<string> allowedExt)
{
var subDir = ResolveSubDir(folderPath, subDirCandidates);
var dir = Path.Combine(folderPath, subDir);
if (!Directory.Exists(dir))
{
return new List<object>();
}
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;
}
}