1

So, I have a script which shows Download Progress from FTP. I just try many ways to solve this task. One of the conclusions was that cmdlet Register-ObjectEvent is a really bad idea.. Async eventing is rather poorly supported in Powershell...
And I stopped there -

$webclient.add_DownloadProgressChanged([System.Net.DownloadProgressChangedEventHandler]$webclient_DownloadProgressChanged )
....
$webclient_DownloadProgressChanged = {
param([System.Net.DownloadProgressChangedEventArgs]$Global:e)
$progressbaroverlay1.value=$e.ProgressPercentage
....
}


And everything in this sript works fine, but you can understand that I did this was for a one file.
But then I started thinking - How I can download several files at the same time and show it in a one progress bar? So anyone have any great ideas? Or best way to solve this task?

P.S

WebClient can only download one file at a time.

Of course, I know it.

  • Two progress bars, where the first shows x of y files, and the second shows the second file's progress? Otherwise you'd have to find the size of each file, keep track of total bytes downloaded, and perform math on that to get your % done. – TheMadTechnician May 31 '17 at 22:36
  • So, make more than one webclient. Have a script scope variable they all update with their individual progress and have the progress bar show the (sum of their progress / how many there are).. – TessellatingHeckler May 31 '17 at 22:39
  • @TessellatingHeckler Yes, but you can't run two or several $WebClient.DownloadFileAsync at the same time. – Daniil Romme May 31 '17 at 23:31
  • @DaniilKuzmin I made two WebClient objects, set them both downlaoding async, and it's working for me; see how Process Monitor tracks to two ISO files being written at the same time, both with hundreds of Mb downloaded (offset), not queuing or failing one? http://i.imgur.com/N82WoEO.png – TessellatingHeckler May 31 '17 at 23:49
  • @TessellatingHeckler It's not that simple)..When you work with EventHandler the game is about to change. Now you can try to show $e.ProgressPercentage without write-progress and Register-ObjectEvent. – Daniil Romme Jun 01 '17 at 00:15
  • @TessellatingHeckler And second issue that you want to know, how can you subscribe to the same event?)! – Daniil Romme Jun 01 '17 at 00:51

3 Answers3

1

I came up with the same kind of Scriptblock Create approach, but I did use Register-ObjectEvent, with more or less success. The Async downloads happen as background jobs, and use events to communicate their progress back to the main script.

$Progress = @{}

$Isos = 'https://cdimage.debian.org/debian-cd/current/i386/iso-cd/debian-8.8.0-i386-CD-1.iso', 
        'https://cdimage.debian.org/debian-cd/current/i386/iso-cd/debian-8.8.0-i386-CD-2.iso'

$Count = 0
$WebClients = $Isos | ForEach-Object {

    $w = New-Object System.Net.WebClient

    $null = Register-ObjectEvent -InputObject $w -EventName DownloadProgressChanged -Action ([scriptblock]::Create(
        "`$Percent = 100 * `$eventargs.BytesReceived / `$eventargs.TotalBytesToReceive; `$null = New-Event -SourceIdentifier MyDownloadUpdate -MessageData @($count,`$Percent)"
    ))
    $w.DownloadFileAsync($_, "C:\PATH_TO_DOWNLOAD_FOLDER\$count.iso")

    $Count = $Count + 1
    $w
}

$event = Register-EngineEvent -SourceIdentifier MyDownloadUpdate -Action {
    $progress[$event.MessageData[0]] = $event.MessageData[1]
}

$Timer = New-Object System.Timers.Timer
Register-ObjectEvent -InputObject $Timer -EventName Elapsed -Action { 
    if ($Progress.Values.Count -gt 0) 
    {
        $PercentComplete =  100 * ($Progress.values | Measure-Object -Sum | Select-Object -ExpandProperty Sum) / $Progress.Values.Count
        Write-Progress -Activity "Download Progress" -PercentComplete $PercentComplete
    }
}

