Private/DownloadFilesInParallel.ps1
function DownloadFilesInParallel { [CmdletBinding()] param ( [Parameter(Mandatory)] [System.Collections.ArrayList] $FileInfoList, [Parameter(Mandatory)] [string] $OutputPath, [Parameter()] [int] $MaxSimultaneousTransfers = 4 ) $ENDED_BITSJOB_STATES = @("Suspended", "Error", "TransientError", "Transferred", "Canceled") $ENDED_JOB_STATES = @("Blocked", "Completed", "Disconnected", "Failed", "Stopped", "Suspended") try { $startTime = Get-Date $minutesTaken = 0 $currentDownloadNumber = 0 $downloadsFinished = 0 $totalUpdateFiles = $FileInfoList.Count $activeTransfers = New-Object -TypeName "System.Collections.ArrayList" $filesLeft = New-Object -TypeName "System.Collections.Queue" $FileInfoList | ForEach-Object { $filesLeft.Enqueue($_) } while ($filesLeft.Count -gt 0 -or $activeTransfers.Count -gt 0) { $elapsedTime = (Get-Date) - $startTime $minutesTakenNew = [int]([Math]::Floor($elapsedTime.TotalMinutes)) if ($minutesTakenNew -gt $minutesTaken) { Write-Verbose "Downloading $totalUpdateFiles update file$( if ($totalUpdateFiles -ne 1) { "s" } ), $downloadsFinished update file$( if ($downloadsFinished -ne 1) { "s" } ) downloaded." Write-Verbose "Elapsed time: $minutesTakenNew minute$( if ($minutesTakenNew -ne 1) { "s" } )." $minutesTaken = $minutesTakenNew } # Start another transfer if we're below the max simultaneous transfers threshold if ($filesLeft.Count -gt 0 -and $activeTransfers.Count -lt $MaxSimultaneousTransfers) { $currentDownloadNumber += 1 $downloadProgress = "$currentDownloadNumber/$totalUpdateFiles" $parsedFileInfo = $filesLeft.Dequeue() $fileDownloadUri = $parsedFileInfo["Uri"] $fileName = $parsedFileInfo["FileName"] $fileDownloadPath = Join-Path $OutputPath $fileName -ErrorAction Stop $fileDownloadPathTemp = "$fileDownloadPath.tmp" if (Test-Path $fileDownloadPath) { Write-Verbose "Skipping already downloaded update file $fileName ($downloadProgress)." continue } if (Test-Path $fileDownloadPathTemp) { Write-Verbose "Restarting download for incomplete update file $fileName ($downloadProgress)..." try { Remove-Item -Path $fileDownloadPathTemp -ErrorAction Stop } catch { Write-Error -Message "Could not remove incomplete update file ($fileDownloadPathTemp). Please remove this file manually and try again." return } } else { Write-Output "Downloading update file $fileName ($downloadProgress)..." } try { $transferParams = GetBitsTransferSplatBase -Source $fileDownloadUri $newBitsJob = Start-BitsTransfer @transferParams ` -DisplayName "Import-WsusUpdate parallel download for $fileName ($downloadProgress)" ` -Destination $fileDownloadPathTemp ` -Asynchronous ` -ErrorAction Stop [void]($activeTransfers.Add(@{ "FileName" = $fileName "TempPath" = $fileDownloadPathTemp "Path" = $fileDownloadPath "BitsJob" = $newBitsJob })) # Immediately restart the loop to queue up another job asap. continue } catch { Write-Warning "BITS transfer for update file $fileName failed with the following error: $_" Write-Verbose "Full error info:" Write-Verbose ($_ | Format-List -Force | Out-String) Write-Warning "Retrying download once more with Invoke-WebRequest as a fallback..." try { $webRequestParams = GetWebRequestSplatBase -Uri $fileDownloadUri $webRequestParams["OutFile"] = $fileDownloadPathTemp # TODO: Deserialization in job prevents WebSession from working, although this likely won't matter right now. # "Cannot convert the "Microsoft.PowerShell.Commands.WebRequestSession" value of type "Deserialized.Microsoft.PowerShell.Commands.WebRequestSession" to type "Microsoft.PowerShell.Commands.WebRequestSession". $webRequestParams["SessionVariable"] = $null $webRequestParams["WebSession"] = $null $job = Start-Job -Name "Import-WsusUpdate parallel download fallback for $fileName ($downloadProgress)" -ErrorAction Stop -ScriptBlock { [CmdletBinding()] param () # Hide progress since this is a parallel background download and it will speed up # Invoke-WebRequest. $ProgressPreference = "SilentlyContinue" $webRequestParams = $using:webRequestParams [void](Invoke-WebRequest @webRequestParams -ErrorAction Stop) } [void]($activeTransfers.Add(@{ "FileName" = $fileName "TempPath" = $fileDownloadPathTemp "Path" = $fileDownloadPath "WebRequestJob" = $job })) } catch { Write-Warning "Failed to start fallback download for update file $fileName (URI: $fileDownloadUri) with the following error: $_" throw } } } # We don't need to start another job - track progress of our current transfers if ($activeTransfers.Count -gt 0) { $transferJustFinished = $false for ($index = 0; $index -lt $activeTransfers.Count; $index += 1) { $activeTransfer = $activeTransfers[$index] if ($null -ne $activeTransfer["BitsJob"]) { $updateBitsJob = Get-BitsTransfer -JobId $activeTransfer["BitsJob"].JobId -ErrorAction Stop if ($updateBitsJob.JobState -notin $ENDED_BITSJOB_STATES) { continue } if ($updateBitsJob.JobState -ne "Transferred") { Write-Error -Message "Download for update file $( $activeTransfer["FileName"] ) ended unfinished with state $( $updateBitsJob.JobState )." ` -Category OperationStopped ` -ErrorId "ParallelBITSTransferFailed" return } Complete-BitsTransfer -BitsJob $activeTransfer["BitsJob"] -ErrorAction Stop } elseif ($null -ne $activeTransfer["WebRequestJob"]) { $jobState = $activeTransfer["WebRequestJob"] if ($jobState.State -notin $ENDED_JOB_STATES) { continue } if ($jobState.State -ne "Completed") { Write-Error -Message "Fallback download for update file $( $activeTransfer["FileName"] ) ended unfinished with state $( $jobState.State )." ` -Category OperationStopped ` -ErrorId "ParallelInvokeWebRequestTransferFailed" return } Remove-Job -Job $activeTransfer["WebRequestJob"] -ErrorAction SilentlyContinue -Confirm:$false } else { throw "Unexpected error: BitsJob and WebRequestJob were both null." } Write-Verbose "Finished downloading update file $( $activeTransfer["FileName"] )..." Write-Verbose "Moving finished download to its proper path $( $activeTransfer["Path"] )." Move-Item -Path $activeTransfer["TempPath"] -Destination $activeTransfer["Path"] -ErrorAction Stop $downloadsFinished += 1 $activeTransfers.RemoveAt($index) $index -= 1 $transferJustFinished = $true } if ($transferJustFinished) { # Skip sleep and start new job immediately continue } } # Check for progress updates every so often Start-Sleep -Milliseconds 500 } } finally { if ($activeTransfers.Count -gt 0) { Write-Verbose "Cleaning up $( $activeTransfers.Count ) active transfer$( if ( $activeTransfers.Count -gt 1 ) { "s" } )." while ($activeTransfers.Count -gt 0) { if ($null -ne $activeTransfers[0]["BitsJob"]) { Remove-BitsTransfer -BitsJob $activeTransfers[0]["BitsJob"] -ErrorAction SilentlyContinue } elseif ($null -ne $activeTransfers[0]["WebRequestJob"]) { [void](Stop-Job -Job $activeTransfers[0]["WebRequestJob"] -ErrorAction SilentlyContinue -Confirm:$false) Remove-Job -Job $activeTransfers[0]["WebRequestJob"] -ErrorAction SilentlyContinue -Confirm:$false } else { Write-Verbose "Skipping removal of lingering transfer as both BitsJob and WebRequestJob were null." } $activeTransfers.RemoveAt(0) } } } } # Copyright (c) 2023 AJ Tek Corporation. All Rights Reserved. # SIG # Begin signature block # MIIVmwYJKoZIhvcNAQcCoIIVjDCCFYgCAQExCzAJBgUrDgMCGgUAMGkGCisGAQQB # gjcCAQSgWzBZMDQGCisGAQQBgjcCAR4wJgIDAQAABBAfzDtgWUsITrck0sYpfvNR # AgEAAgEAAgEAAgEAAgEAMCEwCQYFKw4DAhoFAAQUt37p4lL9/kDyRPMwNgjqlJQ7 # US+gghH7MIIFbzCCBFegAwIBAgIQSPyTtGBVlI02p8mKidaUFjANBgkqhkiG9w0B # AQwFADB7MQswCQYDVQQGEwJHQjEbMBkGA1UECAwSR3JlYXRlciBNYW5jaGVzdGVy # MRAwDgYDVQQHDAdTYWxmb3JkMRowGAYDVQQKDBFDb21vZG8gQ0EgTGltaXRlZDEh # MB8GA1UEAwwYQUFBIENlcnRpZmljYXRlIFNlcnZpY2VzMB4XDTIxMDUyNTAwMDAw # MFoXDTI4MTIzMTIzNTk1OVowVjELMAkGA1UEBhMCR0IxGDAWBgNVBAoTD1NlY3Rp # Z28gTGltaXRlZDEtMCsGA1UEAxMkU2VjdGlnbyBQdWJsaWMgQ29kZSBTaWduaW5n # IFJvb3QgUjQ2MIICIjANBgkqhkiG9w0BAQEFAAOCAg8AMIICCgKCAgEAjeeUEiIE # JHQu/xYjApKKtq42haxH1CORKz7cfeIxoFFvrISR41KKteKW3tCHYySJiv/vEpM7 # fbu2ir29BX8nm2tl06UMabG8STma8W1uquSggyfamg0rUOlLW7O4ZDakfko9qXGr # YbNzszwLDO/bM1flvjQ345cbXf0fEj2CA3bm+z9m0pQxafptszSswXp43JJQ8mTH # qi0Eq8Nq6uAvp6fcbtfo/9ohq0C/ue4NnsbZnpnvxt4fqQx2sycgoda6/YDnAdLv # 64IplXCN/7sVz/7RDzaiLk8ykHRGa0c1E3cFM09jLrgt4b9lpwRrGNhx+swI8m2J # mRCxrds+LOSqGLDGBwF1Z95t6WNjHjZ/aYm+qkU+blpfj6Fby50whjDoA7NAxg0P # OM1nqFOI+rgwZfpvx+cdsYN0aT6sxGg7seZnM5q2COCABUhA7vaCZEao9XOwBpXy # bGWfv1VbHJxXGsd4RnxwqpQbghesh+m2yQ6BHEDWFhcp/FycGCvqRfXvvdVnTyhe # Be6QTHrnxvTQ/PrNPjJGEyA2igTqt6oHRpwNkzoJZplYXCmjuQymMDg80EY2NXyc # uu7D1fkKdvp+BRtAypI16dV60bV/AK6pkKrFfwGcELEW/MxuGNxvYv6mUKe4e7id # FT/+IAx1yCJaE5UZkADpGtXChvHjjuxf9OUCAwEAAaOCARIwggEOMB8GA1UdIwQY # MBaAFKARCiM+lvEH7OKvKe+CpX/QMKS0MB0GA1UdDgQWBBQy65Ka/zWWSC8oQEJw # IDaRXBeF5jAOBgNVHQ8BAf8EBAMCAYYwDwYDVR0TAQH/BAUwAwEB/zATBgNVHSUE # DDAKBggrBgEFBQcDAzAbBgNVHSAEFDASMAYGBFUdIAAwCAYGZ4EMAQQBMEMGA1Ud # HwQ8MDowOKA2oDSGMmh0dHA6Ly9jcmwuY29tb2RvY2EuY29tL0FBQUNlcnRpZmlj # YXRlU2VydmljZXMuY3JsMDQGCCsGAQUFBwEBBCgwJjAkBggrBgEFBQcwAYYYaHR0 # cDovL29jc3AuY29tb2RvY2EuY29tMA0GCSqGSIb3DQEBDAUAA4IBAQASv6Hvi3Sa # mES4aUa1qyQKDKSKZ7g6gb9Fin1SB6iNH04hhTmja14tIIa/ELiueTtTzbT72ES+ # BtlcY2fUQBaHRIZyKtYyFfUSg8L54V0RQGf2QidyxSPiAjgaTCDi2wH3zUZPJqJ8 # ZsBRNraJAlTH/Fj7bADu/pimLpWhDFMpH2/YGaZPnvesCepdgsaLr4CnvYFIUoQx # 2jLsFeSmTD1sOXPUC4U5IOCFGmjhp0g4qdE2JXfBjRkWxYhMZn0vY86Y6GnfrDyo # XZ3JHFuu2PMvdM+4fvbXg50RlmKarkUT2n/cR/vfw1Kf5gZV6Z2M8jpiUbzsJA8p # 1FiAhORFe1rYMIIGGjCCBAKgAwIBAgIQYh1tDFIBnjuQeRUgiSEcCjANBgkqhkiG # 9w0BAQwFADBWMQswCQYDVQQGEwJHQjEYMBYGA1UEChMPU2VjdGlnbyBMaW1pdGVk # MS0wKwYDVQQDEyRTZWN0aWdvIFB1YmxpYyBDb2RlIFNpZ25pbmcgUm9vdCBSNDYw # HhcNMjEwMzIyMDAwMDAwWhcNMzYwMzIxMjM1OTU5WjBUMQswCQYDVQQGEwJHQjEY # MBYGA1UEChMPU2VjdGlnbyBMaW1pdGVkMSswKQYDVQQDEyJTZWN0aWdvIFB1Ymxp # YyBDb2RlIFNpZ25pbmcgQ0EgUjM2MIIBojANBgkqhkiG9w0BAQEFAAOCAY8AMIIB # igKCAYEAmyudU/o1P45gBkNqwM/1f/bIU1MYyM7TbH78WAeVF3llMwsRHgBGRmxD # eEDIArCS2VCoVk4Y/8j6stIkmYV5Gej4NgNjVQ4BYoDjGMwdjioXan1hlaGFt4Wk # 9vT0k2oWJMJjL9G//N523hAm4jF4UjrW2pvv9+hdPX8tbbAfI3v0VdJiJPFy/7Xw # iunD7mBxNtecM6ytIdUlh08T2z7mJEXZD9OWcJkZk5wDuf2q52PN43jc4T9OkoXZ # 0arWZVeffvMr/iiIROSCzKoDmWABDRzV/UiQ5vqsaeFaqQdzFf4ed8peNWh1OaZX # nYvZQgWx/SXiJDRSAolRzZEZquE6cbcH747FHncs/Kzcn0Ccv2jrOW+LPmnOyB+t # AfiWu01TPhCr9VrkxsHC5qFNxaThTG5j4/Kc+ODD2dX/fmBECELcvzUHf9shoFvr # n35XGf2RPaNTO2uSZ6n9otv7jElspkfK9qEATHZcodp+R4q2OIypxR//YEb3fkDn # 3UayWW9bAgMBAAGjggFkMIIBYDAfBgNVHSMEGDAWgBQy65Ka/zWWSC8oQEJwIDaR # XBeF5jAdBgNVHQ4EFgQUDyrLIIcouOxvSK4rVKYpqhekzQwwDgYDVR0PAQH/BAQD # AgGGMBIGA1UdEwEB/wQIMAYBAf8CAQAwEwYDVR0lBAwwCgYIKwYBBQUHAwMwGwYD # VR0gBBQwEjAGBgRVHSAAMAgGBmeBDAEEATBLBgNVHR8ERDBCMECgPqA8hjpodHRw # Oi8vY3JsLnNlY3RpZ28uY29tL1NlY3RpZ29QdWJsaWNDb2RlU2lnbmluZ1Jvb3RS # NDYuY3JsMHsGCCsGAQUFBwEBBG8wbTBGBggrBgEFBQcwAoY6aHR0cDovL2NydC5z # ZWN0aWdvLmNvbS9TZWN0aWdvUHVibGljQ29kZVNpZ25pbmdSb290UjQ2LnA3YzAj # BggrBgEFBQcwAYYXaHR0cDovL29jc3Auc2VjdGlnby5jb20wDQYJKoZIhvcNAQEM # BQADggIBAAb/guF3YzZue6EVIJsT/wT+mHVEYcNWlXHRkT+FoetAQLHI1uBy/YXK # ZDk8+Y1LoNqHrp22AKMGxQtgCivnDHFyAQ9GXTmlk7MjcgQbDCx6mn7yIawsppWk # vfPkKaAQsiqaT9DnMWBHVNIabGqgQSGTrQWo43MOfsPynhbz2Hyxf5XWKZpRvr3d # MapandPfYgoZ8iDL2OR3sYztgJrbG6VZ9DoTXFm1g0Rf97Aaen1l4c+w3DC+IkwF # kvjFV3jS49ZSc4lShKK6BrPTJYs4NG1DGzmpToTnwoqZ8fAmi2XlZnuchC4NPSZa # PATHvNIzt+z1PHo35D/f7j2pO1S8BCysQDHCbM5Mnomnq5aYcKCsdbh0czchOm8b # kinLrYrKpii+Tk7pwL7TjRKLXkomm5D1Umds++pip8wH2cQpf93at3VDcOK4N7Ew # oIJB0kak6pSzEu4I64U6gZs7tS/dGNSljf2OSSnRr7KWzq03zl8l75jy+hOds9TW # SenLbjBQUGR96cFr6lEUfAIEHVC1L68Y1GGxx4/eRI82ut83axHMViw1+sVpbPxg # 51Tbnio1lB93079WPFnYaOvfGAA0e0zcfF/M9gXr+korwQTh2Prqooq2bYNMvUoU # KD85gnJ+t0smrWrb8dee2CvYZXD5laGtaAxOfy/VKNmwuWuAh9kcMIIGZjCCBM6g # AwIBAgIRAMyLU7NDPs1zQXTEeYo/k2YwDQYJKoZIhvcNAQEMBQAwVDELMAkGA1UE # BhMCR0IxGDAWBgNVBAoTD1NlY3RpZ28gTGltaXRlZDErMCkGA1UEAxMiU2VjdGln # byBQdWJsaWMgQ29kZSBTaWduaW5nIENBIFIzNjAeFw0yMjA2MzAwMDAwMDBaFw0y # NTA2MjkyMzU5NTlaMFkxCzAJBgNVBAYTAkNBMRAwDgYDVQQIDAdPbnRhcmlvMRsw # GQYDVQQKDBJBSiBUZWsgQ29ycG9yYXRpb24xGzAZBgNVBAMMEkFKIFRlayBDb3Jw # b3JhdGlvbjCCAiIwDQYJKoZIhvcNAQEBBQADggIPADCCAgoCggIBAOLV0fXklSI7 # zs4Awbc5rzQGG/XJaEgZLEXFVvzqKWlApLYSdXMLTzW14/IEoVMmi+qunmgIF98c # 3gUFm6dTSkf+ccSIWgHwjD/pPMX4leIO1Qen+OBup4p33XRNqHsWLzBqlWmKRcLW # 5cyRDRXedgZDvZozhxTqLhk0tFdTcBoGULA13Z/TfotbwNCx05W304VgluUogNX2 # 4yv80QYmHa6667ewXuAPNiPWQgFE4fyXOvFsRUFEtllRZmUpOPqZtGURw2ZYd+cZ # UFn4QrC9DzlESFEjQzMMF5iUjrKCI25jq53MkTZHnOjO9KGH5SvcLo5CbgNptpzr # d1P7AsoHFedvErNbp6YOhKrkv7F0ksVgqBh1v2Yc1ShpzZBEJ6pL+QsdnWzl6Wzb # B8xEYEdbNF0A0/kyFHm2tYSxxCsVO4caoL2CcsA3q/1h73MBWEcNT2fV6o25L+LY # sNUkFRc6ZRqOQ/ub9na8jCOwuhxi8MXTGo22Crinp5bbzYA/liNm97G77mJe59za # aaoulTWzk5y06k7VgLDYtR7IgZ47Aytnj/jr7S5/P/1F2K3lbRUdGyKdifubSgjk # m1gubK0cm3Y6L7Ar2X1nBm+PpAZqA54jQBs9cV03MVDjZb3AvjooCi9uOCJ4998U # TV9Sn2873/GUXE6dRB+w0fD/FcUxX2pjAgMBAAGjggGsMIIBqDAfBgNVHSMEGDAW # gBQPKssghyi47G9IritUpimqF6TNDDAdBgNVHQ4EFgQU0JE9KqcvG1A1pTeA+vcd # t1flqK0wDgYDVR0PAQH/BAQDAgeAMAwGA1UdEwEB/wQCMAAwEwYDVR0lBAwwCgYI # KwYBBQUHAwMwSgYDVR0gBEMwQTA1BgwrBgEEAbIxAQIBAwIwJTAjBggrBgEFBQcC # ARYXaHR0cHM6Ly9zZWN0aWdvLmNvbS9DUFMwCAYGZ4EMAQQBMEkGA1UdHwRCMEAw # PqA8oDqGOGh0dHA6Ly9jcmwuc2VjdGlnby5jb20vU2VjdGlnb1B1YmxpY0NvZGVT # aWduaW5nQ0FSMzYuY3JsMHkGCCsGAQUFBwEBBG0wazBEBggrBgEFBQcwAoY4aHR0 # cDovL2NydC5zZWN0aWdvLmNvbS9TZWN0aWdvUHVibGljQ29kZVNpZ25pbmdDQVIz # Ni5jcnQwIwYIKwYBBQUHMAGGF2h0dHA6Ly9vY3NwLnNlY3RpZ28uY29tMCEGA1Ud # EQQaMBiBFmFqLmFjY291bnRpbmdAYWp0ZWsuY2EwDQYJKoZIhvcNAQEMBQADggGB # ACa/QJzMPVDohIgQtL4/Yr7z7DlaybhjLQ2gEx7pU6G/hNQ+kClqLR3hykkJ7fwZ # 4cYX1TF4JWnkmQ0GZwj5bk5RaSyDatQVVIW8AQNMIEEhUftHJn1REz16CzXxlgzd # d+GTqYmwommBa6DlFO88fwF0FL3KJgvNguQGSU9sGIGUWyQuxqFUwXRgsXpwQbpR # 1H6qsViLN1SGPXO+iqUSyejmd9mIbc8b0IxuR6rDtU2PIcU8XzwsJv8M/L1paQut # 4m7dNw54gsoRVp+KJWawkkEVM6xvszCo8VIk8ZGetRCBT+ZSunIb/LFripb++lR8 # tIBkq8zYEhEp9U8GQZbKNZzfelix4kRt5wUq39rpBU8aHoU4GRXFs571jb/qBz/x # AVckN5cosppJxe+AW/TR9qKrL8uKOJ9cCLRXjPdLGSmHA5XMN6ecgRE+yfLMOauX # pUo33dCGh36TZuSi7P4uz2trEEUfmaQlr/TEp0xtbgUxTopdUYh9xagN4bbkRmoB # izGCAwowggMGAgEBMGkwVDELMAkGA1UEBhMCR0IxGDAWBgNVBAoTD1NlY3RpZ28g # TGltaXRlZDErMCkGA1UEAxMiU2VjdGlnbyBQdWJsaWMgQ29kZSBTaWduaW5nIENB # IFIzNgIRAMyLU7NDPs1zQXTEeYo/k2YwCQYFKw4DAhoFAKB4MBgGCisGAQQBgjcC # AQwxCjAIoAKAAKECgAAwGQYJKoZIhvcNAQkDMQwGCisGAQQBgjcCAQQwHAYKKwYB # BAGCNwIBCzEOMAwGCisGAQQBgjcCARUwIwYJKoZIhvcNAQkEMRYEFBNM9/sctfox # XVBr7rXytq6T9F6RMA0GCSqGSIb3DQEBAQUABIICAHcBkMkqauTz25rog6lwV0ck # qYBUXvJhs4PFmMyJF9YaCw8FzQv5zdqpO3md0mq+kLnM3kTBMxIFVuIpTgK83U/t # OUaNPghS1ZoEl0hOHCOQcPSTqChWTkaIQ8dHXxm0pDlVGPO0g03X6qz9QXJd+QoK # k5wSWbn1McK6mf31KFRS+MtcROoEdA7oA/61ZjOOmGLcRmgwAIhicIva+TZg4/t0 # zwpzDsrvJTYwCjJzKQ/6xH/+x0x0jfKVOQ0FNv58w8hl7IGJj8hIG/yTmL59+Hez # WP7acO2k40xGt8/AXbioDSnMelH0ToYGIJT8zrQ9nTtvcpQ9aBSM0zjdqW2DbiF5 # gpxO82XSPgbup51+ig8QUU7IaFwREOFnj//HfHcF6HbjKBbonrNutjd9s6bdN+Ci # YzyMNxNlBUQBZ3GTmEcIIrVCbJBNlZ4h4P1QXlmopLUURprjZ3OaBayLVsq/FbiJ # NOqMyNdoLbsPDFOpocIx/qIUvnUz1SOpMcm+WU1A3CzCDP2Cj2I+zop/68ggshDQ # e9ikVoHq43yLBdOuU9Y1Q2NpVkNq8SFr9QB6gq5pbJOi+4Luk6qu2pjGMVcFuHWz # PrN3mj72lDw/XkyV4Pyg8qmprnCgGiggKd9ZQTVNKMVccPt09jPb0AtChehoVRb8 # D3AnV31etSWefG0lxcCS # SIG # End signature block |