From 58ba1a15394b22fab7af3d15c1aea48e90041196 Mon Sep 17 00:00:00 2001 From: Oluwatobi Awe Date: Mon, 10 Feb 2025 14:19:15 +0000 Subject: [PATCH 1/2] TD-5348 stream files with keep alive. --- .../Controllers/Api/ResourceController.cs | 41 ++++++++++++++++++- LearningHub.Nhs.WebUI/Services/FileService.cs | 35 ++++++++++++---- 2 files changed, 65 insertions(+), 11 deletions(-) diff --git a/LearningHub.Nhs.WebUI/Controllers/Api/ResourceController.cs b/LearningHub.Nhs.WebUI/Controllers/Api/ResourceController.cs index 0616474ff..60e816e1a 100644 --- a/LearningHub.Nhs.WebUI/Controllers/Api/ResourceController.cs +++ b/LearningHub.Nhs.WebUI/Controllers/Api/ResourceController.cs @@ -2,6 +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; @@ -9,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; @@ -70,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 { @@ -105,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 { @@ -584,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 4e02db124..fefd834a7 100644 --- a/LearningHub.Nhs.WebUI/Services/FileService.cs +++ b/LearningHub.Nhs.WebUI/Services/FileService.cs @@ -135,20 +135,37 @@ public async Task DownloadFileAsync(string filePath, stri { var file = directory.GetFileClient(fileName); - if (await file.ExistsAsync()) + var properties = await file.GetPropertiesAsync(); + long fileSize = properties.Value.ContentLength; + + try + { + if (fileSize <= 900 * 1024 * 1024) { - return await file.DownloadAsync(); + // For smaller files, download the entire file as a stream. + var response = await file.DownloadAsync(); + return new FileDownloadResponse + { + Content = response.Value.Content, + ContentType = properties.Value.ContentType, + ContentLength = fileSize, + }; } - } - else if (await sourceDirectory.ExistsAsync()) - { - var file = sourceDirectory.GetFileClient(fileName); - - if (await file.ExistsAsync()) + else { - return await file.DownloadAsync(); + // For large files, open a read stream + return new FileDownloadResponse + { + Content = await file.OpenReadAsync(), + ContentType = properties.Value.ContentType, + ContentLength = fileSize, + }; } } + catch (Exception ex) + { + throw new Exception($"Error downloading file: {ex.Message}"); + } return null; } From 8daf5830b5d0ec9bd853f2d664d145d0f9b0d98d Mon Sep 17 00:00:00 2001 From: Oluwatobi Awe Date: Tue, 11 Feb 2025 09:21:56 +0000 Subject: [PATCH 2/2] TD-5348 uthorization for the purpose of testing the infastructure --- LearningHub.Nhs.WebUI/Controllers/Api/ResourceController.cs | 1 + 1 file changed, 1 insertion(+) diff --git a/LearningHub.Nhs.WebUI/Controllers/Api/ResourceController.cs b/LearningHub.Nhs.WebUI/Controllers/Api/ResourceController.cs index 60e816e1a..9eddcc484 100644 --- a/LearningHub.Nhs.WebUI/Controllers/Api/ResourceController.cs +++ b/LearningHub.Nhs.WebUI/Controllers/Api/ResourceController.cs @@ -65,6 +65,7 @@ public async Task AcceptSensitiveContentAsync(int resourceVersionI /// File name. /// A representing the result of the asynchronous operation. [HttpGet("DownloadResource")] + [AllowAnonymous] public async Task DownloadResource(string filePath, string fileName) { if (string.IsNullOrEmpty(fileName))