The specified module 'WebAdministration' was not loaded (Windows 2003, IIS 6) - iis-6

I am trying to run some IIS admin scripts on machine with -
OS - Windows 2003(with SP2)
IIS - V6.0
Powershell - V2
However when I run following commands, I get the error -
- Import-Module WebAdministration
**Error**:
Import-Module : The specified module 'WebAdministration' was not loaded because no valid module file was found in any module directory.
At line:1 char:14 + Import-Module <<<< WebAdministration
+ CategoryInfo : ResourceUnavailable: (WebAdministration:String) [Import-Module], FileNotFoundException
+ FullyQualifiedErrorId : Modules_ModuleNotFound,Microsoft.PowerShell.Commands.ImportModuleCommand
- Add-PSSnapIn WebAdministration
**Error:**
Add-PSSnapin : No snap-ins have been registered for Windows PowerShell version 2.
At line:1 char:13 + Add-PSSnapIn <<<< WebAdministration
+ CategoryInfo : InvalidArgument: (WebAdministration:String) [Add-PSSnapin], PSArgumentException
+ FullyQualifiedErrorId : AddPSSnapInRead,Microsoft.PowerShell.Commands.AddPSSnapinCommand
I checked which modules/snapin are available - here is the result -
Get-Module -ListAvailable
Result:
BitsTransfer
Get-PSSnapIn
Result:
Microsoft.PowerShell.Diagnostics
Microsoft.WSMan.Management
Microsoft.PowerShell.Core
Microsoft.PowerShell.Utility
Microsoft.PowerShell.Host
Microsoft.PowerShell.Management
Microsoft.PowerShell.Security
Please guide what shall I do to run IIS administration scripts.

Below link says Powershell SnapIn is not available for IIS 6.0:
http://forums.iis.net/p/1156851/1903821.aspx#1903821
WMI is the option to go with IIS 6.0 administration.
But WMI is not the option for me as soon we will be upgrading to IIS 7.5

In IIS6 on Windows 2k3 platform, I suggest you try accessing IIS via the old WMI provider ("Microsoftiisv2") or ADSI provider as both are accessible from the PowerShell.
Neither snapins nor WebAdministration module is available for IIS 6.0, so we can access IIS6 metabase from PowerShell using either
For IIS7.0, we can "import WebAdministration" module.
For example, I had to set the physical path for a virtual directory for IIS6, so I made use of a vbs script,iisvdir that comes along with IIS6 in c:/Windows/System32 .
Copying the code snippet
Function resetSiteLocation ($newPath)
{
Write-Host "List of Virtual directories for the site Test123 before reset :"
C:\WINDOWS\system32\iisvdir /query Test/Test123
Write-Host "About to reset site location"
C:\WINDOWS\system32\iisvdir /delete Test/Test123/Test1
C:\WINDOWS\system32\iisvdir /create Test/Test123 Test1 C:\projects\Test\Test123\Test1
C:\WINDOWS\system32\iisvdir /delete Test/Test123/Test2
C:\WINDOWS\system32\iisvdir /create Test/Test123 Test2 C:\projects\Test\Test123\Test2
Write-Host "Finished to reset site location"
Write-Host "List of Virtual directories for the site Test123 after reset :"
C:\WINDOWS\system32\iisvdir /query Test/Test123
}
Since you would be making a switch to higher version of IIS, you could put a switch in your code to determine the IIS version and take action as appropriate.
I did this:
Write-Host "Checking Installed IIS version:"
$iisVersion = Get-ItemProperty "HKLM:\SOFTWARE\Microsoft\InetStp";
Write-host IIS major version : $iisVersion.MajorVersion
Write-host IIS minor version : $iisVersion.MinorVersion
Write-Host "Finished the check."
## IIS inclusion module
## Neither snapins nor WebAdministration module is available for IIS 6.0, so we can access IIS6 metabase
## from PowerShell using either old WMI provider ("Microsoftiisv2") or ADSI provider as both are accessible from the PowerShell.
## For IIS7.0, we can import WebAdministration module
if (($iisVersion.MajorVersion -eq 7 ) -or ($iisVersion.MajorVersion -ge 7 ))
{
Write-host Detected IIS Major Version : $iisVersion.MajorVersion and Minor version : $iisVersion.MinorVersion. Hence importing WebAdministration module.
Import-Module WebAdministration;
Write-Host "About to reset app pool"
Restart-WebAppPool("Application")
Write-Host "Finished resetting app pool"
resetSiteLocation
Write-Host "About to reset site"
Restart-WebItem("IIS:\Sites\My application")
Write-Host "Finished to reset site"
}
elseif ($iisVersion.MajorVersion -eq 6)
{
Write-host IIS version 6 detected. Hence accessing IIS metabase using old WMI provider
##2. Reset App Pool
Write-Host "About to reset app pool"
Write-Host "Finished resetting app pool"
##3. Reset site location
resetSiteLocation
##4.Reset site
Write-Host "About to reset site"
Write-Host "Finished to reset site"
}
else
{
Write-host Detected IIS $iisVersion.MajorVersion
}
Let me know if it helps you.

