diff --git a/LearningHub.Nhs.WebUI/Controllers/Api/ResourceController.cs b/LearningHub.Nhs.WebUI/Controllers/Api/ResourceController.cs index e5e8ab35e..c8c9b447b 100644 --- a/LearningHub.Nhs.WebUI/Controllers/Api/ResourceController.cs +++ b/LearningHub.Nhs.WebUI/Controllers/Api/ResourceController.cs @@ -2,7 +2,10 @@ namespace LearningHub.Nhs.WebUI.Controllers.Api { using System; using System.Collections.Generic; + using System.IO; using System.Linq; + using System.Net.Http.Headers; + using System.Threading; using System.Threading.Tasks; using LearningHub.Nhs.Models.Enums; using LearningHub.Nhs.Models.Resource; @@ -10,6 +13,7 @@ namespace LearningHub.Nhs.WebUI.Controllers.Api using LearningHub.Nhs.Models.Resource.Contribute; using LearningHub.Nhs.WebUI.Interfaces; using Microsoft.AspNetCore.Authorization; + using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Mvc; using Microsoft.Extensions.Configuration; @@ -71,7 +75,15 @@ public async Task DownloadResource(string filePath, string fileNa var file = await this.fileService.DownloadFileAsync(filePath, fileName); if (file != null) { - return this.File(file.Content, file.ContentType, fileName); + // Set response headers. + this.Response.ContentType = file.ContentType; + this.Response.ContentLength = file.ContentLength; + var contentDisposition = new ContentDispositionHeaderValue("attachment") { FileNameStar = fileName }; + this.Response.Headers["Content-Disposition"] = contentDisposition.ToString(); + + // Stream the file in chunks with periodic flushes to keep the connection active. + await this.StreamFileWithKeepAliveAsync(file.Content, this.Response.Body, this.HttpContext.RequestAborted); + return this.Ok(); } else { @@ -106,7 +118,16 @@ public async Task DownloadResourceAndRecordActivity(int resourceV ActivityStatus = ActivityStatusEnum.Completed, }; await this.activityService.CreateResourceActivityAsync(activity); - return this.File(file.Content, file.ContentType, fileName); + + // Set response headers. + this.Response.ContentType = file.ContentType; + this.Response.ContentLength = file.ContentLength; + var contentDisposition = new ContentDispositionHeaderValue("attachment") { FileNameStar = fileName }; + this.Response.Headers["Content-Disposition"] = contentDisposition.ToString(); + + // Stream the file in chunks with periodic flushes to keep the connection active. + await this.StreamFileWithKeepAliveAsync(file.Content, this.Response.Body, this.HttpContext.RequestAborted); + return this.Ok(); } else { @@ -585,5 +606,20 @@ public async Task> GetObsoleteResourceFile(int resourceVersionId, b var result = await this.resourceService.GetObsoleteResourceFile(resourceVersionId, deletedResource); return result; } + + /// + /// Reads from the source stream in chunks and writes to the destination stream, + /// flushing after each chunk to help keep the connection active. + /// + private async Task StreamFileWithKeepAliveAsync(Stream source, Stream destination, CancellationToken cancellationToken) + { + byte[] buffer = new byte[8192]; + int bytesRead; + while ((bytesRead = await source.ReadAsync(buffer, 0, buffer.Length, cancellationToken)) > 0) + { + await destination.WriteAsync(buffer, 0, bytesRead, cancellationToken); + await destination.FlushAsync(cancellationToken); + } + } } } diff --git a/LearningHub.Nhs.WebUI/Services/FileService.cs b/LearningHub.Nhs.WebUI/Services/FileService.cs index e0d19d7f2..1e01a0aaf 100644 --- a/LearningHub.Nhs.WebUI/Services/FileService.cs +++ b/LearningHub.Nhs.WebUI/Services/FileService.cs @@ -147,7 +147,7 @@ public async Task DownloadFileAsync(string filePath, strin { if (fileSize <= 900 * 1024 * 1024) { - // Directly download the entire file as a stream + // For smaller files, download the entire file as a stream. var response = await file.DownloadAsync(); return new FileDownloadResponse { @@ -158,6 +158,7 @@ public async Task DownloadFileAsync(string filePath, strin } else { + // For large files, open a read stream return new FileDownloadResponse { Content = await file.OpenReadAsync(),