Invoke-DownloadFile.ps1


<#PSScriptInfo

.VERSION 19.05.24

.GUID d1de0de3-d0f2-4094-a2f2-f75cd8d0fab3

.AUTHOR Tim Small

.COMPANYNAME

.COPYRIGHT 2019

.TAGS web download webclient

.LICENSEURI

.PROJECTURI https://github.com/Smalls1652/Get-RandomPowerShellScripts/blob/master/Actions/Networking/Invoke-DownloadFile.ps1

.ICONURI

.EXTERNALMODULEDEPENDENCIES

.REQUIREDSCRIPTS

.EXTERNALSCRIPTDEPENDENCIES

.RELEASENOTES
Fixed current dir paths

.PRIVATEDATA

#>
 





<#

.DESCRIPTION
 Download files from the internet through PowerShell.

#>


[CmdletBinding(SupportsShouldProcess)]
param(
    [Parameter(Mandatory = $true)][string]$Url,
    [string]$Path
)

function convertFileSize {
    param(
        $bytes
    )

    if ($bytes -lt 1MB) {
        return "$([Math]::Round($bytes / 1KB, 2)) KB"
    }
    elseif ($bytes -lt 1GB) {
        return "$([Math]::Round($bytes / 1MB, 2)) MB"
    }
    elseif ($bytes -lt 1TB) {
        return "$([Math]::Round($bytes / 1GB, 2)) GB"
    }
}

Write-Verbose "URL set to ""$($Url)""."

switch ($Path) {
    { ([regex]::Match($PSItem, "(?:(?:.+[\/\\]){2,}(?:(?'fileName'.+)|)|.+)").Groups | Where-Object -Property "Name" -eq "fileName" | Select-Object -ExpandProperty "Success") -eq $false } {
        Write-Verbose "Path parameter set, but no filename. Parsing Url for filename."
        if (!([regex]::Match($Path, "^.+[\/\\]$") | Select-Object -ExpandProperty "Success")) {
            if ($Path.ToCharArray() -contains "/") {
                $Path = "$($Path)/"
            }
            elseif ($Path.ToCharArray() -contains "\") {
                $Path = "$($Path)\"
            }
        }
        $URLParser = $Url | Select-String -Pattern ".*\:\/\/.*\/(.*\.{1}\w*).*" -List

        $Path = "$($Path)$($URLParser.Matches.Groups[1].Value)"
    }
    Default {
        Write-Verbose "Path parameter not set, parsing Url for filename."
        $URLParser = $Url | Select-String -Pattern ".*\:\/\/.*\/(.*\.{1}\w*).*" -List

        $Path = "./$($URLParser.Matches.Groups[1].Value)"
    }
}

Write-Verbose "Path set to ""$($Path)""."

#Load in the WebClient object.
Write-Verbose "Loading in WebClient object."
try {
    $Downloader = New-Object -TypeName System.Net.WebClient
}
catch [Exception] {
    Write-Error $_ -ErrorAction Stop
}

#Creating a temporary file.
$TmpFile = New-TemporaryFile
Write-Verbose "TmpFile set to ""$($TmpFile)""."

try {

    #Start the download by using WebClient.DownloadFileTaskAsync, since this lets us show progress on screen.
    Write-Verbose "Starting download..."
    $FileDownload = $Downloader.DownloadFileTaskAsync($Url, $TmpFile)

    #Register the event from WebClient.DownloadProgressChanged to monitor download progress.
    Write-Verbose "Registering the ""DownloadProgressChanged"" event handle from the WebClient object."
    Register-ObjectEvent -InputObject $Downloader -EventName DownloadProgressChanged -SourceIdentifier WebClient.DownloadProgressChanged | Out-Null

    #Wait two seconds for the registration to fully complete
    Start-Sleep -Seconds 3

    if ($FileDownload.IsFaulted) {
        Write-Verbose "An error occurred. Generating error."
        Write-Error $FileDownload.GetAwaiter().GetResult()
        break
    }

    #While the download is showing as not complete, we keep looping to get event data.
    while (!($FileDownload.IsCompleted)) {

        if ($FileDownload.IsFaulted) {
            Write-Verbose "An error occurred. Generating error."
            Write-Error $FileDownload.GetAwaiter().GetResult()
            break
        }

        $EventData = Get-Event -SourceIdentifier WebClient.DownloadProgressChanged | Select-Object -ExpandProperty "SourceEventArgs" -Last 1

        $ReceivedData = ($EventData | Select-Object -ExpandProperty "BytesReceived")
        $TotalToReceive = ($EventData | Select-Object -ExpandProperty "TotalBytesToReceive")
        $TotalPercent = $EventData | Select-Object -ExpandProperty "ProgressPercentage"

        Write-Progress -Activity "Downloading File" -Status "Percent Complete: $($TotalPercent)%" -CurrentOperation "Downloaded $(convertFileSize -bytes $ReceivedData) / $(convertFileSize -bytes $TotalToReceive)" -PercentComplete $TotalPercent
    }
}
catch [Exception] {
    $ErrorDetails = $_

    switch ($ErrorDetails.FullyQualifiedErrorId) {
        "ArgumentNullException" { 
            Write-Error -Exception "ArgumentNullException" -ErrorId "ArgumentNullException" -Message "Either the Url or Path is null." -Category InvalidArgument -TargetObject $Downloader -ErrorAction Stop
        }
        "WebException" {
            Write-Error -Exception "WebException" -ErrorId "WebException" -Message "An error occurred while downloading the resource." -Category OperationTimeout -TargetObject $Downloader -ErrorAction Stop
        }
        "InvalidOperationException" {
            Write-Error -Exception "InvalidOperationException" -ErrorId "InvalidOperationException" -Message "The file at ""$($Path)"" is in use by another process." -Category WriteError -TargetObject $Path -ErrorAction Stop
        }
        Default {
            Write-Error $ErrorDetails -ErrorAction Stop
        }
    }
}
finally {
    #Cleanup tasks
    Write-Verbose "Cleaning up..."
    Write-Progress -Activity "Downloading File" -Completed
    Unregister-Event -SourceIdentifier WebClient.DownloadProgressChanged

    if (($FileDownload.IsCompleted) -and !($FileDownload.IsFaulted)) {
        #If the download was finished without termination, then we move the file.
        Write-Verbose "Moved the downloaded file to ""$($Path)""."
        Move-Item -Path $TmpFile -Destination $Path -Force
    }
    else {
        #If the download was terminated, we remove the file.
        Write-Verbose "Cancelling the download and removing the tmp file."
        $Downloader.CancelAsync()
        Remove-Item -Path $TmpFile -Force
    }

    $Downloader.Dispose()
}