Here is some good information on using the WMI interface with IIS 6:
http://network-nick.blogspot.com/2015/01/powershell-and-iis-6.html
He also points to the Microsoft documentation of cmdlets for this environment, here:
https://learn.microsoft.com/en-us/previous-versions/iis/6.0-sdk/ms525265(v=vs.90)
During the article he develops and explains the following PowerShell script for listing a server's web sites and their virtual directories. I actually tried this and it works.
$WebSiteID = Get-WmiObject -Namespace "root/MicrosoftIISv2" -Class IIsWebServer | Select-Object -ExpandProperty Name
ForEach ( $Site in $WebSiteID ) {
$WebSiteName = Get-WmiObject -Namespace "root/MicrosoftIISv2" -Class IIsWebServerSetting | Where-Object { $_.Name -like "$site" } `
| Select-Object -Expandproperty ServerComment
write-host "`r`n" $WebSiteName
$AppPath = Get-WmiObject -Namespace "root/MicrosoftIISv2" -Class IIsWebVirtualDirSetting | Where-Object { $_.Name -like "$site/*" } `
| select -expandproperty path
$AppPath = $AppPath | select-object -unique | sort-object
$AppPath
}

Related

Azure DevOps Server pipeline build fails when using self-signed SSL certificate with "unable to get local issuer certificate" during NuGet restore

