From 131064789216ae01bddf6db2a0fc1913fb79f332 Mon Sep 17 00:00:00 2001 From: "P. Gritsenko" Date: Fri, 19 Apr 2024 17:37:57 +0300 Subject: [PATCH 1/3] DownloadFileWithouBufferingAsync method added to avoid buffering of the recording file stream --- Source/ZoomNet/Resources/CloudRecordings.cs | 30 ++++++++++++++------ Source/ZoomNet/Resources/ICloudRecordings.cs | 14 ++++++++- 2 files changed, 35 insertions(+), 9 deletions(-) diff --git a/Source/ZoomNet/Resources/CloudRecordings.cs b/Source/ZoomNet/Resources/CloudRecordings.cs index 5fbe2e5e..ea2ef276 100644 --- a/Source/ZoomNet/Resources/CloudRecordings.cs +++ b/Source/ZoomNet/Resources/CloudRecordings.cs @@ -4,10 +4,12 @@ using System.IO; using System.Linq; using System.Net.Http; +using System.Net.Http.Headers; using System.Text.Json.Nodes; using System.Threading; using System.Threading.Tasks; using ZoomNet.Models; +using ZoomNet.Utilities; namespace ZoomNet.Resources { @@ -356,14 +358,7 @@ public Task RejectRegistrantsAsync(long meetingId, IEnumerable registran return UpdateRegistrantsStatusAsync(meetingId, registrantIds, "deny", cancellationToken); } - /// - /// Download the recording file. - /// - /// The URL of the recording file to download. - /// Cancellation token. - /// - /// The containing the file. - /// + /// public Task DownloadFileAsync(string downloadUrl, CancellationToken cancellationToken = default) { return _client @@ -372,6 +367,25 @@ public Task DownloadFileAsync(string downloadUrl, CancellationToken canc .AsStream(); } + /// + public async Task DownloadFileWithouBufferingAsync(string downloadUrl) + { + using (var request = new HttpRequestMessage(HttpMethod.Get, downloadUrl)) + { + var tokenHandler = _client.Filters.OfType().SingleOrDefault(); + if (tokenHandler != null) + { + request.Headers.Authorization = new AuthenticationHeaderValue("Bearer", tokenHandler.Token); + } + + var response = await _client.BaseClient.SendAsync(request, HttpCompletionOption.ResponseHeadersRead).ConfigureAwait(false); + + response.EnsureSuccessStatusCode(); + + return await response.Content.ReadAsStreamAsync(); + } + } + private Task UpdateRegistrantsStatusAsync(long meetingId, IEnumerable registrantIds, string status, CancellationToken cancellationToken = default) { var data = new JsonObject diff --git a/Source/ZoomNet/Resources/ICloudRecordings.cs b/Source/ZoomNet/Resources/ICloudRecordings.cs index 1c4f7fe8..99ea057d 100644 --- a/Source/ZoomNet/Resources/ICloudRecordings.cs +++ b/Source/ZoomNet/Resources/ICloudRecordings.cs @@ -205,13 +205,25 @@ public interface ICloudRecordings Task RejectRegistrantsAsync(long meetingId, IEnumerable registrantIds, CancellationToken cancellationToken = default); /// - /// Download the recording file. + /// Download the recording file into the memory buffer and return the stream from this buffer. /// /// The URL of the recording file to download. /// Cancellation token. /// /// The containing the file. /// + /// + /// Be careful, because if the recording file is too large, it can lead to buffer overflow and an OutOfMemoryException throwing. + /// Task DownloadFileAsync(string downloadUrl, CancellationToken cancellationToken = default); + + /// + /// Download the recording file without buffering. + /// + /// The URL of the recording file to download. + /// + /// The containing the file. + /// + Task DownloadFileWithouBufferingAsync(string downloadUrl); } } From 6b3f0b06810a0f00d64673bc3a8d820d6a0a018f Mon Sep 17 00:00:00 2001 From: "P. Gritsenko" Date: Fri, 19 Apr 2024 17:48:53 +0300 Subject: [PATCH 2/3] cancellation token added --- Source/ZoomNet/Resources/CloudRecordings.cs | 4 ++-- Source/ZoomNet/Resources/ICloudRecordings.cs | 3 ++- 2 files changed, 4 insertions(+), 3 deletions(-) diff --git a/Source/ZoomNet/Resources/CloudRecordings.cs b/Source/ZoomNet/Resources/CloudRecordings.cs index ea2ef276..74bcc913 100644 --- a/Source/ZoomNet/Resources/CloudRecordings.cs +++ b/Source/ZoomNet/Resources/CloudRecordings.cs @@ -368,7 +368,7 @@ public Task DownloadFileAsync(string downloadUrl, CancellationToken canc } /// - public async Task DownloadFileWithouBufferingAsync(string downloadUrl) + public async Task DownloadFileWithouBufferingAsync(string downloadUrl, CancellationToken cancellationToken) { using (var request = new HttpRequestMessage(HttpMethod.Get, downloadUrl)) { @@ -378,7 +378,7 @@ public async Task DownloadFileWithouBufferingAsync(string downloadUrl) request.Headers.Authorization = new AuthenticationHeaderValue("Bearer", tokenHandler.Token); } - var response = await _client.BaseClient.SendAsync(request, HttpCompletionOption.ResponseHeadersRead).ConfigureAwait(false); + var response = await _client.BaseClient.SendAsync(request, HttpCompletionOption.ResponseHeadersRead, cancellationToken).ConfigureAwait(false); response.EnsureSuccessStatusCode(); diff --git a/Source/ZoomNet/Resources/ICloudRecordings.cs b/Source/ZoomNet/Resources/ICloudRecordings.cs index 99ea057d..fcc8e3fe 100644 --- a/Source/ZoomNet/Resources/ICloudRecordings.cs +++ b/Source/ZoomNet/Resources/ICloudRecordings.cs @@ -221,9 +221,10 @@ public interface ICloudRecordings /// Download the recording file without buffering. /// /// The URL of the recording file to download. + /// Cancellation token. /// /// The containing the file. /// - Task DownloadFileWithouBufferingAsync(string downloadUrl); + Task DownloadFileWithouBufferingAsync(string downloadUrl, CancellationToken cancellationToken = default); } } From ac43839dadb189d9d1cfb9433eda2a94cc581baf Mon Sep 17 00:00:00 2001 From: "P. Gritsenko" Date: Mon, 22 Apr 2024 09:15:19 +0300 Subject: [PATCH 3/3] Apply PR comments --- Source/ZoomNet/Resources/CloudRecordings.cs | 24 +++++++++++++++----- Source/ZoomNet/Resources/ICloudRecordings.cs | 15 +----------- 2 files changed, 19 insertions(+), 20 deletions(-) diff --git a/Source/ZoomNet/Resources/CloudRecordings.cs b/Source/ZoomNet/Resources/CloudRecordings.cs index 74bcc913..66204e7e 100644 --- a/Source/ZoomNet/Resources/CloudRecordings.cs +++ b/Source/ZoomNet/Resources/CloudRecordings.cs @@ -359,17 +359,29 @@ public Task RejectRegistrantsAsync(long meetingId, IEnumerable registran } /// - public Task DownloadFileAsync(string downloadUrl, CancellationToken cancellationToken = default) + public async Task DownloadFileAsync(string downloadUrl, CancellationToken cancellationToken = default) { - return _client + /* + * PLEASE NOTE: + * + * The HttpRequestMessage in this method is dispatched with its completion option set to "ResponseHeadersRead". + * This ensures the content of the response is streamed rather than buffered in memory. + * This is important in cases where the downloaded file is quite large. + * In this scenario, we don't want the entirety of the file to be buffered in a MemoryStream because + * it could lead to "out of memory" exceptions if the file is large enough. + * See https://github.com/Jericho/ZoomNet/pull/342 for a discussion on this topic. + * + * Forthermore, as of this writing, the FluentHttp library does not allow us to stream the content of responses + * which means that the code in this method cannot be simplified like so: + return _client .GetAsync(downloadUrl) .WithCancellationToken(cancellationToken) .AsStream(); - } + * + * The downside of not using the FluentHttp library to dispatch the request is that we lose automatic retries, + * error handling, logging, etc. + */ - /// - public async Task DownloadFileWithouBufferingAsync(string downloadUrl, CancellationToken cancellationToken) - { using (var request = new HttpRequestMessage(HttpMethod.Get, downloadUrl)) { var tokenHandler = _client.Filters.OfType().SingleOrDefault(); diff --git a/Source/ZoomNet/Resources/ICloudRecordings.cs b/Source/ZoomNet/Resources/ICloudRecordings.cs index fcc8e3fe..1c4f7fe8 100644 --- a/Source/ZoomNet/Resources/ICloudRecordings.cs +++ b/Source/ZoomNet/Resources/ICloudRecordings.cs @@ -205,26 +205,13 @@ public interface ICloudRecordings Task RejectRegistrantsAsync(long meetingId, IEnumerable registrantIds, CancellationToken cancellationToken = default); /// - /// Download the recording file into the memory buffer and return the stream from this buffer. + /// Download the recording file. /// /// The URL of the recording file to download. /// Cancellation token. /// /// The containing the file. /// - /// - /// Be careful, because if the recording file is too large, it can lead to buffer overflow and an OutOfMemoryException throwing. - /// Task DownloadFileAsync(string downloadUrl, CancellationToken cancellationToken = default); - - /// - /// Download the recording file without buffering. - /// - /// The URL of the recording file to download. - /// Cancellation token. - /// - /// The containing the file. - /// - Task DownloadFileWithouBufferingAsync(string downloadUrl, CancellationToken cancellationToken = default); } }