diff --git a/src/Microsoft.PowerShell.Commands.Utility/commands/utility/WebCmdlet/Common/InvokeRestMethodCommand.Common.cs b/src/Microsoft.PowerShell.Commands.Utility/commands/utility/WebCmdlet/Common/InvokeRestMethodCommand.Common.cs
index 8c1ec228bb4..0970ae46e8f 100644
--- a/src/Microsoft.PowerShell.Commands.Utility/commands/utility/WebCmdlet/Common/InvokeRestMethodCommand.Common.cs
+++ b/src/Microsoft.PowerShell.Commands.Utility/commands/utility/WebCmdlet/Common/InvokeRestMethodCommand.Common.cs
@@ -458,7 +458,7 @@ internal override void ProcessResponse(HttpResponseMessage response)
}
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))
diff --git a/src/Microsoft.PowerShell.Commands.Utility/commands/utility/WebCmdlet/Common/WebResponseObject.Common.cs b/src/Microsoft.PowerShell.Commands.Utility/commands/utility/WebCmdlet/Common/WebResponseObject.Common.cs
index 1d80bcfdb2b..6f9c14638f9 100644
--- a/src/Microsoft.PowerShell.Commands.Utility/commands/utility/WebCmdlet/Common/WebResponseObject.Common.cs
+++ b/src/Microsoft.PowerShell.Commands.Utility/commands/utility/WebCmdlet/Common/WebResponseObject.Common.cs
@@ -204,7 +204,7 @@ private void SetResponse(HttpResponseMessage response, Stream contentStream)
}
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;
diff --git a/src/Microsoft.PowerShell.Commands.Utility/commands/utility/WebCmdlet/CoreCLR/InvokeWebRequestCommand.CoreClr.cs b/src/Microsoft.PowerShell.Commands.Utility/commands/utility/WebCmdlet/CoreCLR/InvokeWebRequestCommand.CoreClr.cs
index 1a46d074048..f13ad1aa4a3 100644
--- a/src/Microsoft.PowerShell.Commands.Utility/commands/utility/WebCmdlet/CoreCLR/InvokeWebRequestCommand.CoreClr.cs
+++ b/src/Microsoft.PowerShell.Commands.Utility/commands/utility/WebCmdlet/CoreCLR/InvokeWebRequestCommand.CoreClr.cs
@@ -38,7 +38,11 @@ internal override void ProcessResponse(HttpResponseMessage response)
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 @@ internal override void ProcessResponse(HttpResponseMessage response)
if (ShouldSaveToOutFile)
{
- StreamHelper.SaveStreamToFile(responseStream, QualifiedOutFile, this, _cancelToken.Token);
+ StreamHelper.SaveStreamToFile(responseStream, QualifiedOutFile, this, response.Content.Headers.ContentLength.GetValueOrDefault(), _cancelToken.Token);
}
}
diff --git a/src/Microsoft.PowerShell.Commands.Utility/commands/utility/WebCmdlet/StreamHelper.cs b/src/Microsoft.PowerShell.Commands.Utility/commands/utility/WebCmdlet/StreamHelper.cs
index d298f427960..ad640dcfd41 100644
--- a/src/Microsoft.PowerShell.Commands.Utility/commands/utility/WebCmdlet/StreamHelper.cs
+++ b/src/Microsoft.PowerShell.Commands.Utility/commands/utility/WebCmdlet/StreamHelper.cs
@@ -24,6 +24,7 @@ internal class WebResponseContentMemoryStream : MemoryStream
{
#region Data
+ private readonly long? _contentLength;
private readonly Stream _originalStreamToProxy;
private bool _isInitialized = false;
private readonly Cmdlet _ownerCmdlet;
@@ -37,9 +38,11 @@ internal class WebResponseContentMemoryStream : MemoryStream
///
///
/// Owner cmdlet if any.
- internal WebResponseContentMemoryStream(Stream stream, int initialCapacity, Cmdlet cmdlet)
+ /// Expected download size in Bytes.
+ internal WebResponseContentMemoryStream(Stream stream, int initialCapacity, Cmdlet cmdlet, long? contentLength)
: base(initialCapacity)
{
+ this._contentLength = contentLength;
_originalStreamToProxy = stream;
_ownerCmdlet = cmdlet;
}
@@ -218,14 +221,24 @@ private void Initialize()
_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 @@ private void Initialize()
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 @@ internal static class StreamHelper
#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 @@ internal static void WriteToStream(Stream input, Stream output, PSCmdlet cmdlet,
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 @@ internal static void WriteToStream(Stream input, Stream output, PSCmdlet cmdlet,
/// Input stream.
/// Output file name.
/// Current cmdlet (Invoke-WebRequest or Invoke-RestMethod).
+ /// Expected download size in Bytes.
/// CancellationToken to track the cmdlet cancellation.
- 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)
diff --git a/src/Microsoft.PowerShell.Commands.Utility/resources/WebCmdletStrings.resx b/src/Microsoft.PowerShell.Commands.Utility/resources/WebCmdletStrings.resx
index 976ec531e25..f0ec8a4b02b 100644
--- a/src/Microsoft.PowerShell.Commands.Utility/resources/WebCmdletStrings.resx
+++ b/src/Microsoft.PowerShell.Commands.Utility/resources/WebCmdletStrings.resx
@@ -199,13 +199,13 @@
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.
- Reading web response completed. (Number of bytes read: {0})
+ Reading web response stream completed. Downloaded: {0}
- Reading web response
+ Reading web response stream
- Reading response stream... (Number of bytes read: {0})
+ Downloaded: {0} of {1}
The operation has timed out.
@@ -223,7 +223,7 @@
Web request status
- Number of bytes processed: {0}
+ Downloaded: {0} of {1}
The ConvertTo-Json and ConvertFrom-Json cmdlets require the 'Json.Net' module. {0}
diff --git a/src/System.Management.Automation/engine/Utils.cs b/src/System.Management.Automation/engine/Utils.cs
index 661bc1e70cf..bc28b5a5d50 100644
--- a/src/System.Management.Automation/engine/Utils.cs
+++ b/src/System.Management.Automation/engine/Utils.cs
@@ -5,8 +5,6 @@
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.Security;
using System.Numerics;
using System.Reflection;
-using System.Runtime.CompilerServices;
using System.Runtime.InteropServices;
using System.Security;
#if !UNIX
@@ -1547,6 +1544,21 @@ internal static bool IsComObject(object obj)
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",
+ };
+ }
}
}
diff --git a/test/xUnit/csharp/test_Utils.cs b/test/xUnit/csharp/test_Utils.cs
index ea45ce533d6..bf562f3a8a4 100644
--- a/test/xUnit/csharp/test_Utils.cs
+++ b/test/xUnit/csharp/test_Utils.cs
@@ -22,6 +22,23 @@ public static void TestIsWinPEHost()
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()
{