After upgrading to Azure DevOps Server 2019, automated pipeline builds are failing at the NuGet restore step showing:
Error: Error: unable to get local issuer certificate
Packages failed to restore
Microsoft's documentation states that the build agent running on Windows uses the Windows certificate store, so I have checked that the required certificates are installed correctly on the build server, however it is still failing.
There are many questions with similar symptoms but not the same cause. After investigation, I have found the solution to this but I didn't spot anything on this exact issue so I will post an answer that will hopefully save somebody else some time!
It turns out that the Azure DevOps build agent is using a version of Node.js that doesn't use the Windows Certificate Store.
The solution required is to point Node.js at an exported copy (*.cer file) of your self-signed SSL certificate's root CA certificate, using either a system environment variable called NODE_EXTRA_CA_CERTS or by using a Task Variable called NODE.EXTRA.CA.CERTS, with a value pointing to the certificate.
Developer Community Issue Link
I use a PowerShell agent job with the following script. This effectively gives a "Use the Windows Machine Certificate Store" option to Node.JS for the pipeline.
Some notes:
Monitoring node.exe with ProcMon suggests that the file referenced in NODE_EXTRA_CA_CERTS is read every time the pipeline is run. However, others have suggested running Restart-Service vstsagent* -Force is required for the change to be picked up. This isn't my experience but perhaps something different between environments causes this behaviour.
This adds an additional ~1s pipeline execution time. Probably an acceptable price for a "set and forget certificate management for Node in Pipelines on Windows" but worth noting nonetheless.
# If running in a pipeline then use the Agent Home directory,
# otherwise use the machine temp folder which is useful for testing
if ($env:AGENT_HOMEDIRECTORY -ne $null) { $TargetFolder = $env:AGENT_HOMEDIRECTORY }
else { $TargetFolder = [System.Environment]::GetEnvironmentVariable('TEMP','Machine') }
# Loop through each CA in the machine store
Get-ChildItem -Path Cert:\LocalMachine\CA | ForEach-Object {
# Convert cert's bytes to Base64-encoded text and add begin/end markers
$Cert = "-----BEGIN CERTIFICATE-----`n"
$Cert+= $([System.Convert]::ToBase64String($_.export([System.Security.Cryptography.X509Certificates.X509ContentType]::Cert),'InsertLineBreaks'))
$Cert+= "`n-----END CERTIFICATE-----`n"
# Append cert to chain
$Chain+= $Cert
}
# Build target path
$CertFile = "$TargetFolder\TrustedRootCAs.pem"
# Write to file system
$Chain | Out-File $CertFile -Force -Encoding ASCII
# Clean-up
$Chain = $null
# Let Node (running later in the pipeline) know from where to read certs
Write-Host "##vso[task.setvariable variable=NODE.EXTRA.CA.CERTS]$CertFile"
I formatted the PowerShell script from #alifen. The script below can be executed on the build agent itself. It takes a parameter for the target path and sets the environment variable on the server.
Credit to #alifen
[CmdletBinding()]
param (
[Parameter()]
[string]
$TargetFolder = "$env:SystemDrive\Certs"
)
If (-not(Test-Path $TargetFolder))
{
$null = New-Item -ItemType Directory -Path $TargetFolder -Force
}
# Loop through each CA in the machine store
Get-ChildItem -Path Cert:\LocalMachine\CA | ForEach-Object {
# Convert cert's bytes to Base64-encoded text and add begin/end markers
$Cert = "-----BEGIN CERTIFICATE-----`n"
$Cert += $([System.Convert]::ToBase64String($_.export([System.Security.Cryptography.X509Certificates.X509ContentType]::Cert), 'InsertLineBreaks'))
$Cert += "`n-----END CERTIFICATE-----`n"
# Append cert to chain
$Chain += $Cert
}
# Build target path
$CertFile = "$TargetFolder\TrustedRootCAs.pem"
# Write to file system
Write-Host "[$($MyInvocation.MyCommand.Name)]: Exporting certs to: [$CertFile]"
$Chain | Out-File $CertFile -Force -Encoding ASCII
# Set Environment variable
Write-Host "[$($MyInvocation.MyCommand.Name)]: Setting environment variable [NODE_EXTRA_CA_CERTS] to [$CertFile]"
[Environment]::SetEnvironmentVariable("NODE_EXTRA_CA_CERTS", "$CertFile", "Machine")

Cannot install Azure/AzureRM PowerShell modules on Worker roles

I have a Worker roles on Azure Cloud service (classic) and I want them to install Azure and AzureRM PowerShell modules on startup. I've added a startup task in my ServiceDefinition.csdef file:
<Startup>
<Task commandLine="InstallAzureModules.cmd" executionContext="elevated" taskType="simple" />
</Startup>
InstallAzureModules.cmd file looks like this:
PowerShell.exe -ExecutionPolicy Unrestricted .\InstallAzureModules.ps1 >> "D:\InstallAzureModulesLogs.txt" 2>&1
And InstallAzureModules.ps1 looks like this:
Install-PackageProvider NuGet -Force
Install-Module Azure -AllowClobber -Force
Install-Module AzureRM -AllowClobber -Force
In result I have an error There is not enough space on the disk, however when I connect to any Worker instance using Remote Desktop and run InstallAzureModules.cmd manually all modules are installed without any errors.
Please help to have these modules installed.
Thanks.
Finally after contacting Microsoft Support the issue was resolved!
According to Support Professional who was working with my request this error is due to a redirection of application temporary folder. I was recommended to go through this link for a possible fix, but it didn't work.
Also updates to my PS script were provided and what actually helped was setting TMP and TEMP environment variables to a folder on drive C. So here is my final script:
$env_TMP = $env:TMP
$env_TEMP = $env:TEMP
$env:TMP = "C:\_trashable\Modules"
$env:TEMP = "C:\_trashable\Modules"
Install-PackageProvider NuGet -Force
Install-Module Azure -AllowClobber -Force
$env:TMP = $env_TMP
$env:TEMP = $env_TEMP
ECHO "Finished"

How to execute .sql file using powershell?

