Downloading large files in Windows command prompt / PowerShell
Asked Answered
W

2

11

I'm kind of running out of options now...

Attempt 1

Use iwr in Powershell. It works, shows progress but its 10X slower and doesn't flush until while file is in memory :(.

 powershell -command "& { iwr https://github.com/mitchellspryn/AirsimHighPolySuv/releases/download/V1.0.0/SUV.zip -OutFile SUV.zip }"

Attempt 2

Use .Net webclient in Powershell. It works but shows no progress and you can't terminate by Ctrl+C :(. Last issue is a big setback.

powershell -command "& { (New-Object System.Net.WebClient).DownloadFile('https://github.com/mitchellspryn/AirsimHighPolySuv/releases/download/V1.0.0/SUV.zip', 'SUV.zip') }"

Attempt 3

Use BITS transfer in Powershell. It works, shows progress and almost perfect... until you find out that it mysteriously doesn't work on GitHub (errors out with 403 forbidden)!!

powershell -command "& { Start-BitsTransfer -Source https://github.com/mitchellspryn/AirsimHighPolySuv/releases/download/V1.0.0/SUV.zip -Destination SUV.zip }"
Whitacre answered 19/10, 2017 at 13:10 Comment(3)
Why run powershell.exe separately (powershell.exe -command ...)? Just run the command you need directly from the PowerShell command line.Gob
I'm using this from another build script and converting whole script to PS is not our goal for now.Whitacre
I tried Start-BitsTransfer on a github asset today, and it worked. (even tried yours). Is it possible this now works on Github like ou wanted?Experiment
R
10

After some research I found @MichaelS's method the simplest, nevertheless I added some modifications that you could find useful, as such:

  1. Added Try/Finally blocks, so we could handle user interruptions (Ctrl+C) and cleanup (See Gracefully stopping in Powershell)
  2. Utilize Write-Progress functionality, so it feels more like Start-BitsTransfer
  3. Buffer size optimization, atleast for my particular connection/machine (see https://www.microsoft.com/en-us/research/wp-content/uploads/2004/12/tr-2004-136.pdf, cf. https://mcmap.net/q/52964/-file-i-o-with-streams-best-memory-buffer-size)

In action it looks like on that gif.

function Get-FileFromURL {
    [CmdletBinding()]
    param(
        [Parameter(Mandatory, Position = 0)]
        [System.Uri]$URL,
        [Parameter(Mandatory, Position = 1)]
        [string]$Filename
    )

    process {
        try {
            $request = [System.Net.HttpWebRequest]::Create($URL)
            $request.set_Timeout(5000) # 5 second timeout
            $response = $request.GetResponse()
            $total_bytes = $response.ContentLength
            $response_stream = $response.GetResponseStream()

            try {
                # 256KB works better on my machine for 1GB and 10GB files
                # See https://www.microsoft.com/en-us/research/wp-content/uploads/2004/12/tr-2004-136.pdf
                # Cf. https://mcmap.net/q/52964/-file-i-o-with-streams-best-memory-buffer-size
                $buffer = New-Object -TypeName byte[] -ArgumentList 256KB
                $target_stream = [System.IO.File]::Create($Filename)

                $timer = New-Object -TypeName timers.timer
                $timer.Interval = 1000 # Update progress every second
                $timer_event = Register-ObjectEvent -InputObject $timer -EventName Elapsed -Action {
                    $Global:update_progress = $true
                }
                $timer.Start()

                do {
                    $count = $response_stream.Read($buffer, 0, $buffer.length)
                    $target_stream.Write($buffer, 0, $count)
                    $downloaded_bytes = $downloaded_bytes + $count

                    if ($Global:update_progress) {
                        $percent = $downloaded_bytes / $total_bytes
                        $status = @{
                            completed  = "{0,6:p2} Completed" -f $percent
                            downloaded = "{0:n0} MB of {1:n0} MB" -f ($downloaded_bytes / 1MB), ($total_bytes / 1MB)
                            speed      = "{0,7:n0} KB/s" -f (($downloaded_bytes - $prev_downloaded_bytes) / 1KB)
                            eta        = "eta {0:hh\:mm\:ss}" -f (New-TimeSpan -Seconds (($total_bytes - $downloaded_bytes) / ($downloaded_bytes - $prev_downloaded_bytes)))
                        }
                        $progress_args = @{
                            Activity        = "Downloading $URL"
                            Status          = "$($status.completed) ($($status.downloaded)) $($status.speed) $($status.eta)"
                            PercentComplete = $percent * 100
                        }
                        Write-Progress @progress_args

                        $prev_downloaded_bytes = $downloaded_bytes
                        $Global:update_progress = $false
                    }
                } while ($count -gt 0)
            }
            finally {
                if ($timer) { $timer.Stop() }
                if ($timer_event) { Unregister-Event -SubscriptionId $timer_event.Id }
                if ($target_stream) { $target_stream.Dispose() }
                # If file exists and $count is not zero or $null, than script was interrupted by user
                if ((Test-Path $Filename) -and $count) { Remove-Item -Path $Filename }
            }
        }
        finally {
            if ($response) { $response.Dispose() }
            if ($response_stream) { $response_stream.Dispose() }
        }
    }
}
Rettke answered 9/1, 2019 at 2:43 Comment(0)
D
3

Not sure where I've got this piece of code from originally but I've modified it several times. Hope this will help you.

function downloadFile($url, $targetFile)
{
    "Downloading $url"
    $uri = New-Object "System.Uri" "$url"
    $request = [System.Net.HttpWebRequest]::Create($uri)
    $request.set_Timeout(15000) #15 second timeout
    $response = $request.GetResponse()
    $totalLength = [System.Math]::Floor($response.get_ContentLength()/1024)
    $responseStream = $response.GetResponseStream()
    $targetStream = New-Object -TypeName System.IO.FileStream -ArgumentList $targetFile, Create
    $buffer = new-object byte[] 10KB
    $count = $responseStream.Read($buffer,0,$buffer.length)
    $downloadedBytes = $count
    while ($count -gt 0)
    {
        [System.Console]::CursorLeft = 0
        [System.Console]::Write("Downloaded {0}K of {1}K", [System.Math]::Floor($downloadedBytes/1024), $totalLength)
        $targetStream.Write($buffer, 0, $count)
        $count = $responseStream.Read($buffer,0,$buffer.length)
        $downloadedBytes = $downloadedBytes + $count
    }
    "Finished Download"
    $targetStream.Flush()
    $targetStream.Close()
    $targetStream.Dispose()
    $responseStream.Dispose()
}

downloadFile "http://URL_to_your_file" "C:\Path\to\destination.file"
Dorothadorothea answered 19/10, 2017 at 13:18 Comment(1)
Can user cancel this by Ctrl+C?Whitacre

© 2022 - 2024 — McMap. All rights reserved.