Files
Migu2.0/MiGu.Server/Controllers/OtaReceiveController.cs
T
zhaowei.huangandCursor 4223a572c5 新增 OTA WatchDog 编排与车队健康/报警后端。
覆盖包库回传、任务下发、CDM 任务同步与报警采集,并为包/任务 ID 与上传文件名加上路径安全校验。

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-07-26 11:31:51 +08:00

109 lines
4.4 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.Authorization;
using Microsoft.AspNetCore.Mvc;
using MiGu.Server.Ota;
namespace MiGu.Server.Controllers;
/// <summary>
/// WatchDog 回传包接收端。
/// WatchDog 写死 POST 到 http://{config.serverIP}:8000/upload-mdcs/{routeKey}
/// 必须与参考 Electron Express :8000 路径一致;/api/ota/receive/* 仅作兼容别名。
/// </summary>
[ApiController]
[AllowAnonymous]
public class OtaReceiveController : ControllerBase
{
private readonly OtaStore _store;
private readonly ILogger<OtaReceiveController> _log;
public OtaReceiveController(OtaStore store, ILogger<OtaReceiveController> log)
{
_store = store;
_log = log;
}
[HttpGet("/hello")]
[HttpGet("/api/ota/receive/hello")]
public IActionResult Hello() => Ok("ok");
/// <summary>WatchDog 官方路径:/upload-mdcs/{Medullaexe|...}</summary>
[HttpPost("/upload-mdcs/{routeKey}")]
[HttpPost("/api/ota/receive/upload-mdcs/{routeKey}")]
[HttpPost("/api/ota/receive/upload-mdcs{routeKey}")]
[RequestSizeLimit(512_000_000)]
public Task<IActionResult> UploadMdcs(string routeKey, CancellationToken ct)
=> SaveAsync(routeKey, ct);
[HttpPost("/upload-history/{routeKey}")]
[HttpPost("/api/ota/receive/upload-history/{routeKey}")]
[HttpPost("/api/ota/receive/upload-history{routeKey}")]
[RequestSizeLimit(512_000_000)]
public async Task<IActionResult> UploadHistory(string routeKey, CancellationToken ct)
{
var ip = HttpContext.Connection.RemoteIpAddress?.ToString() ?? "unknown";
if (!_store.TryGetActivePullId(ip, out _))
{
_log.LogWarning("OTA history rejected without active pull session from {Ip}", ip);
return BadRequest("no active pull session");
}
var day = DateTime.Now.ToString("yyyy-MM-dd");
var dir = Path.Combine(_store.HistoryDir, day, SafeFileName(ip, "unknown"));
Directory.CreateDirectory(dir);
var file = await ReadFirstFileAsync(ct);
if (file == null || file.Length == 0) return BadRequest("empty");
// 净化文件名:routeKey / FileName 都可能含路径分隔符,必须 GetFileName 防穿越。
var rawName = string.IsNullOrWhiteSpace(file.FileName) ? routeKey : file.FileName;
var safeName = SafeFileName(rawName, "unnamed");
var path = Path.Combine(dir, safeName);
await using var fs = System.IO.File.Create(path);
await file.CopyToAsync(fs, ct);
_log.LogInformation("OTA history receive {Route} -> {Path} ({Len})", routeKey, path, file.Length);
return Ok(new { ok = true });
}
private async Task<IActionResult> SaveAsync(string routeKey, CancellationToken ct)
{
try
{
var clientIp = HttpContext.Connection.RemoteIpAddress?.ToString();
if (!_store.TryGetActivePullId(clientIp, out _))
{
_log.LogWarning("OTA mdcs rejected without active pull session from {Ip}", clientIp ?? "unknown");
return BadRequest("no active pull session");
}
var file = await ReadFirstFileAsync(ct);
if (file == null || file.Length == 0) return BadRequest("empty file");
var dest = _store.ResolveReceivePath(routeKey, clientIp);
Directory.CreateDirectory(Path.GetDirectoryName(dest)!);
await using (var fs = System.IO.File.Create(dest))
await file.CopyToAsync(fs, ct);
_log.LogInformation("OTA mdcs receive {Route} -> {Dest} ({Len})", routeKey, dest, file.Length);
return Ok(new { ok = true, path = dest });
}
catch (Exception ex)
{
_log.LogWarning(ex, "OTA receive failed {Route}", routeKey);
return BadRequest(ex.Message);
}
}
private async Task<IFormFile?> ReadFirstFileAsync(CancellationToken ct)
{
if (!Request.HasFormContentType) return null;
var form = await Request.ReadFormAsync(ct);
return form.Files.FirstOrDefault();
}
private static string SafeFileName(string raw, string fallback)
{
var safe = Path.GetFileName(raw);
if (string.IsNullOrWhiteSpace(safe)) safe = fallback;
foreach (var ch in Path.GetInvalidFileNameChars())
safe = safe.Replace(ch, '_');
return string.IsNullOrWhiteSpace(safe) ? fallback : safe;
}
}