I have a .sql file. I am trying to pass connection string details through a Powershell script and invoke an .sql file.
I was searching and came up with a cmdlet related to Invoke-sqlcmd. While I was trying to find a module corresponding to SQL, I did not find any one in my machine.
Should I install anything in my machine (the machine already has SQL Server Management Studio 2008 R2) to get the modules or is there any easy way to execute .sql files using Powershell?
Try to see if SQL snap-ins are present:
get-pssnapin -Registered
Name : SqlServerCmdletSnapin100
PSVersion : 2.0
Description : This is a PowerShell snap-in that includes various SQL Server cmdlets.
Name : SqlServerProviderSnapin100
PSVersion : 2.0
Description : SQL Server Provider
If so
Add-PSSnapin SqlServerCmdletSnapin100 # here lives Invoke-SqlCmd
Add-PSSnapin SqlServerProviderSnapin100
then you can do something like this:
invoke-sqlcmd -inputfile "c:\mysqlfile.sql" -serverinstance "servername\serverinstance" -database "mydatabase" # the parameter -database can be omitted based on what your sql script does.
Quoting from Import the SQLPS Module on MSDN,
The recommended way to manage SQL Server from PowerShell is to import
the sqlps module into a Windows PowerShell 2.0 environment.
So, yes, you could use the Add-PSSnapin approach detailed by Christian, but it is also useful to appreciate the recommended sqlps module approach.
The simplest case assumes you have SQL Server 2012: sqlps is included in the installation so you simply load the module like any other (typically in your profile) via Import-Module sqlps. You can check if the module is available on your system with Get-Module -ListAvailable.
If you do not have SQL Server 2012, then all you need do is download the sqlps module into your modules directory so Get-Module/Import-Module will find it. Curiously, Microsoft does not make this module available for download! However, Chad Miller has kindly packaged up the requisite pieces and provided this module download. Unzip it under your ...Documents\WindowsPowerShell\Modules directory and proceed with the import.
It is interesting to note that the module approach and the snapin approach are not identical. If you load the snapins then run Get-PSSnapin (without the -Registered parameter, to show only what you have loaded) you will see the SQL snapins. If, on the other hand, you load the sqlps module Get-PSSnapin will not show the snapins loaded, so the various blog entries that test for the Invoke-Sqlcmd cmdlet by only examining snapins could be giving a false negative result.
2012.10.06 Update
For the complete story on the sqlps module vs. the sqlps mini-shell vs. SQL Server snap-ins, take a look at my two-part mini-series Practical PowerShell for SQL Server Developers and DBAs recently published on Simple-Talk.com where I have, according to one reader's comment, successfully "de-confused" the issue. :-)
if(Test-Path "C:\Program Files\Microsoft SQL Server\MSSQL11.SQLEXPRESS") { #Sql Server 2012
Import-Module SqlPs -DisableNameChecking
C: # Switch back from SqlServer
} else { #Sql Server 2008
Add-PSSnapin SqlServerCmdletSnapin100 # here live Invoke-SqlCmd
}
Invoke-Sqlcmd -InputFile "MySqlScript.sql" -ServerInstance "Database name" -ErrorAction 'Stop' -Verbose -QueryTimeout 1800 # 30min
Here is a function that I have in my PowerShell profile for loading SQL snapins:
function Load-SQL-Server-Snap-Ins
{
try
{
$sqlpsreg="HKLM:\SOFTWARE\Microsoft\PowerShell\1\ShellIds\Microsoft.SqlServer.Management.PowerShell.sqlps"
if (!(Test-Path $sqlpsreg -ErrorAction "SilentlyContinue"))
{
throw "SQL Server Powershell is not installed yet (part of SQLServer installation)."
}
$item = Get-ItemProperty $sqlpsreg
$sqlpsPath = [System.IO.Path]::GetDirectoryName($item.Path)
$assemblyList = #(
"Microsoft.SqlServer.Smo",
"Microsoft.SqlServer.SmoExtended",
"Microsoft.SqlServer.Dmf",
"Microsoft.SqlServer.WmiEnum",
"Microsoft.SqlServer.SqlWmiManagement",
"Microsoft.SqlServer.ConnectionInfo ",
"Microsoft.SqlServer.Management.RegisteredServers",
"Microsoft.SqlServer.Management.Sdk.Sfc",
"Microsoft.SqlServer.SqlEnum",
"Microsoft.SqlServer.RegSvrEnum",
"Microsoft.SqlServer.ServiceBrokerEnum",
"Microsoft.SqlServer.ConnectionInfoExtended",
"Microsoft.SqlServer.Management.Collector",
"Microsoft.SqlServer.Management.CollectorEnum"
)
foreach ($assembly in $assemblyList)
{
$assembly = [System.Reflection.Assembly]::LoadWithPartialName($assembly)
if ($assembly -eq $null)
{ Write-Host "`t`t($MyInvocation.InvocationName): Could not load $assembly" }
}
Set-Variable -scope Global -name SqlServerMaximumChildItems -Value 0
Set-Variable -scope Global -name SqlServerConnectionTimeout -Value 30
Set-Variable -scope Global -name SqlServerIncludeSystemObjects -Value $false
Set-Variable -scope Global -name SqlServerMaximumTabCompletion -Value 1000
Push-Location
if ((Get-PSSnapin -Name SqlServerProviderSnapin100 -ErrorAction SilentlyContinue) -eq $null)
{
cd $sqlpsPath
Add-PsSnapin SqlServerProviderSnapin100 -ErrorAction Stop
Add-PsSnapin SqlServerCmdletSnapin100 -ErrorAction Stop
Update-TypeData -PrependPath SQLProvider.Types.ps1xml
Update-FormatData -PrependPath SQLProvider.Format.ps1xml
}
}
catch
{
Write-Host "`t`t$($MyInvocation.InvocationName): $_"
}
finally
{
Pop-Location
}
}
Here's a light weight approach for simple scripts that requires no additional tools / setup / PowerShell add-ons.
$conn = New-Object System.Data.SqlClient.SqlConnection
$conn.ConnectionString = $connectionStringGoesHere
$conn.Open()
$content = Get-Content $scriptFileNameGoesHere
$cmds = New-Object System.Collections.ArrayList
$cmd = ""
$content | foreach {
if ($_.Trim() -eq "GO") { $cmds.Add($cmd); $cmd = "" }
else { $cmd = $cmd + $_ +"`r`n" }
}
$cmds | foreach {
$sc = New-Object System.Data.SqlClient.SqlCommand
$sc.CommandText = $_
$sc.Connection = $conn
$sc.ExecuteNonQuery()
}
with 2008 Server 2008 and 2008 R2
Add-PSSnapin -Name SqlServerCmdletSnapin100, SqlServerProviderSnapin100
with 2012 and 2014
Push-Location
Import-Module -Name SQLPS -DisableNameChecking
Pop-Location
Invoke-Sqlcmd -Database MyDatabase -Query "exec dbo.sp_executesql N'$(Get-Content "c:\my.sql")'"

