Functions/Publish-ProGetUniversalPackage.ps1


function Publish-ProGetUniversalPackage
{
    <#
    .SYNOPSIS
    Publishes a package to the specified ProGet instance
 
    .DESCRIPTION
    The `Publish-ProGetUniversalPackage` function will upload a package to the `FeedName` universal feed. It uses .NET 4.5's `HttpClient` to upload the file.
 
    .EXAMPLE
    Publish-ProGetUniversalPackage -Session $ProGetSession -FeedName 'Apps' -PackagePath 'C:\ProGetPackages\TestPackage.upack'
 
    Demonstrates how to call `Publish-ProGetUniversalPackage`. In this case, the package named 'TestPackage.upack' will be published to the 'Apps' feed located at $Session.Uri using the $Session.Credential authentication credentials
    #>

    [CmdletBinding(SupportsShouldProcess=$true)]
    param(
        [Parameter(Mandatory=$true)]
        [pscustomobject]
        # The session includes ProGet's URI and the credentials to use when utilizing ProGet's API.
        $Session,

        [Parameter(Mandatory=$true)]
        [string]
        # The feed name indicates the appropriate feed where the package should be published.
        $FeedName,

        [Parameter(Mandatory=$true)]
        [string]
        # The path to the package that will be published to ProGet.
        $PackagePath,

        [int]
        # The timeout (in seconds) for the upload. The default is 100 seconds.
        $Timeout = 100,

        [Switch]
        # Replace the package if it already exists in ProGet.
        $Force
    )

    Set-StrictMode -Version 'Latest'
    Use-CallerPreference -Cmdlet $PSCmdlet -SessionState $ExecutionContext.SessionState
    
    $shouldProcessCaption = ('creating {0} package' -f $PackagePath)
    $proGetPackageUri = New-Object 'Uri' $Session.Uri,('/upack/{0}' -f $FeedName)
    $proGetCredential = $Session.Credential

    $PackagePath = Resolve-Path -Path $PackagePath | Select-Object -ExpandProperty 'ProviderPath'
    if( -not $PackagePath )
    {
        Write-Error -Message ('Package ''{0}'' does not exist.' -f $PSBoundParameters['PackagePath'])
        return
    }

    $userMsg = ''
    if( $proGetCredential )
    {
        $userMsg = ' as ''{0}''' -f $proGetCredential.UserName
    }

    if( -not $Force )
    {
        $version = $null
        $name = $null
        $group = $null
        $zip = $null
        $foundUpackJson = $true
        $invalidUpackJson = $false
        try
        {
            $zip = [IO.Compression.ZipFile]::OpenRead($PackagePath)
            $foundUpackJson = $false
            foreach( $entry in $zip.Entries )
            {
                if($entry.FullName -ne "upack.json" )
                {
                    continue
                }

                $foundUpackJson = $true
                $stream = $entry.Open()
                $stringReader = New-Object 'IO.StreamReader' $stream
                try
                {
                    $packageJson = $stringReader.ReadToEnd() | ConvertFrom-Json
                    $version = $packageJson.version
                    $name = $packageJson.name
                    if( $packageJson | Get-Member -Name 'group' )
                    {
                        $group = $packageJson.group
                    }
                }
                catch
                {
                    $invalidUpackJson = $true
                }
                finally
                {
                    $stringReader.Close()
                    $stream.Close()
                }
                break
            }
        }
        catch
        {
            Write-Error -Message ('The upack file ''{0}'' isn''t a valid ZIP file.' -f $PackagePath)
            return
        }
        finally
        {
            if( $zip )
            {
                $zip.Dispose()
            }
        }

        if( -not $foundUpackJson )
        {
            Write-Error -Message ('The upack file ''{0}'' is invalid. It must contain a upack.json metadata file. See http://inedo.com/support/documentation/various/universal-packages/universal-feed-api for more information.' -f $PackagePath) 
            return
        }

        if( $invalidUpackJson )
        {
            Write-Error -Message (@"
The upack.json metadata file in '$($PackagePath)' is invalid. It must be a valid JSON file with ''version'' and ''name'' properties that have values, e.g.
     
    {
        ""name"": ""HDARS"",
        ""version": ""1.3.9""
    }
     
See http://inedo.com/support/documentation/various/universal-packages/universal-feed-api for more information.
     
"@
)        
            return
        }

        if( -not $name -or -not $version )
        {
            [string[]]$propertyNames = @( 'name', 'version') | Where-Object { -not (Get-Variable -Name $_ -ValueOnly) }
            $description = 'property doesn''t have a value'
            if( $propertyNames.Count -gt 1 )
            {
                $description = 'properties don''t have values'
            }
            $emptyPropertyNames =  $propertyNames -join ''' and '''
                                    
            Write-Error -Message ('The upack.json metadata file in ''{0}'' is invalid. The ''{1}'' {2}. See http://inedo.com/support/documentation/various/universal-packages/universal-feed-api for more information.' -f $PackagePath,$emptyPropertyNames,$description)
            return
        }

        $packageInfo = Get-ProGetUniversalPackage -Session $Session -FeedName $FeedName -GroupName $group -Name $name -ErrorAction Ignore
        if( $packageInfo -and $packageInfo.versions -contains $version )
        {
            Write-Error -Message ('Package {0} {1} already exists in universal ProGet feed ''{2}''.' -f $name,$version,$proGetPackageUri)
            return
        }
    }

    $operationDescription = 'Uploading ''{0}'' package to ProGet at ''{1}''{2}.' -f ($PackagePath | Split-Path -Leaf), $proGetPackageUri, $userMsg
    if( $PSCmdlet.ShouldProcess($operationDescription, $operationDescription, $shouldProcessCaption) )
    {
        Write-Verbose -Message $operationDescription

        $networkCred = $null
        if( $proGetCredential )
        {
            $networkCred = $proGetCredential.GetNetworkCredential()
        }

        $maxDuration = New-Object 'TimeSpan' 0,0,$Timeout

        [Net.Http.HttpClientHandler]$httpClientHandler = $null
        [Net.Http.HttpClient]$httpClient = $null
        [IO.FileStream]$packageStream = $null
        [Net.Http.StreamContent]$streamContent = $null
        [Threading.Tasks.Task[Net.Http.HttpResponseMessage]]$httpResponseMessage = $null
        [Net.Http.HttpResponseMessage]$response = $null
        [Threading.CancellationTokenSource]$canceller = $null
        try
        {
            $httpClientHandler = New-Object 'Net.Http.HttpClientHandler'
            if( $proGetCredential )
            {
                $httpClientHandler.UseDefaultCredentials = $false
                $httpClientHandler.Credentials = $networkCred
            }

            $httpClientHandler.PreAuthenticate = $true;

            $httpClient = New-Object 'Net.Http.HttpClient' ([Net.Http.HttpMessageHandler]$httpClientHandler)
            $httpClient.Timeout = $maxDuration

            $packageStream = New-Object 'IO.FileStream' ($PackagePath, 'Open', 'Read')
            $streamContent = New-Object 'Net.Http.StreamContent' ([IO.Stream]$packageStream)
            $streamContent.Headers.ContentType = New-Object 'Net.Http.Headers.MediaTypeHeaderValue' ('application/octet-stream')
            $canceller = New-Object 'Threading.CancellationTokenSource'
            $httpResponseMessage = $httpClient.PutAsync($proGetPackageUri, [Net.Http.HttpContent]$streamContent, $canceller.Token)
            if( -not $httpResponseMessage.Wait($maxDuration) )
            {
                $canceller.Cancel()
                $maxTries = 1000
                $tryNum = 0
                while( $tryNum -lt $maxTries -and -not $httpResponseMessage.IsCanceled )
                {
                    $tryNum += 1
                    Start-Sleep -Milliseconds 100
                }
                Write-Error -Message ('Uploading file ''{0}'' to ''{1}'' timed out after {2} second(s). To increase this timeout, set the Timeout parameter to the number of seconds to wait for the upload to complete.' -f $PackagePath,$proGetPackageUri,$Timeout)
                return
            }
                        
            $response = $httpResponseMessage.Result
            if( -not $response.IsSuccessStatusCode )
            {
                Write-Error -Message ('Failed to upload ''{0}'' to ''{1}''. We received the following ''{2} {3}'' response:{4} {4}{5}{4} {4}' -f $PackagePath,$proGetPackageUri,[int]$response.StatusCode,$response.StatusCode,[Environment]::NewLine,$response.Content.ReadAsStringAsync().Result)
                return
            }
        }
        catch
        {
            $ex = $_.Exception
            while( $ex.InnerException )
            {
                $ex = $ex.InnerException
            }

            if( $ex -is [Threading.Tasks.TaskCanceledException] )
            {
                Write-Error -Message ('Uploading file ''{0}'' to ''{1}'' was cancelled. This is usually because the upload took longer than the timeout, which was {2} second(s). Use the Timeout parameter to increase the upload timeout.' -f $PackagePath,$proGetPackageUri,$Timeout) 
                return
            }

            Write-Error -Message ('An unknown error occurred uploading ''{0}'' to ''{1}'': {2}' -f $PackagePath,$proGetPackageUri,$_)
            return
        }
        finally
        {
            $disposables = @( 'httpClientHandler', 'httpClient', 'canceller', 'packageStream', 'streamContent', 'httpResponseMessage', 'response' ) 
            $disposables |
                ForEach-Object { Get-Variable -Name $_ -ValueOnly -ErrorAction Ignore } |
                Where-Object { $_ -ne $null } |
                ForEach-Object { $_.Dispose() }
            $disposables | ForEach-Object { Remove-Variable -Name $_ -Force -ErrorAction Ignore }
        }
    }
}