Google
 

Saturday, May 2, 2009

Downloading files using Windows PowerShell with progress information

Downloading a file using PowerShell is very easy, you just need to call WebClient.DownloadFile method from the .net framework. But let's make things more interesting.

I need a script to download a list of files whose URLs are specified in a file to local folder. And the script should check if the file already exists. If it is, it should skip to the next file. To give a better user experience, a (progress bar) will be displayed to notify the user about the progress.
Error handling and reporting is important. So we'll take care of it inside the script.

Let's start analyzing how to accomplish this.

The code:
The first line of code defines the script parameters which are inputFile: the path of the file that contains the list of URLs and folder where we'll download files to.

param ([string] $inputFile,[string]$folder)

We make some basic input validation to check if parameters are set:

trap {Write-Host "Error: $_" -Foregroundcolor Red -BackGroundColor Black;exit}

if([string]::IsNullOrEmpty($folder))
{
throw "folder parameter not set";
}

if([string]::IsNullOrEmpty($inputFile))
{
throw "inputFile parameter not set";
}


The above code starts with defining an error handler that will run when a stopping error occurs in the script (in the current scope). It simply say: When an error occurs, write it to the host and exit the script.
Note that I pass Foregroundcolor and BackGroundColor parameter to the Write-Host Cmdlet so the user gets the same experience he gets with other PowerShell errors.
The next validation code simply check if the parameters are passed. If not, an error is thrown.

Next, we read the contents of the input file:

$files = Get-Content $inputFile -ErrorAction Stop

I use the Get-Content Cmdlet specifying the ErrorAction parameter = Stop. ErrorAction is a common parameter for PowerShell Cmdlets. The Stop value asks PowerShell to consider any errors fatal errors that will stop the script. But as I defined the trap handler as shown above, the same error handling will apply. The error message will be displayed and the script will exit.

The next line creates a WebClient object to be used in the download porcess:

$web = New-Object System.Net.WebClient

Next, I initialize a counter to be used in progress display based on the number of files downloaded so far. The a for loop is used to iterate on files.
Note that I define another trap handler that will act whenever an error is thrown within the for loop scope. It calls a specific function that handles download errors then continues the loop:

foreach($file in $files)
{
trap {ErrorHandler($_);continue}
.
.
.
}


Inside the loop, I create the download file path and display the progress using Write-Progress Cmdlet.

$path = [IO.Path]::Combine($folder,$file.SubString($file.LastIndexOf("/")+1));

Write-Progress -Activity "downloading" -Status $path -PercentComplete (($i / $files.Length)*100)


And if the file does not exit, DownloadFile is called.

if([System.IO.File]::Exists($path) -eq $False )
{
$web.DownloadFile($file,$path)
}


I used the script to download presentations from the MIX 2009 conference. The attached ZIP file includes both the PowerShell script and the file that contains the URLs)




Complete code listing:

param ([string] $inputFile,[string]$folder)

function ErrorHandler($error)
{
Write-Host "Error while downloading file:$file" -Foregroundcolor Red -BackGroundColor Black
Write-Host $error -Foregroundcolor Red -BackGroundColor Black
Write-Host ""
}

trap {Write-Host "Error: $_" -Foregroundcolor Red -BackGroundColor Black;exit}

if([string]::IsNullOrEmpty($folder))
{
throw "folder parameter not set";
}

if([string]::IsNullOrEmpty($inputFile))
{
throw "inputFile parameter not set";
}


$files = Get-Content $inputFile -ErrorAction Stop

$web = New-Object System.Net.WebClient
$i = 0
foreach($file in $files)
{
trap {ErrorHandler($_);continue}

$path = [IO.Path]::Combine($folder,$file.SubString($file.LastIndexOf("/")+1));

Write-Progress -Activity "downloading" -Status $path -PercentComplete (($i / $files.Length)*100)



if([System.IO.File]::Exists($path) -eq $False )
{
$web.DownloadFile($file,$path)
}

$i = $i+1
}

3 comments:

KK said...

Great to see you here brother..Thanks for stopping by and sharing your thoughts.
Backup Online

Atique said...

Is it possible to show download progress for single big file download?

Hesham A. Amin said...

@ Atique:
DownloadFile is blocking. But I think the progress bar can still be made but not sure how.
When I figure it out, this might be a new blog post :)