Powershell Version 2 Load DLL for use on persistent connection's invoke command scriptblock

Does anyone know how I can load a DLL without having it on each remote server I am using in a persistent connection and running the invoke-command cmdlet with?
I am using DotNetZip to backup folders on about 13 servers. Everything is working locally, but when it gets to a remote server (the first one in the array is the local server), it errors because it doesn't see the DLL on the remote server.
I execute this script on one server and it should zip up folders on each remote server:
foreach($i in $appServers) {
$sessionForI = New-PSSession -computername $i
Invoke-Command -Session $sessionForI -ScriptBlock {
if (!(Test-Path -path C:\\newDeploy)) {
New-Item C:\\newDeploy -type directory
}
[System.Reflection.Assembly]::LoadFrom("C:\\newDeploy\\Ionic.Zip.dll");
$directoryToZip = "C:\\Program Files (x86)\\SubDir\\$folder"
$zipfile = new-object Ionic.Zip.ZipFile
$e = $zipfile.AddSelectedFiles("name != '*.e2e'",$directoryToZip, "",1)
if (!(Test-Path -path C:\\newDeploy\\backup)) {
New-Item C:\\newDeploy\\backup -type directory
}
$zipfile.Save("C:\\newDeploy\\backup\\" + $folder+ ".zip")
$zipfile.Dispose()
}
remove-PSSession -session $sessionForI
}
Thank you .
-Jim
I'm pretty sure you are going to need to copy Ionic.Zip.dll to the remote machines to do this. You could try sharing it out from your lead system and using a UNC path to load it from the remote machines (i've never tried that... going to now...) :-)
Update - yep just confirmed you can pass a UNC path to [System.Reflection.Assembly]::LoadFrom.
Update 2 - While the assembly loaded, using it didn't work so well:
Exception calling "AddFile" with "1" argument(s): "Request for the permission of type 'System.Security.Permissions.File
IOPermission, mscorlib, Version=2.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089' failed."
At line:1 char:11
+ $z.AddFile <<<< ("C:\AMCleanUp.log")
+ CategoryInfo : NotSpecified: (:) [], MethodInvocationException
+ FullyQualifiedErrorId : DotNetMethodException
When I loaded a local copy of the the DLL the AddFile method worked fine. You're only option might be to copy this DLL to all your servers...
You can use a UNC path in the LoadFrom for the remote boxes, but I see that someone has had problem doing the same with DotNetZip:
http://social.technet.microsoft.com/Forums/en-US/winserverpowershell/thread/dd5dcae2-1ccc-4be2-b986-61c069102ffb/
I think your problems with accessing remote resources in an already remote session has to do with double-hop authentication. Check this link http://www.ravichaganti.com/blog/?p=1230

