HIR 流式写入zip 返回前端
continuous-integration/drone/push Build is passing
Details
continuous-integration/drone/push Build is passing
Details
parent
93362ea4c9
commit
d78a42dead
|
|
@ -1,6 +1,8 @@
|
|||
using AutoMapper;
|
||||
using ExcelDataReader;
|
||||
using IRaCIS.Application.Contracts;
|
||||
using IRaCIS.Application.Interfaces;
|
||||
using IRaCIS.Core.API._ServiceExtensions.NewtonsoftJson;
|
||||
using IRaCIS.Core.Application.BusinessFilter;
|
||||
using IRaCIS.Core.Application.Contracts;
|
||||
using IRaCIS.Core.Application.Contracts.Dicom;
|
||||
|
|
@ -30,12 +32,16 @@ using Microsoft.Extensions.Logging;
|
|||
using Microsoft.Net.Http.Headers;
|
||||
using MiniExcelLibs;
|
||||
using Newtonsoft.Json;
|
||||
using NPOI.HPSF;
|
||||
using Serilog;
|
||||
using SharpCompress.Archives;
|
||||
using SharpCompress.Common;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.ComponentModel.DataAnnotations;
|
||||
using System.Data;
|
||||
using System.IO;
|
||||
using System.IO.Compression;
|
||||
using System.Linq;
|
||||
using System.Text;
|
||||
using System.Threading.Tasks;
|
||||
|
|
@ -880,7 +886,102 @@ namespace IRaCIS.Core.API.Controllers
|
|||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
|
||||
[HttpPost("download/PatientStudyBatchDownload")]
|
||||
public async Task<IActionResult> DownloadPatientStudyBatch([FromServices] IPatientService _patientService, [FromServices] IOSSService _oSSService,
|
||||
[FromServices] IHubContext<DownloadHub, IDownloadClient> _downLoadHub,
|
||||
PatientImageDownloadCommand inCommand)
|
||||
{
|
||||
var rusult = await _patientService.GetDownloadPatientStudyInfo(inCommand);
|
||||
|
||||
var patientList = rusult.Data;
|
||||
var downloadInfo = (SubejctVisitDownload)rusult.OtherData;
|
||||
|
||||
|
||||
long totalSize = downloadInfo.ImageSize;
|
||||
var abortToken = HttpContext.RequestAborted;
|
||||
var lastNotify = DateTime.UtcNow;
|
||||
|
||||
Response.ContentType = "application/zip";
|
||||
Response.Headers["Content-Disposition"] = $"attachment; filename=Image_{ExportExcelConverterDate.DateTimeInternationalToString(DateTime.Now, _userInfo.TimeZoneId)}.zip";
|
||||
Response.Headers["Cache-Control"] = "no-store";
|
||||
|
||||
|
||||
try
|
||||
{
|
||||
await using var responseStream = Response.BodyWriter.AsStream();
|
||||
using var zip = new ZipArchive(responseStream, ZipArchiveMode.Create, leaveOpen: true);
|
||||
|
||||
foreach (var patient in patientList)
|
||||
{
|
||||
foreach (var study in patient.StudyList)
|
||||
{
|
||||
abortToken.ThrowIfCancellationRequested();
|
||||
|
||||
var studyTime = study.StudyTime?.ToString("yyyy-MM-dd HH:mm:ss") ?? "UnknownTime";
|
||||
var modalitysStr = string.Join('_', study.SeriesList.Select(t => t.Modality).Distinct());
|
||||
|
||||
// ---------- DICOMDIR ----------
|
||||
var dicomDirPath = $"{patient.PatientIdStr}/{studyTime}_{modalitysStr}/DICOMDIR";
|
||||
var dicomDirEntry = zip.CreateEntry(dicomDirPath, CompressionLevel.Fastest);
|
||||
|
||||
await using (var entryStream = dicomDirEntry.Open())
|
||||
await using (var dirStream = await _oSSService.GetStreamFromOSSAsync(study.StudyDIRPath))
|
||||
{
|
||||
await dirStream.CopyToAsync(entryStream, 32 * 1024, abortToken);
|
||||
}
|
||||
|
||||
// ---------- IMAGE FILES ----------
|
||||
foreach (var series in study.SeriesList)
|
||||
{
|
||||
foreach (var instance in series.InstanceList)
|
||||
{
|
||||
abortToken.ThrowIfCancellationRequested();
|
||||
|
||||
var entryPath =
|
||||
$"{patient.PatientIdStr}/{studyTime}_{modalitysStr}/IMAGE/{instance.FileName}";
|
||||
|
||||
var entry = zip.CreateEntry(entryPath, CompressionLevel.Fastest);
|
||||
|
||||
await using var entryStream = entry.Open();
|
||||
await using var source = await _oSSService.GetStreamFromOSSAsync(instance.Path);
|
||||
|
||||
await source.CopyToAsync(entryStream, 32 * 1024, abortToken);
|
||||
|
||||
|
||||
//await _downLoadHub.Clients.User(_userInfo.IdentityUserId.ToString()).ReceivProgressAsync(archiveStudyCommand.StudyInstanceUid, receivedCount);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 正常完成
|
||||
await _patientService.DownloadImageSuccess(downloadInfo.Id);
|
||||
}
|
||||
catch (OperationCanceledException)
|
||||
{
|
||||
// ✅ 客户端取消 / 断开 —— 正常情况
|
||||
Log.Logger.Warning("Download canceled by client");
|
||||
}
|
||||
catch (IOException ex) when (abortToken.IsCancellationRequested)
|
||||
{
|
||||
// ✅ HttpClient 流在中断时的常见异常
|
||||
Log.Logger.Warning($"Client disconnected: {ex.Message}");
|
||||
}
|
||||
catch (NullReferenceException ex) when (abortToken.IsCancellationRequested)
|
||||
{
|
||||
// ✅ HttpConnection.ContentLengthReadStream 已知问题
|
||||
Log.Logger.Warning($"Stream aborted: {ex.Message}");
|
||||
}
|
||||
|
||||
return new EmptyResult();
|
||||
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
|||
|
|
@ -247,6 +247,7 @@ app.MapMasaMinimalAPIs();
|
|||
app.MapControllers();
|
||||
|
||||
app.MapHub<UploadHub>("/UploadHub");
|
||||
app.MapHub<DownloadHub>("/DownloadHub");
|
||||
app.MapHealthChecks("/health");
|
||||
|
||||
|
||||
|
|
|
|||
|
|
@ -3,14 +3,12 @@ using Microsoft.AspNetCore.Authorization;
|
|||
using Microsoft.AspNetCore.Cors;
|
||||
using Microsoft.AspNetCore.SignalR;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using System;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace IRaCIS.Core.API
|
||||
{
|
||||
public interface IUploadClient
|
||||
{
|
||||
Task ReceivProgressAsync(string studyInstanceUid, int haveReceivedCount);
|
||||
}
|
||||
|
||||
|
||||
|
||||
public class IRaCISUserIdProvider : IUserIdProvider
|
||||
|
|
@ -21,6 +19,11 @@ namespace IRaCIS.Core.API
|
|||
}
|
||||
}
|
||||
|
||||
public interface IUploadClient
|
||||
{
|
||||
Task ReceivProgressAsync(string studyInstanceUid, int haveReceivedCount);
|
||||
}
|
||||
|
||||
[AllowAnonymous]
|
||||
[DisableCors]
|
||||
public class UploadHub : Hub<IUploadClient>
|
||||
|
|
@ -53,5 +56,34 @@ namespace IRaCIS.Core.API
|
|||
// await Clients.All.ReceivProgressAsync(studyInstanceUid, haveReceivedCount);
|
||||
//}
|
||||
|
||||
}
|
||||
|
||||
|
||||
public interface IDownloadClient
|
||||
{
|
||||
Task ReceivProgressAsync(Guid downloadId, string percent);
|
||||
}
|
||||
|
||||
|
||||
[AllowAnonymous]
|
||||
[DisableCors]
|
||||
public class DownloadHub : Hub<IDownloadClient>
|
||||
{
|
||||
|
||||
public ILogger<UploadHub> _logger { get; set; }
|
||||
public DownloadHub(ILogger<UploadHub> logger)
|
||||
{
|
||||
_logger = logger;
|
||||
}
|
||||
|
||||
public override Task OnConnectedAsync()
|
||||
{
|
||||
_logger.LogError("连接: " + Context.ConnectionId);
|
||||
|
||||
return base.OnConnectedAsync();
|
||||
}
|
||||
|
||||
|
||||
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -541,16 +541,46 @@ public class OSSService : IOSSService
|
|||
.WithHttpClient(new HttpClient(httpClientHandler))
|
||||
.Build();
|
||||
|
||||
var memoryStream = new MemoryStream();
|
||||
var pipe = new System.IO.Pipelines.Pipe();
|
||||
|
||||
var getObjectArgs = new GetObjectArgs()
|
||||
_ = Task.Run(async () =>
|
||||
{
|
||||
try
|
||||
{
|
||||
var args = new GetObjectArgs()
|
||||
.WithBucket(minIOConfig.BucketName)
|
||||
.WithObject(ossRelativePath)
|
||||
.WithCallbackStream(stream => stream.CopyToAsync(memoryStream));
|
||||
.WithCallbackStream( stream =>
|
||||
{
|
||||
stream.CopyTo(pipe.Writer.AsStream());
|
||||
});
|
||||
|
||||
await minioClient.GetObjectAsync(args);
|
||||
await pipe.Writer.CompleteAsync();
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
await pipe.Writer.CompleteAsync(ex);
|
||||
}
|
||||
});
|
||||
|
||||
return pipe.Reader.AsStream();
|
||||
|
||||
#region 废弃
|
||||
|
||||
//var memoryStream = new MemoryStream();
|
||||
|
||||
//var getObjectArgs = new GetObjectArgs()
|
||||
// .WithBucket(minIOConfig.BucketName)
|
||||
// .WithObject(ossRelativePath)
|
||||
// .WithCallbackStream(stream => stream.CopyToAsync(memoryStream));
|
||||
|
||||
//await minioClient.GetObjectAsync(getObjectArgs);
|
||||
//memoryStream.Position = 0;
|
||||
//return memoryStream;
|
||||
|
||||
#endregion
|
||||
|
||||
await minioClient.GetObjectAsync(getObjectArgs);
|
||||
memoryStream.Position = 0;
|
||||
return memoryStream;
|
||||
}
|
||||
else if (ObjectStoreServiceOptions.ObjectStoreUse == "AWS")
|
||||
{
|
||||
|
|
|
|||
|
|
@ -1208,7 +1208,14 @@ namespace IRaCIS.Application.Contracts
|
|||
|
||||
}
|
||||
|
||||
public class DownloadPatientDto
|
||||
{
|
||||
public string PatientName { get; set; }
|
||||
|
||||
public string PatientIdStr { get; set; }
|
||||
|
||||
public List<DownloadDicomStudyDto> StudyList { get; set; }
|
||||
}
|
||||
public class DownloadDicomStudyDto
|
||||
{
|
||||
public Guid StudyId { get; set; }
|
||||
|
|
|
|||
|
|
@ -0,0 +1,14 @@
|
|||
using IRaCIS.Application.Contracts;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
|
||||
namespace IRaCIS.Application.Interfaces
|
||||
{
|
||||
public interface IPatientService
|
||||
{
|
||||
|
||||
public Task<IResponseOutput<List<DownloadPatientDto>>> GetDownloadPatientStudyInfo(PatientImageDownloadCommand inCommand);
|
||||
|
||||
public Task<IResponseOutput> DownloadImageSuccess(Guid trialImageDownloadId);
|
||||
|
||||
}
|
||||
}
|
||||
|
|
@ -75,7 +75,7 @@ namespace IRaCIS.Application.Services
|
|||
IDistributedLockProvider _distributedLockProvider, IMapper _mapper, IUserInfo _userInfo, IWebHostEnvironment _hostEnvironment, IStringLocalizer _localizer, IFusionCache _fusionCache
|
||||
|
||||
|
||||
) : BaseService
|
||||
) : BaseService, IPatientService
|
||||
{
|
||||
#region 访视提交生成任务了,但是需要退回
|
||||
|
||||
|
|
@ -3301,7 +3301,7 @@ namespace IRaCIS.Application.Services
|
|||
/// <param name="inCommand"></param>
|
||||
/// <returns></returns>
|
||||
[HttpPost]
|
||||
public async Task<IResponseOutput> GetDownloadPatientStudyInfo(PatientImageDownloadCommand inCommand)
|
||||
public async Task<IResponseOutput<List<DownloadPatientDto>>> GetDownloadPatientStudyInfo(PatientImageDownloadCommand inCommand)
|
||||
{
|
||||
var isAdminOrOA = _userInfo.UserTypeEnumInt == (int)UserTypeEnum.Admin || _userInfo.UserTypeEnumInt == (int)UserTypeEnum.OA || _userInfo.UserTypeEnumInt == (int)UserTypeEnum.SuperAdmin;
|
||||
|
||||
|
|
@ -3384,10 +3384,10 @@ namespace IRaCIS.Application.Services
|
|||
#endregion
|
||||
|
||||
|
||||
var query = _patientRepository.Where(t => patientIdList.Contains(t.Id)).Select(t => new
|
||||
var query = _patientRepository.Where(t => patientIdList.Contains(t.Id)).Select(t => new DownloadPatientDto()
|
||||
{
|
||||
t.PatientName,
|
||||
t.PatientIdStr,
|
||||
PatientName= t.PatientName,
|
||||
PatientIdStr= t.PatientIdStr,
|
||||
|
||||
StudyList = t.SCPStudyList.Where(t => studyIdList.Count > 0 ? studyIdList.Contains(t.Id) : true)
|
||||
.Where(t => isAdminOrOA ? true : t.HospitalGroupList.Any(c => currentUserHospitalGroupIdList.Contains(c.HospitalGroupId)))
|
||||
|
|
@ -3414,7 +3414,7 @@ namespace IRaCIS.Application.Services
|
|||
FileSize = k.FileSize
|
||||
}).ToList()
|
||||
}).ToList()
|
||||
})
|
||||
}).ToList()
|
||||
});
|
||||
|
||||
var patientList = await query.ToListAsync();
|
||||
|
|
@ -3610,7 +3610,7 @@ namespace IRaCIS.Application.Services
|
|||
[HttpGet]
|
||||
public async Task<IResponseOutput> DownloadImageSuccess(Guid trialImageDownloadId)
|
||||
{
|
||||
await _subejctVisitDownloadRepository.UpdatePartialFromQueryAsync(t => t.Id == trialImageDownloadId, u => new SubejctVisitDownload()
|
||||
await _subejctVisitDownloadRepository.UpdatePartialFromQueryAsync( trialImageDownloadId, u => new SubejctVisitDownload()
|
||||
{ DownloadEndTime = DateTime.Now, IsSuccess = true }, true);
|
||||
return ResponseOutput.Ok();
|
||||
}
|
||||
|
|
|
|||
|
|
@ -33,6 +33,8 @@
|
|||
/// 返回数据
|
||||
/// </summary>
|
||||
T Data { get; set; }
|
||||
|
||||
object OtherData { get; set; }
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in New Issue