using IM_API.Dtos; using IM_API.Interface.Services; using IM_API.Models.Upload; using IM_API.Tools; using IM_API.VOs; using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Mvc; using Microsoft.EntityFrameworkCore.Storage; using StackExchange.Redis; using System.Text; using System.Text.Json; using IDatabase = StackExchange.Redis.IDatabase; namespace IM_API.Controllers { [Authorize] [Route("api/[controller]")] [ApiController] public class UploadController : ControllerBase { private readonly IWebHostEnvironment _env; private readonly IStorageService _storage; private readonly IDatabase _redis; public UploadController(IWebHostEnvironment env, IStorageService storage, IConnectionMultiplexer connectionMultiplexer) { _env = env; _storage = storage; _redis = connectionMultiplexer.GetDatabase(); } [HttpPost("local/{taskId}/parts/{partNumber}")] [ProducesResponseType(typeof(BaseResponse), StatusCodes.Status200OK)] public async Task LocalUpload(Guid taskId, int partNumber, IFormFile file) { var baseDir = Path.Combine(_env.ContentRootPath, "uploads"); // 项目根目录下 uploads Directory.CreateDirectory(baseDir); var path = Path.Combine(baseDir, "temp", taskId.ToString(), $"{partNumber}.part.tmp"); Directory.CreateDirectory(Path.GetDirectoryName(path)!); using var stream = System.IO.File.Create(path); await file.CopyToAsync(stream); await _redis.SetAddAsync(RedisKeys.GetUploadPartKey(taskId), partNumber); return Ok(new BaseResponse()); } [HttpPost("CreateTask")] [ProducesResponseType(typeof(BaseResponse), StatusCodes.Status200OK)] public async Task CreateUpload(CreateUploadTaskDto dto) { var vo = await _storage.InitTaskAsync(dto); return Ok(new BaseResponse(vo)); } [HttpPost("CreatePart")] public async Task CreatePart(Guid taskId, int partNum) { var vo = await _storage.CreatePartInstructionAsync(taskId, partNum); return Ok(new BaseResponse(vo)); } [HttpPost("CompleteTask")] public async Task CompleteTask([FromQuery]Guid taskId, [FromBody]List dtos) { var taskIdRes = await _storage.CompleteAsync(taskId, dtos); return Ok(new BaseResponse(data: taskIdRes.ToString())); } [HttpGet("events/{taskId}")] [AllowAnonymous] public async Task Events(Guid taskId) { Response.Headers.Add("Content-Type", "text/event-stream"); Response.Headers.Add("Cache-Control", "no-cache"); Response.Headers.Add("Connection", "keep-alive"); var lastProgress = -1; while (!HttpContext.RequestAborted.IsCancellationRequested) { var hash = await _redis.HashGetAllAsync(RedisKeys.MergeStatus(taskId)); if (hash.Length == 0) { await Task.Delay(1000); continue; } var status = hash.FirstOrDefault(x => x.Name == "status").Value; var progress = hash.FirstOrDefault(x => x.Name == "progress").Value; var url = hash.FirstOrDefault(x => x.Name == "url").Value; // 避免重复发送 if (progress != lastProgress) { var data = new { status = status.ToString(), progress = progress.ToString(), url = (string)url }; await Response.WriteAsync($"data: {JsonSerializer.Serialize(data)}\n\n"); await Response.Body.FlushAsync(); // 完成后关闭 SSE if (status == "Completed") break; await Task.Delay(1000); // 每秒检查一次 } } } [HttpPost("upload/{hash}")] public async Task UploadSmallFile(IFormFile file,string hash) { using var stream = file.OpenReadStream(); var res = await _storage.UploadSmallFileAsync(stream, file.FileName, file.ContentType, file.Length, hash); return Ok(new BaseResponse(res)); } } }