How to use Cloudberry Powershell Snap-in (for Amazon S3) from within a scheduled SQL Agent Job?

I am trying to automate my SQL database backup process. My goal is to use the Cloudberry Powershell cmdlet to give me direct control and access over my S3 buckets. I am able to do this manually but cannot get my SQL jobs to work with this.
According to Cloudberry's installation instructions, I shouldn't have to register the Cloudberry Powershell snap-in if Powershell is already installed. I have found that to be false. I have tried to register it, both 64-bit and 32-bit with no luck.
This works when executed manually/explicitly from the ISE:
Add-PSSnapin CloudBerryLab.Explorer.PSSnapIn
$today = Get-Date -format "yyyy.MM.dd.HH.mm.ss"
$key = "mykeygoeshere"
$secret = "mysecretgoeshere"
$s3 = Get-CloudS3Connection -Key $key -Secret $secret
$destination = $s3 | Select-CloudFolder -path "ProductionBackups/MyClient/log/" | Add-CloudFolder $today
$src = Get-CloudFilesystemConnection | Select-CloudFolder "X:\backups\MyClient\current\"
$src | Copy-CloudItem $destination -filter "log.trn"
^ When this command is executed in a SQL Agent job, it fails with this message:
Executed as user: DB-MAIN\SYSTEM. A job step received an error at line 1 in a PowerShell script. The corresponding line is 'Add-PSSnapin CloudBerryLab.Explorer.PSSnapIn'. Correct the script and reschedule the job. The error information returned by PowerShell is: 'The term 'Add-PSSnapin' is not recognized as the name of a cmdlet, function, script file, or operable program. Check the spelling of the name, or if a path was included, verify that the path is correct and try again. '. Process Exit Code -1. The step failed.
I read in this blog post that SQLPS.exe cannot execute 'Add-PSSnapin' commands? Is that true? I cannot find any clarification on the subject...
how can I automate my SQL backup files to the Amazon S3 cloud? I have tried everything. TNT Drive was a huge waste of time. I am hoping Cloudberry can do it, any tips?
You could use Amazon AWS .Net SDK. You can download it from here:
http://aws.amazon.com/sdkfornet/
Here's the example function to download file from S3:
function DownloadS3File([string]$bucket, [string]$file, [string]$localFile)
{
if (Test-Path "C:\Program Files (x86)")
{
Add-Type -Path "C:\Program Files (x86)\AWS SDK for .NET\bin\AWSSDK.dll"
}
else
{
Add-Type -Path "C:\Program Files\AWS SDK for .NET\bin\AWSSDK.dll"
}
$secretKeyID= $env:AWS_ACCESS_KEY_ID
$secretAccessKeyID= $env:AWS_SECRET_ACCESS_KEY
$client=[Amazon.AWSClientFactory]::CreateAmazonS3Client($secretKeyID,$secretAccessKeyID)
$request = New-Object -TypeName Amazon.S3.Model.GetObjectRequest
$request.BucketName = $bucket
$request.Key = $file
$response = $client.GetObject($request)
$writer = new-object System.IO.FileStream ($localFile ,[system.IO.filemode]::Create)
[byte[]]$buffer = new-object byte[] 4096
[int]$total = [int]$count = 0
do
{
$count = $response.ResponseStream.Read($buffer, 0, $buffer.Length)
$writer.Write($buffer, 0, $count)
}
while ($count -gt 0)
$response.ResponseStream.Close()
$writer.Close()
echo "File downloaded: $localFile"
}