Skip to content

Commit d3d81c3

Browse files
authored
Display download progress in human readable format for Invoke-WebRequest (PowerShell#14611)
1 parent 1847e86 commit d3d81c3

File tree

7 files changed

+78
-21
lines changed

7 files changed

+78
-21
lines changed

src/Microsoft.PowerShell.Commands.Utility/commands/utility/WebCmdlet/Common/InvokeRestMethodCommand.Common.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -458,7 +458,7 @@ internal override void ProcessResponse(HttpResponseMessage response)
458458
}
459459
else if (ShouldSaveToOutFile)
460460
{
461-
StreamHelper.SaveStreamToFile(baseResponseStream, QualifiedOutFile, this, _cancelToken.Token);
461+
StreamHelper.SaveStreamToFile(baseResponseStream, QualifiedOutFile, this, response.Content.Headers.ContentLength.GetValueOrDefault(), _cancelToken.Token);
462462
}
463463

464464
if (!string.IsNullOrEmpty(StatusCodeVariable))

src/Microsoft.PowerShell.Commands.Utility/commands/utility/WebCmdlet/Common/WebResponseObject.Common.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -204,7 +204,7 @@ private void SetResponse(HttpResponseMessage response, Stream contentStream)
204204
}
205205

206206
int initialCapacity = (int)Math.Min(contentLength, StreamHelper.DefaultReadBuffer);
207-
_rawContentStream = new WebResponseContentMemoryStream(st, initialCapacity, null);
207+
_rawContentStream = new WebResponseContentMemoryStream(st, initialCapacity, cmdlet: null, response.Content.Headers.ContentLength.GetValueOrDefault());
208208
}
209209
// set the position of the content stream to the beginning
210210
_rawContentStream.Position = 0;

src/Microsoft.PowerShell.Commands.Utility/commands/utility/WebCmdlet/CoreCLR/InvokeWebRequestCommand.CoreClr.cs

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -38,7 +38,11 @@ internal override void ProcessResponse(HttpResponseMessage response)
3838
if (ShouldWriteToPipeline)
3939
{
4040
// creating a MemoryStream wrapper to response stream here to support IsStopping.
41-
responseStream = new WebResponseContentMemoryStream(responseStream, StreamHelper.ChunkSize, this);
41+
responseStream = new WebResponseContentMemoryStream(
42+
responseStream,
43+
StreamHelper.ChunkSize,
44+
this,
45+
response.Content.Headers.ContentLength.GetValueOrDefault());
4246
WebResponseObject ro = WebResponseObjectFactory.GetResponseObject(response, responseStream, this.Context);
4347
ro.RelationLink = _relationLink;
4448
WriteObject(ro);
@@ -52,7 +56,7 @@ internal override void ProcessResponse(HttpResponseMessage response)
5256

5357
if (ShouldSaveToOutFile)
5458
{
55-
StreamHelper.SaveStreamToFile(responseStream, QualifiedOutFile, this, _cancelToken.Token);
59+
StreamHelper.SaveStreamToFile(responseStream, QualifiedOutFile, this, response.Content.Headers.ContentLength.GetValueOrDefault(), _cancelToken.Token);
5660
}
5761
}
5862

src/Microsoft.PowerShell.Commands.Utility/commands/utility/WebCmdlet/StreamHelper.cs

