Invoke-UploadInstaller.ps1
<#
.SYNOPSIS This script uploads the installer file to be converted. .DESCRIPTION This script reads the package path, package version, package install arguments and package name and invokes upload MSIX REST APIs. This script divide the package file into multiple blocks, these blocks will be stored in a temporary directory under the input package path directory .PARAMETER packagePath Specifies a path to a location of the installer file. The value of packagePath is used exactly as it is typed. No characters are interpreted as wildcards. If the path includes escape characters, enclose it in single quotation marks. Single quotation marks tell Windows PowerShell not to interpret any characters as escape sequences. .PARAMETER packageVersion Specifies the version of the installer .PARAMETER publisherName Specifies the name of the publisher of the installer .PARAMETER packageInstallArguments Specifies the install arguments of the installer .PARAMETER packageName Specifies the package name of the installer .PARAMETER IsUnattendedInstallWithoutArgument Specifies the boolean attribute which supports unattended install .PARAMETER Env Optional parameter of the environment value. #> # Uncomment for debugging # Set-PSDebug -Trace 2 # Function to display the progress bar function Write-ProgressHelper { param ( [Parameter(Mandatory, HelpMessage = "Please provide the step weightage.")] [int]$Weightage, [Parameter(Mandatory, HelpMessage = "Please provide the message to be dipsplayed")] [string]$Message ) $PSStyle.Progress.MaxWidth = 120 Write-Progress -Activity 'Upload Status :- ' -Status $Message -PercentComplete (($Weightage / 16) * 100) } #number of times to retry before suspending upload operation $noOfRetries = 3 function Write-To-LogFile { [CmdletBinding()] Param( [Parameter(Mandatory, HelpMessage = "Please provide the log statement.")] [string] $LogString, [Parameter(HelpMessage = "Please provide the log level.")] [ValidateSet("INFO","WARN","ERROR","FATAL","DEBUG", ErrorMessage="'{0}' is not a valid level! Please use one of: '{1}'")] [string] $Level = "INFO" ) $Logfile = [System.IO.Path]::GetFileNameWithoutExtension($PackagePath) + ".log" $PathToLog = $("$env:temp\UploadFlowLogs") if ($(Test-Path -Path $PathToLog) -eq $False) { New-Item -Path $PathToLog -ItemType Directory >> $null } $Logfile = $PathToLog + "\" + $Logfile $Stamp = (Get-Date).toString("yyyy-MM-dd HH:mm:ss") $LogMessage = "$Stamp $Level $LogString" Write-Debug $LogMessage -Debug Add-content $LogFile -value $LogMessage } # Function to create the temporary folder path to store all the chunks function Add-Temp_Folder($FullPackagePath, $PackageName, $PackageType) { # Extract Package Directory $PackageDirectoryPath = [System.IO.Path]::GetDirectoryName($FullPackagePath) # Prepare Temporary Directory Name and Path $TempPackageDirectoryName = "${PackageName}${PackageType}_tmp" $PackageTempDirectoryPath = Join-Path -Path $PackageDirectoryPath -ChildPath $TempPackageDirectoryName return $PackageTempDirectoryPath } <# Below function * creates a temporary sub directory under input package directory path * split package into multiple blocks based on BlocksSize. #> function Split-Package-File-Into-Block($PackageTempDirectoryPath) { # Create the temp directory $tmp = New-Item -Path $PackageTempDirectoryPath -ItemType Directory -Force Write-Debug "Created tmp directory for blocks: $tmp" # Open input and read input file $fsobj = [System.IO.File]::OpenRead($FullPackagePath) $ByteCount = $fsobj.Length Write-Debug "Package Size: $ByteCount bytes" # Default No of Bytes in a block. $BlockSize = 1024 * 1024 * 1 $Offset = 0 $BytesRemaining = $ByteCount Write-Debug "BlockSize: $BlockSize" $BlockNumber = 0 while ($BytesRemaining -gt 0) { $BlockNumber += 1 $DataToRead = [System.Math]::Min($BytesRemaining, $BlockSize) $DataByte = New-Object -TypeName byte[] -ArgumentList $DataToRead $DataRead = $fsobj.Read($DataByte, $Offset, $DataToRead) $BytesRemaining -= $DataRead if ($DataRead -gt 0) { [System.IO.File]::WriteAllBytes("${PackageTempDirectoryPath}/${BlockNumber}.tmp", $DataByte) } else { Write-Error "No data to write!" } } Write-Debug "File split into $BlockNumber blocks done!" $global:BlocksCount = $BlockNumber $global:PackageTempDirectoryPath = $PackageTempDirectoryPath } # function to initiate package upload function Push-Package ($PackageName, $PackageType, $PackageVersion, $PackageInstallArguments, $PublisherName, $IsUnattendedInstallWithoutArgument) { # REST API invocation $InitUrl = $global:devRestEndPoint + "upload/v1/init/" $header = @{ authorization=$Token } $body = @{ packageName=$PackageName packageVersion=$PackageVersion packageExtension=$PackageType publisherName=$PublisherName packageInstallArgs=$PackageInstallArguments totalBlocksCount=$BlocksCount isUnattendedInstallWithoutArgument=$IsUnattendedInstallWithoutArgument } $JsonBody = $body | ConvertTo-Json Try { $Response = Invoke-RestMethod -Method 'POST' -Uri $InitUrl -Body $JsonBody -Header $header -MaximumRedirection 1 $global:conversionId = $Response.conversionId Write-Debug "Conversion id is generated $global:conversionId" } Catch { Write-To-LogFile -LogString "The HTTP error code received is - $($_.Exception.Response.StatusCode.value__)" -Level "ERROR" Write-To-LogFile -LogString "The error received is - $($_.Exception.Response.ReasonPhrase)" -Level "ERROR" throw "Failed upload process" } } <# function to upload package blocks Config: $noOfRetries to control block retry attempt #> function Push-Package-Block() { # array to store block ids $global:blockIds = [String[]]::new($global:BlocksCount) $failed = @($false) # Start multiple threads to parallely upload blocks 1..$global:BlocksCount | ForEach-Object -ThrottleLimit 10 -Parallel { # if one block's upload has already failed multiple times, break all upload operations $status = $using:failed $failed = $status[0] if($failed){ break } # method to save response in array function getResponse($BlockUploadUrl, $JsonBody, $BlockNumber){ $header = @{ authorization=$using:Token } $Response = Invoke-RestMethod -Method 'POST' -Uri $BlockUploadUrl -Body $JsonBody -Header $header -MaximumRedirection 1 $blockIds = $using:blockIds # save block id $blockIds[$($BlockNumber-1)]=$Response.blockId $blockId = $Response.blockId Write-Debug "Block $BlockNumber uploaded with blockId: $blockId" -Debug } # method to initiate block upload function blockUpload($fileContentEncoded, $BlockNumber) { $BlockUploadUrl = $using:global:devRestEndPoint+'upload/v1/block' $noOfRetries = $using:noOfRetries $body = @{ conversionId = $using:conversionId block = @{ BlockNumber=$BlockNumber byteArray=$fileContentEncoded } } $JsonBody = $body | ConvertTo-Json # try to upload block and retry if necessary until noOfRetries is exceeded Try { getResponse -BlockUploadUrl $BlockUploadUrl -JsonBody $JsonBody -BlockNumber $BlockNumber } Catch { Write-Debug "Block $BlockNumber failed. Retrying..." -Debug $i=0; for(;$i -lt $noOfRetries;$i++){ Try { getResponse -BlockUploadUrl $BlockUploadUrl -JsonBody $JsonBody $BlockNumber break } Catch { if($($i+1) -ne $noOfRetries) { Write-Debug "Block $BlockNumber failed $($i+2) times, retrying..." -Debug } else { Write-Error "Retry count exhausted for Block $BlockNumber" } } } if($i -eq $noOfRetries) { $status[0]=$true } } } # get block content and encode it $FileName = $using:PackageTempDirectoryPath+"\"+$_+".tmp" $FileContent = Get-Content $FileName -Raw -AsByteStream $fileContentEncoded = [System.Convert]::ToBase64String($FileContent) # call block upload method blockUpload $fileContentEncoded $_ } Write-Debug "Block upload completed." # if block upload failed if($failed[0]) { Write-Error "Block upload failed" -ForegroundColor Red throw "Failed upload process" } } # function to initiate block merge function Approve-Blocks-Merge ($PackageName) { $MergeUrl = $global:devRestEndPoint + 'upload/v1/merge' $header = @{ authorization=$Token } Write-Debug "Number of blockIds to be merged: $($global:blockIds.count)" Try { $body = @{ conversionId = $global:conversionId blockIdList = @($global:blockIds) } $JsonBody = $body | ConvertTo-Json Invoke-RestMethod -Method 'POST' -Uri $MergeUrl -Body $JsonBody -Header $header -MaximumRedirection 1 >> $null Write-To-LogFile "File $PackageName Uploaded for Conversion with Id: $global:conversionId" Write-Output "File $PackageName Uploaded for Conversion with Id: $global:conversionId" Write-To-LogFile "Use above Conversion ID to check Conversion Summary and to Download the converted $PackageName package." Write-Output "Use above Conversion ID to check Conversion Summary and to Download the converted $PackageName package." } Catch { Register-Error($_) throw "Failed upload process" } } # function to clean up residual folder and files function Clear-Residual ($PackageTempDirectoryPath) { if (!(Test-Path -Path "$PackageTempDirectoryPath")) { Write-Debug "Nothing to clean up" } Remove-Item $PackageTempDirectoryPath -Recurse } # Read input file and upload to MSIX Cloud service. function Invoke-UploadInstaller() { param( [Parameter(Mandatory)] [string]$PSScriptRoot, [Parameter(HelpMessage = "Please provide the silent install flag.")] [string]$IsUnattendedInstallWithoutArgument, [Parameter(Mandatory, HelpMessage = "Please provide a valid input package path")] [string]$PackagePath, [Parameter(Mandatory, HelpMessage = "Please provide the Package Version")] [ValidatePattern( "^((0|[1-9][0-9]{0,3}|[1-5][0-9]{4}|6[0-4][0-9]{3}|65[0-4][0-9]{2}|655[0-2][0-9]|6553[0-5])(\.(0|[1-9][0-9]{0,3}|[1-5][0-9]{4}|6[0-4][0-9]{3}|65[0-4][0-9]{2}|655[0-2][0-9]|6553[0-5])){3})$", ErrorMessage = "{0} is not valid. Valid example for entry package version is 1.0.0.0" )] [string]$PackageVersion, [Parameter(Mandatory, HelpMessage = "Please provide package silent arguments")] [string]$PackageInstallArguments, [Parameter(HelpMessage = "Please provide the bearer token.")] [string]$Token, [Parameter(HelpMessage = "Please provide the Package Publisher Name")] [ValidatePattern( "^((CN|L|O|OU|E|C|S|STREET|T|G|I|SN|DC|SERIALNUMBER|Description|PostalCode|POBox|Phone|" + "X21Address|dnQualifier|(OID\.(0|[1-9][0-9]*)(\.(0|[1-9][0-9]*))+))=(([^,+=""<;>;#;])+|"".*"")" + "(, ((CN|L|O|OU|E|C|S|STREET|T|G|I|SN|DC|SERIALNUMBER|Description|PostalCode|POBox|Phone|X21Address|dnQualifier|" + "(OID\.(0|[1-9][0-9]*)(\.(0|[1-9][0-9]*))+))=(([^,+=""<;>;#;])+|"".*"")))*)$", ErrorMessage = "{0} is not valid. Valid example for entry publisher name is CN=MyCorporation" )] [string]$PublisherName, [Parameter(HelpMessage = "Optional Parameter: package name")] [ValidateLength(3, 50)] [ValidatePattern( "^[-.A-Za-z0-9]+$", ErrorMessage = "{0} is not valid. Valid example for entry package name is My-Package1" )] [string]$PackageName, [Parameter(HelpMessage = "Optional Parameter: Please provide the environment value.")] [string]$Env ) . "$PSScriptRoot/Common.ps1" . "$PSScriptRoot/Invoke-UploadInstaller.ps1" # Get Environment value if ( !$Env ) { $Env = "msix" } $global:devRestEndPoint = $(Get-Content "$PSScriptRoot/config.json" | ConvertFrom-Json).$Env.url Try { if ( Test-Path -Path "$PackagePath" ) { Write-Debug "Valid PackagePath: $PackagePath" } else { Write-Error "Not a valid PackagePath: $PackagePath" } # Get Full Absolute Package Path $FullPackagePath = [System.IO.Path]::GetFullPath($PackagePath) # Get Package Name if ( !$PackageName ) { $PackageName = [System.IO.Path]::GetFileNameWithoutExtension($FullPackagePath) } Write-To-LogFile "Package Info: " Write-To-LogFile "Name: $PackageName" # Get PackageType $PackageType = [System.IO.Path]::GetExtension($FullPackagePath) Write-To-LogFile "Type: $PackageType" # Print PackageVersion Write-To-LogFile "Version: $PackageVersion" #Print PublisherName if(!$PublisherName) { $PublisherName = $null } else { Write-To-LogFile "PublisherName: $PublisherName" } #Print IsUnattendedInstallWithoutArgument if(!$IsUnattendedInstallWithoutArgument) { $IsUnattendedInstallWithoutArgument = $null } else { Write-To-LogFile "Silent install flag: $IsUnattendedInstallWithoutArgument" } # Print PackageVersion Write-To-LogFile "InstallArguments: $PackageInstallArguments" Write-To-LogFile "----------------------------" $PackageTempDirectoryPath = Add-Temp_Folder -FullPackagePath $FullPackagePath -PackageName $PackageName -PackageType $PackageType } Catch { Write-Output($_) } # Function to upload blob to Azure function Push-Blob() { Try { # Split input installer into multiple blocks in a sub directory. Write-ProgressHelper -Message 'Uploading the file...' -Weightage 1 Split-Package-File-Into-Block -PackageTempDirectoryPath $PackageTempDirectoryPath Write-ProgressHelper -Message 'Uploading the file...' -Weightage 3 # Trigger init API call Push-Package -PackageName $PackageName -PackageType $PackageType -PackageVersion $PackageVersion -PublisherName $PublisherName -PackageInstallArguments $PackageInstallArguments -IsUnattendedInstallWithoutArgument $IsUnattendedInstallWithoutArgument Write-ProgressHelper -Message 'Uploading the file...' -Weightage 5 # Start batch of parallel block upload API call Push-Package-Block Write-ProgressHelper -Message 'Uploading the file...' -Weightage 13 Write-Debug "--------- Uploaded ---------" } Catch { throw "Failed upload process" } Finally { # Trigger merge blocks API call Approve-Blocks-Merge($PackageName) Write-ProgressHelper -Message 'Uploading the file...' -Weightage 15 } } Try { Push-Blob } Catch { Write-To-LogFile "Upload process failed. Retrying..." for($i = 0 ; $i -lt $noOfRetries ; $i++) { Try { Push-Blob break } Catch { if($($i+1) -ne $noOfRetries) { Write-To-LogFile "Upload process failed $($i+2) times. Retrying..." } else { Write-To-LogFile "Retry count exhausted for Upload process. Terminating..." Write-To-LogFile "--------- Upload Failed ---------" } } } } Finally { # Logic for cleanup operation. Clear-Residual -PackageTempDirectoryPath $PackageTempDirectoryPath Start-Sleep 1 Write-ProgressHelper -Message 'Completed...' -Weightage 16 } } |