$timer.Interval = 100
$timer.AutoReset = $true
$timer.Start()

Exercise for the reader for how to tell that the downloads have finished.

TessellatingHeckler
  • 27,511
  • 4
  • 48
  • 87
  • That is, very good, very good...But this script may contain bugs and work incorrectly. [Why is register-ObjectEvent a really bad idea?](https://stackoverflow.com/questions/18151704/slow-updates-to-my-event-handler-from-downloadfileasync-during-downloadprogressc) However, what you did is a great script. – Daniil Romme Jun 01 '17 at 21:21
0

You can use BitsTransfer module Asynchronous download. https://technet.microsoft.com/en-us/library/dd819420.aspx

Example code to show overall process of 3 files, you specify an equal array of urls and download locations, you can do further with that to your liking like exception handling etc:

Import-Module BitsTransfer
[string[]]$url = @();
$url += 'https://www.samba.org/ftp/talloc/talloc-2.1.6.tar.gz';
$url += 'https://e.thumbs.redditmedia.com/pF525auqxnTG-FFj.png';
$url += 'http://bchavez.bitarmory.com/Skins/DayDreaming/images/bg-header.gif';

[string[]]$destination = @();
$destination += 'C:\Downloads\talloc-2.1.6.tar.gz';
$destination += 'C:\Downloads\pF525auqxnTG-FFj.png';
$destination += 'C:\Downloads\bg-header.gif';

$result = Start-BitsTransfer -Source $url -Destination $destination -TransferType Download -Asynchronous
$downloadsFinished = $false;
    While ($downloadsFinished -ne $true) {
        sleep 1
        $jobstate = $result.JobState;
        if($jobstate.ToString() -eq 'Transferred') { $downloadsFinished = $true }
        $percentComplete = ($result.BytesTransferred / $result.BytesTotal) * 100
        Write-Progress -Activity ('Downloading' + $result.FilesTotal + ' files') -PercentComplete $percentComplete 
}
Iggy Zofrin
  • 515
  • 3
  • 10
0

I see two possible concepts for this:

  1. Create (with [scriptblock]::Create) an anonymous function on the fly, some something like:

$Id = 0
... | ForEach {
    $webclient[$Id].add_DownloadProgressChanged([System.Net.DownloadProgressChangedEventHandler]{[scriptblock]::Create("
        ....
        `$webclient_DownloadProgressChanged = {
        param([System.Net.DownloadProgressChangedEventArgs]`$e)
        `$Global:ProgressPercentage[$Id]=`$e.ProgressPercentage
        `$progressbaroverlay1.value=(`$Global:ProgressPercentage | Measure-Object -Average).Average
        ....
        "
        $Id++
    })
}

Note that in this idea you need to prevent everything but the $Id to be directly interpreted with a backtick.

Or if the function gets too large to be read, simplify the [ScriptBlock]:

[ScriptBlock]::Create("param(`$e); webclient_DownloadProgressChanged $Id `$e")

and call a global function:

$Global:webclient_DownloadProgressChanged($Id, $e) {
    $Global:ProgressPercentage[$Id]=$e.ProgressPercentage
    $progressbaroverlay1.value=($Global:ProgressPercentage | Measure-Object -Average).Average
}
  1. Create your own custom background workers (threads):

For an example see: PowerShell: Job Event Action with Form not executed

  • In the main thread build your UI with progress bars
  • For each FTP download:
    • Create a shared (hidden) windows control (e.g. .TextBox[$Id])
    • Start a new background worker and share the related control, something like:
      $SyncHash = [hashtable]::Synchronized(@{TextBox = $TextBox[$Id]})
  • Update the shared $SyncHash.TextBox.Text = from within the WebWorker(s)
  • Capture the events (e.g. .Add_TextChanged) on each .TextBox[$Id] in the main thread
  • Update your progress bars accordingly based the average status passed in each .TextBox[$Id].Text
iRon
  • 20,463
  • 10
  • 53
  • 79