190 lines
6.3 KiB
C#
190 lines
6.3 KiB
C#
|
|
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;
|
|||
|
|
}
|
|||
|
|
}
|