Lines changed: 34 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@ internal class WebResponseContentMemoryStream : MemoryStream
2424
{
2525
#region Data
2626

27+
private readonly long? _contentLength;
2728
private readonly Stream _originalStreamToProxy;
2829
private bool _isInitialized = false;
2930
private readonly Cmdlet _ownerCmdlet;
@@ -37,9 +38,11 @@ internal class WebResponseContentMemoryStream : MemoryStream
3738
/// <param name="stream"></param>
3839
/// <param name="initialCapacity"></param>
3940
/// <param name="cmdlet">Owner cmdlet if any.</param>
40-
internal WebResponseContentMemoryStream(Stream stream, int initialCapacity, Cmdlet cmdlet)
41+
/// <param name="contentLength">Expected download size in Bytes.</param>
42+
internal WebResponseContentMemoryStream(Stream stream, int initialCapacity, Cmdlet cmdlet, long? contentLength)
4143
: base(initialCapacity)
4244
{
45+
this._contentLength = contentLength;
4346
_originalStreamToProxy = stream;
4447
_ownerCmdlet = cmdlet;
4548
}
@@ -218,14 +221,24 @@ private void Initialize()
218221
_isInitialized = true;
219222
try
220223
{
221-
long totalLength = 0;
224+
long totalRead = 0;
222225
byte[] buffer = new byte[StreamHelper.ChunkSize];
223226
ProgressRecord record = new(StreamHelper.ActivityId, WebCmdletStrings.ReadResponseProgressActivity, "statusDescriptionPlaceholder");
224-
for (int read = 1; read > 0; totalLength += read)
227+
string totalDownloadSize = _contentLength is null ? "???" : Utils.DisplayHumanReadableFileSize((long)_contentLength);
228+
for (int read = 1; read > 0; totalRead += read)
225229
{
226230
if (_ownerCmdlet != null)
227231
{
228-
record.StatusDescription = StringUtil.Format(WebCmdletStrings.ReadResponseProgressStatus, totalLength);
232+
record.StatusDescription = StringUtil.Format(
233+
WebCmdletStrings.ReadResponseProgressStatus,
234+
Utils.DisplayHumanReadableFileSize(totalRead),
235+
totalDownloadSize);
236+
237+
if (_contentLength > 0)
238+
{
239+
record.PercentComplete = Math.Min((int)(totalRead * 100 / (long)_contentLength), 100);
240+
}
241+
229242
_ownerCmdlet.WriteProgress(record);
230243

231244
if (_ownerCmdlet.IsStopping)
@@ -244,13 +257,13 @@ private void Initialize()
244257

245258
if (_ownerCmdlet != null)
246259
{
247-
record.StatusDescription = StringUtil.Format(WebCmdletStrings.ReadResponseComplete, totalLength);
260+
record.StatusDescription = StringUtil.Format(WebCmdletStrings.ReadResponseComplete, totalRead);
248261
record.RecordType = ProgressRecordType.Completed;
249262
_ownerCmdlet.WriteProgress(record);
250263
}
251264

252265
// make sure the length is set appropriately
253-
base.SetLength(totalLength);
266+
base.SetLength(totalRead);
254267
base.Seek(0, SeekOrigin.Begin);
255268
}
256269
catch (Exception)
@@ -276,7 +289,7 @@ internal static class StreamHelper
276289

277290
#region Static Methods
278291

279-
internal static void WriteToStream(Stream input, Stream output, PSCmdlet cmdlet, CancellationToken cancellationToken)
292+
internal static void WriteToStream(Stream input, Stream output, PSCmdlet cmdlet, long? contentLength, CancellationToken cancellationToken)
280293
{
281294
if (cmdlet == null)
282295
{
@@ -289,12 +302,22 @@ internal static void WriteToStream(Stream input, Stream output, PSCmdlet cmdlet,
289302
ActivityId,
290303
WebCmdletStrings.WriteRequestProgressActivity,
291304
WebCmdletStrings.WriteRequestProgressStatus);
305+
string totalDownloadSize = contentLength is null ? "???" : Utils.DisplayHumanReadableFileSize((long)contentLength);
292306

293307
try
294308
{
295309
while (!copyTask.Wait(1000, cancellationToken))
296310
{
297-
record.StatusDescription = StringUtil.Format(WebCmdletStrings.WriteRequestProgressStatus, output.Position);
311+
record.StatusDescription = StringUtil.Format(
312+
WebCmdletStrings.WriteRequestProgressStatus,
313+
Utils.DisplayHumanReadableFileSize(output.Position),
314+
totalDownloadSize);
315+
316+
if (contentLength != null && contentLength > 0)
317+
{
318+
record.PercentComplete = Math.Min((int)(output.Position * 100 / (long)contentLength), 100);
319+
}
320+
298321
cmdlet.WriteProgress(record);
299322
}
300323

@@ -316,13 +339,14 @@ internal static void WriteToStream(Stream input, Stream output, PSCmdlet cmdlet,
316339
/// <param name="stream">Input stream.</param>
317340
/// <param name="filePath">Output file name.</param>
318341
/// <param name="cmdlet">Current cmdlet (Invoke-WebRequest or Invoke-RestMethod).</param>
342+
/// <param name="contentLength">Expected download size in Bytes.</param>
319343
/// <param name="cancellationToken">CancellationToken to track the cmdlet cancellation.</param>
320-
internal static void SaveStreamToFile(Stream stream, string filePath, PSCmdlet cmdlet, CancellationToken cancellationToken)
344+
internal static void SaveStreamToFile(Stream stream, string filePath, PSCmdlet cmdlet, long? contentLength, CancellationToken cancellationToken)
321345
{
322346
// If the web cmdlet should resume, append the file instead of overwriting.
323347
FileMode fileMode = cmdlet is WebRequestPSCmdlet webCmdlet && webCmdlet.ShouldResume ? FileMode.Append : FileMode.Create;
324348
using FileStream output = new(filePath, fileMode, FileAccess.Write, FileShare.Read);
325-
WriteToStream(stream, output, cmdlet, cancellationToken);
349+
WriteToStream(stream, output, cmdlet, contentLength, cancellationToken);
326350
}
327351

328352
private static string StreamToString(Stream stream, Encoding encoding)

src/Microsoft.PowerShell.Commands.Utility/resources/WebCmdletStrings.resx

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -199,13 +199,13 @@
199199
<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>
200200
</data>
201201
<data name="ReadResponseComplete" xml:space="preserve">
202-
<value>Reading web response completed. (Number of bytes read: {0})</value>
202+
<value>Reading web response stream completed. Downloaded: {0}</value>
203203
</data>
204204
<data name="ReadResponseProgressActivity" xml:space="preserve">
205-
<value>Reading web response</value>
205+
<value>Reading web response stream</value>
206206
</data>
207207
<data name="ReadResponseProgressStatus" xml:space="preserve">
208-
<value>Reading response stream... (Number of bytes read: {0})</value>
208+
<value>Downloaded: {0} of {1}</value>
209209
</data>
210210
<data name="RequestTimeout" xml:space="preserve">
211211
<value>The operation has timed out.</value>
@@ -223,7 +223,7 @@
223223
<value>Web request status</value>
224224
</data>
225225
<data name="WriteRequestProgressStatus" xml:space="preserve">
226-
<value>Number of bytes processed: {0}</value>
226+
<value>Downloaded: {0} of {1}</value>
227227
</data>
228228
<data name="JsonNetModuleRequired" xml:space="preserve">
229229
<value>The ConvertTo-Json and ConvertFrom-Json cmdlets require the 'Json.Net' module. {0}</value>

src/System.Management.Automation/engine/Utils.cs

Lines changed: 15 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -5,8 +5,6 @@
55
using System.Collections.Concurrent;
66
using System.Collections.Generic;
77
using System.Collections.ObjectModel;
8-
using System.ComponentModel;
9-
using System.Diagnostics;
108
using System.Diagnostics.CodeAnalysis;
119
using System.Globalization;
1210
using System.IO;
@@ -17,7 +15,6 @@
1715
using System.Management.Automation.Security;
1816
using System.Numerics;
1917
using System.Reflection;
20-
using System.Runtime.CompilerServices;
2118
using System.Runtime.InteropServices;
2219
using System.Security;
2320
#if !UNIX
@@ -1547,6 +1544,21 @@ internal static bool IsComObject(object obj)
15471544

15481545
return oldMode;
15491546
}
1547+
1548+
internal static string DisplayHumanReadableFileSize(long bytes)
1549+
{
1550+
return bytes switch
1551+
{
1552+
< 1024 and >= 0 => $"{bytes} Bytes",
1553+
< 1048576 and >= 1024 => $"{(bytes / 1024.0).ToString("0.0")} KB",
1554+
< 1073741824 and >= 1048576 => $"{(bytes / 1048576.0).ToString("0.0")} MB",
1555+
< 1099511627776 and >= 1073741824 => $"{(bytes / 1073741824.0).ToString("0.000")} GB",
1556+
< 1125899906842624 and >= 1099511627776 => $"{(bytes / 1099511627776.0).ToString("0.00000")} TB",
1557+
< 1152921504606847000 and >= 1125899906842624 => $"{(bytes / 1125899906842624.0).ToString("0.0000000")} PB",
1558+
>= 1152921504606847000 => $"{(bytes / 1152921504606847000.0).ToString("0.000000000")} EB",
1559+
_ => $"0 Bytes",
1560+
};
1561+
}
15501562
}
15511563
}
15521564

test/xUnit/csharp/test_Utils.cs

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,23 @@ public static void TestIsWinPEHost()
2222
Assert.False(Utils.IsWinPEHost());
2323
}
2424

25+
[Theory]
26+
[InlineData(long.MinValue, "0 Bytes")]
27+
[InlineData(-1, "0 Bytes")]
28+
[InlineData(0, "0 Bytes")]
29+
[InlineData(1, "1 Bytes")]
30+
[InlineData(1024, "1.0 KB")]
31+
[InlineData(3000, "2.9 KB")]
32+
[InlineData(1024 * 1024, "1.0 MB")]
33+
[InlineData(1024 * 1024 * 1024, "1.000 GB")]
34+
[InlineData((long)(1024 * 1024 * 1024) * 1024, "1.00000 TB")]
35+
[InlineData((long)(1024 * 1024 * 1024) * 1024 * 1024, "1.0000000 PB")]
36+
[InlineData(long.MaxValue, "8.000000000 EB")]
37+
public static void DisplayHumanReadableFileSize(long bytes, string expected)
38+
{
39+
Assert.Equal(expected, Utils.DisplayHumanReadableFileSize(bytes));
40+
}
41+
2542
[Fact]
2643
public static void TestHistoryStack()
2744
{

0 commit comments

Comments
 (0)