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
    }
}