Display download progress in human readable format for Invoke-WebRequest (#14611)

This commit is contained in:
Christoph Bergmeister
2022-09-19 11:16:21 -07:00
committed by GitHub
parent 1847e86b9a
commit d3d81c3e44
7 changed files with 78 additions and 21 deletions
@@ -458,7 +458,7 @@ namespace Microsoft.PowerShell.Commands
}
else if (ShouldSaveToOutFile)
{
StreamHelper.SaveStreamToFile(baseResponseStream, QualifiedOutFile, this, _cancelToken.Token);
StreamHelper.SaveStreamToFile(baseResponseStream, QualifiedOutFile, this, response.Content.Headers.ContentLength.GetValueOrDefault(), _cancelToken.Token);
}
if (!string.IsNullOrEmpty(StatusCodeVariable))
@@ -204,7 +204,7 @@ namespace Microsoft.PowerShell.Commands
}
int initialCapacity = (int)Math.Min(contentLength, StreamHelper.DefaultReadBuffer);
_rawContentStream = new WebResponseContentMemoryStream(st, initialCapacity, null);
_rawContentStream = new WebResponseContentMemoryStream(st, initialCapacity, cmdlet: null, response.Content.Headers.ContentLength.GetValueOrDefault());
}
// set the position of the content stream to the beginning
_rawContentStream.Position = 0;
@@ -38,7 +38,11 @@ namespace Microsoft.PowerShell.Commands
if (ShouldWriteToPipeline)
{
// creating a MemoryStream wrapper to response stream here to support IsStopping.
responseStream = new WebResponseContentMemoryStream(responseStream, StreamHelper.ChunkSize, this);
responseStream = new WebResponseContentMemoryStream(
responseStream,
StreamHelper.ChunkSize,
this,
response.Content.Headers.ContentLength.GetValueOrDefault());
WebResponseObject ro = WebResponseObjectFactory.GetResponseObject(response, responseStream, this.Context);
ro.RelationLink = _relationLink;
WriteObject(ro);
@@ -52,7 +56,7 @@ namespace Microsoft.PowerShell.Commands
if (ShouldSaveToOutFile)
{
StreamHelper.SaveStreamToFile(responseStream, QualifiedOutFile, this, _cancelToken.Token);
StreamHelper.SaveStreamToFile(responseStream, QualifiedOutFile, this, response.Content.Headers.ContentLength.GetValueOrDefault(), _cancelToken.Token);
}
}
@@ -24,6 +24,7 @@ namespace Microsoft.PowerShell.Commands
{
#region Data
private readonly long? _contentLength;
private readonly Stream _originalStreamToProxy;
private bool _isInitialized = false;
private readonly Cmdlet _ownerCmdlet;
@@ -37,9 +38,11 @@ namespace Microsoft.PowerShell.Commands
/// <param name="stream"></param>
/// <param name="initialCapacity"></param>
/// <param name="cmdlet">Owner cmdlet if any.</param>
internal WebResponseContentMemoryStream(Stream stream, int initialCapacity, Cmdlet cmdlet)
/// <param name="contentLength">Expected download size in Bytes.</param>
internal WebResponseContentMemoryStream(Stream stream, int initialCapacity, Cmdlet cmdlet, long? contentLength)
: base(initialCapacity)
{
this._contentLength = contentLength;
_originalStreamToProxy = stream;
_ownerCmdlet = cmdlet;
}
@@ -218,14 +221,24 @@ namespace Microsoft.PowerShell.Commands
_isInitialized = true;
try
{
long totalLength = 0;
long totalRead = 0;
byte[] buffer = new byte[StreamHelper.ChunkSize];
ProgressRecord record = new(StreamHelper.ActivityId, WebCmdletStrings.ReadResponseProgressActivity, "statusDescriptionPlaceholder");
for (int read = 1; read > 0; totalLength += read)
string totalDownloadSize = _contentLength is null ? "???" : Utils.DisplayHumanReadableFileSize((long)_contentLength);
for (int read = 1; read > 0; totalRead += read)
{
if (_ownerCmdlet != null)
{
record.StatusDescription = StringUtil.Format(WebCmdletStrings.ReadResponseProgressStatus, totalLength);
record.StatusDescription = StringUtil.Format(
WebCmdletStrings.ReadResponseProgressStatus,
Utils.DisplayHumanReadableFileSize(totalRead),
totalDownloadSize);
if (_contentLength > 0)
{
record.PercentComplete = Math.Min((int)(totalRead * 100 / (long)_contentLength), 100);
}
_ownerCmdlet.WriteProgress(record);
if (_ownerCmdlet.IsStopping)
@@ -244,13 +257,13 @@ namespace Microsoft.PowerShell.Commands
if (_ownerCmdlet != null)
{
record.StatusDescription = StringUtil.Format(WebCmdletStrings.ReadResponseComplete, totalLength);
record.StatusDescription = StringUtil.Format(WebCmdletStrings.ReadResponseComplete, totalRead);
record.RecordType = ProgressRecordType.Completed;
_ownerCmdlet.WriteProgress(record);
}
// make sure the length is set appropriately
base.SetLength(totalLength);
base.SetLength(totalRead);
base.Seek(0, SeekOrigin.Begin);
}
catch (Exception)
@@ -276,7 +289,7 @@ namespace Microsoft.PowerShell.Commands
#region Static Methods
internal static void WriteToStream(Stream input, Stream output, PSCmdlet cmdlet, CancellationToken cancellationToken)
internal static void WriteToStream(Stream input, Stream output, PSCmdlet cmdlet, long? contentLength, CancellationToken cancellationToken)
{
if (cmdlet == null)
{
@@ -289,12 +302,22 @@ namespace Microsoft.PowerShell.Commands
ActivityId,
WebCmdletStrings.WriteRequestProgressActivity,
WebCmdletStrings.WriteRequestProgressStatus);
string totalDownloadSize = contentLength is null ? "???" : Utils.DisplayHumanReadableFileSize((long)contentLength);
try
{
while (!copyTask.Wait(1000, cancellationToken))
{
record.StatusDescription = StringUtil.Format(WebCmdletStrings.WriteRequestProgressStatus, output.Position);
record.StatusDescription = StringUtil.Format(
WebCmdletStrings.WriteRequestProgressStatus,
Utils.DisplayHumanReadableFileSize(output.Position),
totalDownloadSize);
if (contentLength != null && contentLength > 0)
{
record.PercentComplete = Math.Min((int)(output.Position * 100 / (long)contentLength), 100);
}
cmdlet.WriteProgress(record);
}
@@ -316,13 +339,14 @@ namespace Microsoft.PowerShell.Commands
/// <param name="stream">Input stream.</param>
/// <param name="filePath">Output file name.</param>
/// <param name="cmdlet">Current cmdlet (Invoke-WebRequest or Invoke-RestMethod).</param>
/// <param name="contentLength">Expected download size in Bytes.</param>
/// <param name="cancellationToken">CancellationToken to track the cmdlet cancellation.</param>
internal static void SaveStreamToFile(Stream stream, string filePath, PSCmdlet cmdlet, CancellationToken cancellationToken)
internal static void SaveStreamToFile(Stream stream, string filePath, PSCmdlet cmdlet, long? contentLength, CancellationToken cancellationToken)
{
// If the web cmdlet should resume, append the file instead of overwriting.
FileMode fileMode = cmdlet is WebRequestPSCmdlet webCmdlet && webCmdlet.ShouldResume ? FileMode.Append : FileMode.Create;
using FileStream output = new(filePath, fileMode, FileAccess.Write, FileShare.Read);
WriteToStream(stream, output, cmdlet, cancellationToken);
WriteToStream(stream, output, cmdlet, contentLength, cancellationToken);
}
private static string StreamToString(Stream stream, Encoding encoding)
@@ -199,13 +199,13 @@
<value>The cmdlet cannot run because the following parameter is missing: Proxy. Provide a valid proxy URI for the Proxy parameter when using the ProxyCredential or ProxyUseDefaultCredentials parameters, then retry.</value>
</data>
<data name="ReadResponseComplete" xml:space="preserve">
<value>Reading web response completed. (Number of bytes read: {0})</value>
<value>Reading web response stream completed. Downloaded: {0}</value>
</data>
<data name="ReadResponseProgressActivity" xml:space="preserve">
<value>Reading web response</value>
<value>Reading web response stream</value>
</data>
<data name="ReadResponseProgressStatus" xml:space="preserve">
<value>Reading response stream... (Number of bytes read: {0})</value>
<value>Downloaded: {0} of {1}</value>
</data>
<data name="RequestTimeout" xml:space="preserve">
<value>The operation has timed out.</value>
@@ -223,7 +223,7 @@
<value>Web request status</value>
</data>
<data name="WriteRequestProgressStatus" xml:space="preserve">
<value>Number of bytes processed: {0}</value>
<value>Downloaded: {0} of {1}</value>
</data>
<data name="JsonNetModuleRequired" xml:space="preserve">
<value>The ConvertTo-Json and ConvertFrom-Json cmdlets require the 'Json.Net' module. {0}</value>
@@ -5,8 +5,6 @@ using System.Collections;
using System.Collections.Concurrent;
using System.Collections.Generic;
using System.Collections.ObjectModel;
using System.ComponentModel;
using System.Diagnostics;
using System.Diagnostics.CodeAnalysis;
using System.Globalization;
using System.IO;
@@ -17,7 +15,6 @@ using System.Management.Automation.Remoting;
using System.Management.Automation.Security;
using System.Numerics;
using System.Reflection;
using System.Runtime.CompilerServices;
using System.Runtime.InteropServices;
using System.Security;
#if !UNIX
@@ -1547,6 +1544,21 @@ namespace System.Management.Automation
return oldMode;
}
internal static string DisplayHumanReadableFileSize(long bytes)
{
return bytes switch
{
< 1024 and >= 0 => $"{bytes} Bytes",
< 1048576 and >= 1024 => $"{(bytes / 1024.0).ToString("0.0")} KB",
< 1073741824 and >= 1048576 => $"{(bytes / 1048576.0).ToString("0.0")} MB",
< 1099511627776 and >= 1073741824 => $"{(bytes / 1073741824.0).ToString("0.000")} GB",
< 1125899906842624 and >= 1099511627776 => $"{(bytes / 1099511627776.0).ToString("0.00000")} TB",
< 1152921504606847000 and >= 1125899906842624 => $"{(bytes / 1125899906842624.0).ToString("0.0000000")} PB",
>= 1152921504606847000 => $"{(bytes / 1152921504606847000.0).ToString("0.000000000")} EB",
_ => $"0 Bytes",
};
}
}
}
+17
View File
@@ -22,6 +22,23 @@ namespace PSTests.Parallel
Assert.False(Utils.IsWinPEHost());
}
[Theory]
[InlineData(long.MinValue, "0 Bytes")]
[InlineData(-1, "0 Bytes")]
[InlineData(0, "0 Bytes")]
[InlineData(1, "1 Bytes")]
[InlineData(1024, "1.0 KB")]
[InlineData(3000, "2.9 KB")]
[InlineData(1024 * 1024, "1.0 MB")]
[InlineData(1024 * 1024 * 1024, "1.000 GB")]
[InlineData((long)(1024 * 1024 * 1024) * 1024, "1.00000 TB")]
[InlineData((long)(1024 * 1024 * 1024) * 1024 * 1024, "1.0000000 PB")]
[InlineData(long.MaxValue, "8.000000000 EB")]
public static void DisplayHumanReadableFileSize(long bytes, string expected)
{
Assert.Equal(expected, Utils.DisplayHumanReadableFileSize(bytes));
}
[Fact]
public static void TestHistoryStack()
{