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 a27e4e70008..feed6d0c0ad 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 @@ -140,8 +140,12 @@ internal override void ProcessResponse(HttpResponseMessage response) } } else if (ShouldSaveToOutFile) - { - StreamHelper.SaveStreamToFile(baseResponseStream, QualifiedOutFile, this, response.Content.Headers.ContentLength.GetValueOrDefault(), _cancelToken.Token); + { + string outFilePath = WebResponseHelper.GetOutFilePath(response, _qualifiedOutFile); + + WriteVerbose(string.Create(System.Globalization.CultureInfo.InvariantCulture, $"File Name: {Path.GetFileName(_qualifiedOutFile)}")); + + StreamHelper.SaveStreamToFile(baseResponseStream, outFilePath, this, response.Content.Headers.ContentLength.GetValueOrDefault(), _cancelToken.Token); } if (!string.IsNullOrEmpty(StatusCodeVariable)) diff --git a/src/Microsoft.PowerShell.Commands.Utility/commands/utility/WebCmdlet/Common/WebRequestPSCmdlet.Common.cs b/src/Microsoft.PowerShell.Commands.Utility/commands/utility/WebCmdlet/Common/WebRequestPSCmdlet.Common.cs index 567a98ebfb9..c3a7be4f317 100644 --- a/src/Microsoft.PowerShell.Commands.Utility/commands/utility/WebCmdlet/Common/WebRequestPSCmdlet.Common.cs +++ b/src/Microsoft.PowerShell.Commands.Utility/commands/utility/WebCmdlet/Common/WebRequestPSCmdlet.Common.cs @@ -474,6 +474,8 @@ public virtual string CustomMethod internal string QualifiedOutFile => QualifyFilePath(OutFile); + internal string _qualifiedOutFile; + internal bool ShouldCheckHttpStatus => !SkipHttpErrorCheck; /// @@ -820,7 +822,7 @@ internal virtual void ValidateParameters() } // Output ?? - if (PassThru && OutFile is null) + if (PassThru.IsPresent && OutFile is null) { ErrorRecord error = GetValidationError(WebCmdletStrings.OutFileMissing, "WebCmdletOutFileMissingException", nameof(PassThru)); ThrowTerminatingError(error); @@ -832,6 +834,15 @@ internal virtual void ValidateParameters() ErrorRecord error = GetValidationError(WebCmdletStrings.OutFileMissing, "WebCmdletOutFileMissingException", nameof(Resume)); ThrowTerminatingError(error); } + + _qualifiedOutFile = ShouldSaveToOutFile ? QualifiedOutFile : null; + + // OutFile must not be a directory to use Resume. + if (Resume.IsPresent && Directory.Exists(_qualifiedOutFile)) + { + ErrorRecord error = GetValidationError(WebCmdletStrings.ResumeNotFilePath, "WebCmdletResumeNotFilePathException", _qualifiedOutFile); + ThrowTerminatingError(error); + } } internal virtual void PrepareSession() @@ -1083,6 +1094,7 @@ internal virtual HttpRequestMessage GetRequest(Uri uri) if (Resume.IsPresent) { FileInfo fileInfo = new(QualifiedOutFile); + if (fileInfo.Exists) { request.Headers.Range = new RangeHeaderValue(fileInfo.Length, null); 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 e101bcc65b7..94245548e80 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 @@ -56,7 +56,11 @@ internal override void ProcessResponse(HttpResponseMessage response) if (ShouldSaveToOutFile) { - StreamHelper.SaveStreamToFile(responseStream, QualifiedOutFile, this, response.Content.Headers.ContentLength.GetValueOrDefault(), _cancelToken.Token); + string outFilePath = WebResponseHelper.GetOutFilePath(response, _qualifiedOutFile); + + WriteVerbose(string.Create(System.Globalization.CultureInfo.InvariantCulture, $"File Name: {Path.GetFileName(_qualifiedOutFile)}")); + + StreamHelper.SaveStreamToFile(responseStream, outFilePath, this, response.Content.Headers.ContentLength.GetValueOrDefault(), _cancelToken.Token); } } diff --git a/src/Microsoft.PowerShell.Commands.Utility/commands/utility/WebCmdlet/CoreCLR/WebResponseHelper.CoreClr.cs b/src/Microsoft.PowerShell.Commands.Utility/commands/utility/WebCmdlet/CoreCLR/WebResponseHelper.CoreClr.cs index aaf921c7f0d..a4b3ca1c0a1 100644 --- a/src/Microsoft.PowerShell.Commands.Utility/commands/utility/WebCmdlet/CoreCLR/WebResponseHelper.CoreClr.cs +++ b/src/Microsoft.PowerShell.Commands.Utility/commands/utility/WebCmdlet/CoreCLR/WebResponseHelper.CoreClr.cs @@ -4,6 +4,7 @@ using System; using System.Collections.Generic; using System.Globalization; +using System.IO; using System.Net.Http; namespace Microsoft.PowerShell.Commands @@ -34,6 +35,14 @@ internal static Dictionary> GetHeadersDictionary(Htt return headers; } + internal static string GetOutFilePath(HttpResponseMessage response, string _qualifiedOutFile) + { + // Get file name from last segment of Uri + string lastUriSegment = System.Net.WebUtility.UrlDecode(response.RequestMessage.RequestUri.Segments[^1]); + + return Directory.Exists(_qualifiedOutFile) ? Path.Join(_qualifiedOutFile, lastUriSegment) : _qualifiedOutFile; + } + internal static string GetProtocol(HttpResponseMessage response) => string.Create(CultureInfo.InvariantCulture, $"HTTP/{response.Version}"); internal static int GetStatusCode(HttpResponseMessage response) => (int)response.StatusCode; diff --git a/src/Microsoft.PowerShell.Commands.Utility/resources/WebCmdletStrings.resx b/src/Microsoft.PowerShell.Commands.Utility/resources/WebCmdletStrings.resx index a9628c647e3..a6ca8173025 100644 --- a/src/Microsoft.PowerShell.Commands.Utility/resources/WebCmdletStrings.resx +++ b/src/Microsoft.PowerShell.Commands.Utility/resources/WebCmdletStrings.resx @@ -203,6 +203,9 @@ Downloaded: {0} of {1} + + + The Resume switch can only be used if OutFile targets a file but it resolves to a directory: {0}. The cmdlet cannot run because the following conflicting parameters are specified: Session and SessionVariable. Specify either Session or SessionVariable, then retry. diff --git a/test/powershell/Modules/Microsoft.PowerShell.Utility/WebCmdlets.Tests.ps1 b/test/powershell/Modules/Microsoft.PowerShell.Utility/WebCmdlets.Tests.ps1 index 2da345fcd3a..de539c40f46 100644 --- a/test/powershell/Modules/Microsoft.PowerShell.Utility/WebCmdlets.Tests.ps1 +++ b/test/powershell/Modules/Microsoft.PowerShell.Utility/WebCmdlets.Tests.ps1 @@ -719,6 +719,19 @@ Describe "Invoke-WebRequest tests" -Tags "Feature", "RequireAdminOnWindows" { $jsonContent.headers.Host | Should -Be $uri.Authority } + It "Invoke-WebRequest -OutFile folder Downloads the file and names it" { + $uri = Get-WebListenerUrl -Test 'Get' + $content = Invoke-WebRequest -Uri $uri + $outFile = Join-Path $TestDrive $content.BaseResponse.RequestMessage.RequestUri.Segments[-1] + + # ensure the file does not exist + Remove-Item -Force -ErrorAction Ignore -Path $outFile + Invoke-WebRequest -Uri $uri -OutFile $TestDrive + + Test-Path $outFile | Should -Be $true + Get-Item $outFile | Select-Object -ExpandProperty Length | Should -Be $content.Content.Length + } + It "Invoke-WebRequest should fail if -OutFile is ." -TestCases @( @{ Name = "empty"; Value = [string]::Empty } @{ Name = "null"; Value = $null } @@ -1943,6 +1956,11 @@ Describe "Invoke-WebRequest tests" -Tags "Feature", "RequireAdminOnWindows" { Should -Throw -ErrorId 'WebCmdletOutFileMissingException,Microsoft.PowerShell.Commands.InvokeWebRequestCommand' } + It "Invoke-WebRequest -Resume should fail if -OutFile folder" { + { Invoke-WebRequest -Resume -Uri $resumeUri -OutFile $TestDrive -ErrorAction Stop } | + Should -Throw -ErrorId 'WebCmdletResumeNotFilePathException,Microsoft.PowerShell.Commands.InvokeWebRequestCommand' + } + It "Invoke-WebRequest -Resume Downloads the whole file when the file does not exist" { $response = Invoke-WebRequest -Uri $resumeUri -OutFile $outFile -Resume -PassThru @@ -2477,6 +2495,19 @@ Describe "Invoke-RestMethod tests" -Tags "Feature", "RequireAdminOnWindows" { $jsonContent.headers.Host | Should -Be $uri.Authority } + It "Invoke-RestMethod -OutFile folder Downloads the file and names it" { + $uri = Get-WebListenerUrl -Test 'Get' + $content = Invoke-WebRequest -Uri $uri + $outFile = Join-Path $TestDrive $content.BaseResponse.RequestMessage.RequestUri.Segments[-1] + + # ensure the file does not exist + Remove-Item -Force -ErrorAction Ignore -Path $outFile + Invoke-RestMethod -Uri $uri -OutFile $TestDrive + + Test-Path $outFile | Should -Be $true + Get-Item $outFile | Select-Object -ExpandProperty Length | Should -Be $content.Content.Length + } + It "Invoke-RestMethod should fail if -OutFile is ." -TestCases @( @{ Name = "empty"; Value = [string]::Empty } @{ Name = "null"; Value = $null } @@ -3627,6 +3658,11 @@ Describe "Invoke-RestMethod tests" -Tags "Feature", "RequireAdminOnWindows" { Should -Throw -ErrorId 'WebCmdletOutFileMissingException,Microsoft.PowerShell.Commands.InvokeRestMethodCommand' } + It "Invoke-RestMethod -Resume should fail if -OutFile folder" { + { Invoke-RestMethod -Resume -Uri $resumeUri -OutFile $TestDrive -ErrorAction Stop } | + Should -Throw -ErrorId 'WebCmdletResumeNotFilePathException,Microsoft.PowerShell.Commands.InvokeRestMethodCommand' + } + It "Invoke-RestMethod -Resume Downloads the whole file when the file does not exist" { # ensure the file does not exist Remove-Item -Force -ErrorAction 'SilentlyContinue' -Path $outFile