2026-06-22 09:20:20 +08:00

190 lines
6.3 KiB
C#
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

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;
}
}