ProGetAutomation.psm1


Add-Type -AssemblyName 'System.Net.Http'
Add-Type -AssemblyName 'System.Web'
Add-Type -AssemblyName 'System.IO.Compression.FileSystem'

$functionsDirPath = Join-Path -Path $PSScriptRoot -ChildPath 'Functions'
if( (Test-Path -Path $functionsDirPath -PathType Container) )
{
    foreach( $item in Get-ChildItem -Path $functionsDirPath -Filter '*.ps1' )
    {
        Write-Debug -Message $item.FullName
        . $item.FullName
    }
}



function Add-ProGetUniversalPackageFile
{
    <#
    .SYNOPSIS
    Adds files and directories to a ProGet universal package.
 
    .DESCRIPTION
    The `Add-ProGetUniversalPackageFile` function adds files and directories to a upack file (use `New-ProGetUniversalPackage` to create a upack file). All items are added under the `package` directory in the package, per the upack specification.
 
    Files are added to the package using their names. They are always added to the `package` directory in the package. For example, if you added `C:\Projects\Zip\Zip\Zip.psd1` to the package, it would get added at `package\Zip.psd1`.
 
    Directories are added into a directory in the `package` directory. The directory in the package will use the name of the source directory. For example, if you add 'C:\Projects\Zip', all items will be added to the package at `package\Zip`.
 
    You can change the name an item will have in the package with the `PackageItemName` parameter. Path separators are allowed, so you can put any item into any directory in the package.
 
    If you don't want to add an entire directory to the package, but instead want a filtered set of files from that directory, pipe the filtered list of files to `Add-ProGetUniversalPackageFile` and use the `BasePath` parameter to specify the base path of the incoming files. `Add-ProGetUniversalPackageFile` removes the base path from each file and uses the remaining path as the file's name in the package.
 
    If you want to change an item's parent directory structure in the package, pass the parent path you want to the `PackageParentPath` parameter. For example, if you passed `tools` as the `PackageParentPath`, every item added will be put in a `package\tools` directory in the package.
 
    You can control the compression level of items getting added with the `CompressionLevel` parameter. The default is `Optimal`. Other options are `Fastest` (larger files, compresses faster) and `None`.
 
    This function uses the `Zip` PowerShell module, which uses the native .NET `System.IO.Compression` namespace/classes to do its work.
 
    .EXAMPLE
    Get-ChildItem 'C:\Projects\Zip' | Add-ProGetUniversalPackageFile -PackagePath 'zip.upack'
 
    Demonstrates how to pipe the files you want to add to your package into `Add-ProGetUniversalPackageFile`. In this case, all the files and directories in the `C:\Projects\Zip` directory are added to the package to the `package` directory.
 
    .EXAMPLE
    Get-ChildItem -Path 'C:\Projects\Zip' -Filter '*.ps1' -Recurse | Add-ProGetUniversalPackageFile -PackagePath 'zip.upack' -BasePath 'C:\Projects\Zip'
 
    This is like the previous example, but instead of adding every file under `C:\Projects\Zip`, we're only adding files with a `.ps1` extension. Since we're piping all the files to the `Add-ProGetUniversalPackageFile` function, we need to pass the base path of our search to the `BasePath` parameter. Otherwise, every file would get added to the `package` directory without preserving their directory structure. Instead, the `BasePath` is removed from every file's path and the remaining path is used as the item's path in the package.
 
    .EXAMPLE
    Get-Item -Path '.\Zip' | Add-ProGetUniversalPackageFile -PackagePath 'zip.upack' -PackageParentPath 'tools'
 
    Demonstrates how to customize the directory in the package files will be added at. In this case, the `.\Zip` directory will be put in a `package\tools` directory, e.g. `package\tools\Zip`.
 
    .EXAMPLE
    Get-ChildItem 'C:\Projects\Zip' | Add-ProGetUniversalPackageFile -PackagePath 'zip.upack' -EntryName 'tools\ZipModule'
 
    Demonstrates how to change the name of an item. In this case, the `C:\Projects\Zip` directory will be added to the package with a path of `package\tools\ZipModule` instead of `package\Zip`.
    #>

    [CmdletBinding(DefaultParameterSetName='ItemName')]
    param(
        [Parameter(Mandatory)]
        [string]
        # The path to the upack file. The files will be added to this package.
        $PackagePath,

        [Parameter(Mandatory,ValueFromPipeline,ValueFromPipelineByPropertyName)]
        [Alias('FullName')]
        [Alias('Path')]
        [string[]]
        # The files/directories to add to the upack file. Normally, you would pipe file/directory objects to `Add-ProGetUniversalPackageFile`. You may also pass any object that has a `FullName` or `Path property. You may also pass the path as a string.
        #
        # If you pass a directory object or path to a directory, that directory and all its sub-directories will be added to the upack file.
        #
        # All files/directories are added to the `packages` directory in the upack file.
        $InputObject,

        [Parameter(ParameterSetName='BasePath')]
        [string]
        # When determining a file's path/name in the package, the value of this parameter is removed from the beginning of each file's path. Use this parameter if you are piping in a filtered list of files from a directory instead of the directory itself.
        $BasePath,

        [Parameter(ParameterSetName='ItemName')]
        [ValidatePattern('^[^\\/]')]
        [ValidatePattern('[^\\/]$')]
        [string]
        # By default, items are added to the package using their name. You can change the name with this parameter. For example, if you added file `Zip.psd1` and passed `NewZip.psd1` as the value to the parameter, the file would get added to the package as `NewZip.psd1`.
        $PackageItemName,

        [ValidatePattern('^[^\\/]')]
        [string]
        $PackageParentPath,

        [IO.Compression.CompressionLevel]
        # The compression level of the upack file. The default is `Optimal`. Pass `Fastest` to compress faster but have a larger file. Pass `None` to not compress at all.
        $CompressionLevel = [IO.Compression.CompressionLevel]::Optimal,

        [Switch]
        # By default, if a file already exists in the upack file, you'll get an error. Use this switch to replace any existing files.
        $Force,

        [switch]
        # Suppress progress messages while adding files to the package.
        $Quiet
    )

    begin
    {
        Set-StrictMode -Version 'Latest'
        Use-CallerPreference -Cmdlet $PSCmdlet -SessionState $ExecutionContext.SessionState

        Write-Debug -Message ('ProGetAutomation\Add-ProGetUniversalPackageFile BEGIN')

        $parentPath = 'package'
        if( $PackageParentPath )
        {
            $parentPath = Join-Path -Path $parentPath -ChildPath $PackageParentPath
        }

        $params = @{ }
        if( $BasePath )
        {
            $params['BasePath'] = $BasePath
        }

        if( $PackageItemName )
        {
            $params['EntryName'] = $PackageItemName
        }

        if( $Force )
        {
            $params['Force'] = $true
        }

        if( $Quiet )
        {
            $params['Quiet'] = $true
        }

        $items = New-Object 'Collections.Generic.List[string]'
    }

    process
    {
        foreach( $item in $InputObject )
        {
            $items.Add($item)
        }
    }

    end
    {
        $items | Add-ZipArchiveEntry -ZipArchivePath $PackagePath -EntryParentPath $parentPath -CompressionLevel $CompressionLevel @params
        Write-Debug -Message ('ProGetAutomation\Add-ProGetUniversalPackageFile END')
    }
}


# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.

function Add-PSTypeName
{
    [CmdletBinding()]
    param(
        [Parameter(Mandatory=$true,ValueFromPipeline=$true)]
        $InputObject,

        [Parameter(Mandatory=$true,ParameterSetName='PackageInfo')]
        [Switch]
        $PackageInfo,

        [Parameter(Mandatory=$true,ParameterSetName='Native.Feed')]
        [Switch]
        $NativeFeed
    )

    process
    {
        Set-StrictMode -Version 'Latest'

        $typeName = 'Inedo.ProGet.{0}' -f $PSCmdlet.ParameterSetName
        $InputObject.pstypenames.Add( $typeName )

        if( $PackageInfo )
        {
            if( -not ($InputObject | Get-Member -Name 'group') )
            {
                $InputObject | Add-Member -MemberType NoteProperty -Name 'group' -Value ''
            }
        }

        $InputObject
    }
}

function Get-ProGetAsset
{
    <#
        .SYNOPSIS
        Gets metadata about items in an asset directory.
 
        .DESCRIPTION
        The `Get-ProGetAsset` function gets metadata from ProGet about assets. Pass the name of the root asset directory to the `DirectoryName` parameter. Information about all the files in that asset directory is returned. If the URL to an asset directory in ProGet is `https://proget.example.com/assets/versions/subdirectory/file`, the directory parameter is the first directory after `assets/` in this example `versions`, The path parameter the rest of the url in this case `subdirectory/file`.
 
        If you also pass a value to the `$filter` parameter, only files that match `$filter` value in the directory will be returned. Wildcards are supported.
 
        Pass a ProGet session object to the `$Session` parameter. This object controls what instance of ProGet to use and what credentials and/or API keys to use. Use the `New-ProGetSession` function to create session objects.
 
        .Example
        Get-ProGetAsset -Session $session -Path 'myAsset' -DirectoryName 'versions'
         
        Demonstrates how to get metadata about an asset. In this case, information about the `/versions/myAsset` file is returned. if `myAsset` is a directory then all files in that directory will be returned
 
        .Example
        Get-ProGetAsset -Session $session -Directory 'versions/subdirectory'
         
        Demonstrates how to get metadata from all files in the `versions/subdirectory` asset directory. If no files found an empty list is returned.
 
    #>

    param(
        [Parameter(Mandatory = $true)]
        [Object]
        # A session object that represents the ProGet instance to use. Use the `New-ProGetSession` function to create session objects.
        $Session,

        [Parameter(Mandatory = $true)]
        [string]
        # The name of a valid path to the directory to get metadata of the desired assets in ProGet.
        $DirectoryName,        

        [string]
        # The path to the subdirectory in the asset directory in ProGet.
        $Path,

        [string]
        # Name of the asset in the ProGet assets directory that will be retrieved. only file metadata that match `$Name` in the directory will be returned. Wildcards are supported.
        $Filter
    )

    Set-StrictMode -Version 'Latest'
    Use-CallerPreference -Cmdlet $PSCmdlet -SessionState $ExecutionContext.SessionState

    $uri = '/endpoints/{0}/dir/{1}' -f $DirectoryName,$Path


    if(!$Filter)
    {
        $Filter = '*'
    }
    return Invoke-ProGetRestMethod -Session $Session -Path $uri -Method Get | Where-Object { $_.Name -like $Filter }
}



function Get-ProGetAssetContent
{
    <#
    .SYNOPSIS
    Gets the content of an asset in an asset directory.
 
    .DESCRIPTION
    The `Get-ProGetAssetContent` function gets an asset's content from ProGet. Pass the name of the root asset directory to the `DirectoryName` parameter. Pass the path to the asset to the `Path` parameter. If the URL to an asset directory in ProGet is `https://proget.example.com/assets/versions/subdirectory/file`, the directory parameter is the first directory after `assets/` (in this example `versions`). The `Path` parameter would be the rest of the url in this case `subdirectory/file`.
 
    If an asset doesn't exist, an error will be written and nothing is returned.
 
    Pass a ProGet session object to the `Session` parameter. This object controls what instance of ProGet to use and what credentials and/or API keys to use. Use the `New-ProGetSession` function to create session objects.
 
    .Example
    Get-ProGetAssetContent -Session $session -DirectoryName 'versions' -Path 'subdirectory/file.json'
         
    Demonstrates how to get the contents of an asset. In this case, the `subdirectory/file.json` asset's contents in the `versions` asset directory is returned.
    #>

    param(
        [Parameter(Mandatory = $true)]
        [Object]
        # A session object that represents the ProGet instance to use. Use the `New-ProGetSession` function to create session objects.
        $Session,

        [Parameter(Mandatory = $true)]
        [string]
        # The name of the asset's asset directory.
        $DirectoryName,        

        [string]
        # The path to the file in the asset directory, without the asset directory's name.
        $Path
    )

    Set-StrictMode -Version 'Latest'
    Use-CallerPreference -Cmdlet $PSCmdlet -SessionState $ExecutionContext.SessionState

    $uri = '/endpoints/{0}/content/{1}' -f $DirectoryName,$Path

    return Invoke-ProGetRestMethod -Session $Session -Path $uri -Method Get -Raw
}



function Get-ProGetFeed
{
    <#
    .SYNOPSIS
    Gets the feeds in a ProGet instance.
 
    .DESCRIPTION
    The `Get-ProGetFeed` function gets all the feeds from a ProGet instance. Pass the session to the ProGet instance to the `Session` parameter. Use `New-ProGetSession` to create a session. By default, only active feeds are returned. Use the `-Force` switch to also return inactive feeds.
 
    To get a specific feed, pass its name to the `Name` parameter. If the feed by that name doesn't exist, nothing is returned and no errors are written.
 
    This function uses the `Feeds_GetFeed` and `Feeds_GetFeeds` endpoints in ProGet's [native API](https://inedo.com/support/documentation/proget/reference/api/native).
 
    .EXAMPLE
    Get-ProGetFeed -Session $session
 
    Demonstrates how to get all the feeds in a ProGet instance.
 
    .EXAMPLE
    Get-ProGetFeed -Session $session -Name PowerShell
 
    Demonstrates how to get a specific feed. In this case, the `PowerShell` feed is returned.
    #>

    [CmdletBinding(DefaultParameterSetName='AllFeeds')]
    param(
        [Parameter(Mandatory)]
        [object]
        $Session,

        [Parameter(Mandatory,ParameterSetName='ByName')]
        [string]
        # By default, all feeds are returned. Use this parameter to return a specific feed using its name.
        $Name,

        [Parameter(Mandatory,ParameterSetName='ByID')]
        [string]
        # By default, all feeds are returned. Use this parameter to return a specific feed using its ID.
        $ID,

        [Parameter(ParameterSetName='AllFeeds')]
        [Switch]
        # By default, only active feeds are returned. Use this witch to return inactive feeds, too.
        $Force
    )

    Set-StrictMode -Version 'Latest'
    Use-CallerPreference -Cmdlet $PSCmdlet -SessionState $ExecutionContext.SessionState

    $parameter = @{ 
                    'IncludeInactive_Indicator' = $Force.IsPresent;
                 }

    $methodName = 'Feeds_GetFeeds'
    if( $Name )
    {
        $methodName = 'Feeds_GetFeed'
        $parameter = @{ 
                        'Feed_Name' = $Name;
                    }
    }
    elseif( $ID )
    {
        $methodName = 'Feeds_GetFeed'
        $parameter = @{ 
                        'Feed_Id' = $ID;
                    }
    }

    Invoke-ProGetNativeApiMethod -Session $Session -Name $methodName -Parameter $parameter |
        Where-Object { $_ } |
        Add-PSTypeName -NativeFeed
}



function Get-ProGetUniversalPackage
{
    <#
    .SYNOPSIS
    Gets ProGet universal package information.
 
    .DESCRIPTION
    The `Get-ProGetUniversalPackage` function gets all the packages in a ProGet universal feed. Pass a ProGet sesion to the `Session` parameter (use `New-ProGetSession` to create a session). Pass the name of the universal feed to the `FeedName` parameter.
     
    You can get information about a specific package by passing its name to the `Name` parameter. Wildcards are supported. If the package is in a group, you must pass the group's name to the `GroupName` parameter. Otherwise, ProGet won't find it (i.e. if you don't pass the group name, ProGet only looks for a package not in a group).
 
    If the package doesn't exist, you'll get an error.
 
    To get all the packages in a group, pass the group name to the `GroupName` parameter and nothing to the `Name` parameter. (Note: there is currently a bug in ProGet 4.8.6 where this functionality doesn't work.)
 
    You can use wildcards to search for packages with names or in groups. Whenever you do a wildcard search, the function downloads *all* packages from ProGet and searches through them locally. If a wildcard search finds no packages, nothing happens (i.e. you won't see any errors).
 
    The ProGet API doesn't return a `group` property on objecs that aren't in a group. This function adds a `group` property whose value is an empty string.
 
    This function uses ProGet's [universal feed API](https://inedo.com/support/documentation/upack/feed-api/endpoints).
 
    .EXAMPLE
    Get-ProGetUniversalPackage -Session $session -FeedName 'Apps'
 
    Demonstrates how to get a list of all packages in the `Apps` feed.
 
    .EXAMPLE
    Get-ProGetUniversalPackage -Session $session -FeedName 'Apps' -Name 'ProGetAutomation'
 
    Demonstrates how to get a specific package from ProGet that is not in a group. In this case, the `ProGetAutomation` package will be returned. If a package doesn't exist, nothing is returned.
 
    .EXAMPLE
    Get-ProGetUniversalPackage -Session $session -FeedName 'Apps' -GroupName 'PSModules' -Name 'ProGetAutomation'
 
    Demonstrates how to get a specific package in a specific group in a universal feed. In this case, will return the `ProGetAutomation` package in the `PSModules` group in the `Apps` feed.
 
    .EXAMPLE
    Get-ProGetUniversalPackage -Session $session -FeedName 'Apps' -Name 'ProGet*'
 
    Demonstrates how to get multiple packages using wildcards. In this case, any package that begins with `ProGet` would be returned.
 
    .EXAMPLE
    Get-ProGetUniversalPackage -Session $session -FeedName 'Apps' -GroupName 'PSModules'
 
    Demonstrates how to get a list of all packages in a specific group in a universal feed. In this case, all packages in the `PSModules` group in the `Apps` feed will be returned.
 
    Note: due to a bug in ProGet 4.8.6, no packages will be returned.
    #>

    [CmdletBinding()]
    param(
        [Parameter(Mandatory)]
        [object]
        # A session object representing the ProGet instance to connect to. Use `New-ProGetSession` to create a new session.
        $Session,

        [Parameter(Mandatory)]
        [string]
        # The name of the feed whose packages to get.
        $FeedName,

        [string]
        # The name of a specific package to get. Wildcards supported. If the package is in a group, you must pass its group name to the `GroupName` parameter
        $Name,

        [string]
        $GroupName
    )

    Set-StrictMode -Version 'Latest'
    Use-CallerPreference -Cmdlet $PSCmdlet -SessionState $ExecutionContext.SessionState
    
    $searchingName = ($Name -and [WildcardPattern]::ContainsWildcardCharacters($Name))
    $searchingGroup = ($GroupName -and [WildcardPattern]::ContainsWildcardCharacters($GroupName))
    $queryString = ''
    if( -not $searchingName -and -not $searchingGroup )
    {
        $queryString = & {

                                if( $Name )
                                {
                                    'name={0}' -f [uri]::EscapeDataString($Name)
                                }

                                if( $GroupName )
                                {
                                    'group={0}' -f [Uri]::EscapeDataString($GroupName)
                                }
                        }
    }

    if( $queryString )
    {
        $queryString = '?{0}' -f ($queryString -join '&')
    }

    Invoke-ProGetRestMethod -Session $Session -Path ('/upack/{0}/packages{1}' -f [uri]::EscapeDataString($FeedName),$queryString) -Method Get |
        Where-Object {
            if( -not $searchingName )
            {
                return $true
            }

            return $_.name -like $Name
        } |
        Add-PSTypeName -PackageInfo |
        Where-Object {
            if( -not $GroupName -or -not $searchingGroup )
            {
                return $true
            }

            return $_.group -like $GroupName
        }
}



function Invoke-ProGetNativeApiMethod
{
    <#
    .SYNOPSIS
    Calls a method on ProGet's Native API.
 
    .DESCRIPTION
    The `Invoke-ProGetNativeApiMethod` calls a method on ProGet's Native API. From Inedo:
 
    > This API endpoint should be avoided if there is an alternate API endpoint available, as those are much easier to use and will likely not change.
 
    In other words, use a native API at your own peril.
 
    .EXAMPLE
    Invoke-ProGetNativeApiMethod -Session $session -Name 'Feeds_CreateOrUpdateProGetFeed' -Parameter @{ Feed_Name = 'Apps' }
 
    Demonstrates how to call `Invoke-ProGetNativeApiMethod`. In this example, it is calling the `Feeds_CreateOrUpdateProGetFeed` method to create a new Universal feed named `Apps`.
    #>

    [CmdletBinding(SupportsShouldProcess)]
    param(
        [Parameter(Mandatory)]
        [object]
        # A session object that represents the ProGet instance to use. Use the `New-ProGetSession` function to create session objects.
        $Session,

        [Parameter(Mandatory)]
        [string]
        # The name of the API method to use. The list can be found at `http://inedo.com/support/documentation/proget/reference/api/native` or in your ProGet installation at `/reference/api/native`
        $Name,

        [hashtable]
        $Parameter
    )

    Set-StrictMode -Version 'Latest'
    Use-CallerPreference -Cmdlet $PSCmdlet -SessionState $ExecutionContext.SessionState

    if( -not $Parameter )
    {
        $Parameter = @{}
    }
    
    Invoke-ProGetRestMethod -Session $Session -Path ('/api/json/{0}' -f $Name) -Method Post -Parameter $Parameter -ContentType Json

}



function Invoke-ProGetRestMethod
{
    <#
    .SYNOPSIS
    Invokes a ProGet REST method.
 
    .DESCRIPTION
    The `Invoke-ProGetRestMethod` invokes a ProGet REST API method. You pass the path to the endpoint (everything after `/api/`) via the `Name` parameter, the HTTP method to use via the `Method` parameter, and the parameters to pass in the body of the request via the `Parameter` parameter. This function converts the `Parameter` hashtable to JSON and sends it in the body of the request.
 
    You also need to pass an object that represents the ProGet instance and API key to use when connecting via the `Session` parameter. Use the `New-ProGetSession` function to create a session object.
    #>

    [CmdletBinding(SupportsShouldProcess,DefaultParameterSetName='None')]
    param(
        [Parameter(Mandatory)]
        [object]
        # A session object that represents the ProGet instance to use. Use the `New-ProGetSession` function to create session objects.
        $Session,

        [Parameter(Mandatory)]
        [String]
        # The path to the API endpoint.
        $Path,

        [Microsoft.PowerShell.Commands.WebRequestMethod]
        # The HTTP/web method to use. The default is `POST`.
        $Method = [Microsoft.PowerShell.Commands.WebRequestMethod]::Post,

        [Parameter(ParameterSetName='ByParameter')]
        [hashtable]
        # The parameters to pass to the method.
        $Parameter,

        [Parameter(ParameterSetName='ByParameter')]
        [string]
        [ValidateSet('Form','Json')]
        # Controls how the parameters are sent to the API. The default is `Form`, which sends them as URL-encoded name/value pairs (i.e. like a HTML form submission). The other options is `Json`, which converts the parameters to JSON and sends that JSON text as the content/body of the request. This parameter is ignored if there are no parmaeters to send or if the `InFile` parameter is used.
        $ContentType,

        [Parameter(ParameterSetName='ByFile')]
        [String]
        # Send the contents of the file at this path as the body of the web request.
        $InFile,

        [Parameter(ParameterSetName='ByContent')]
        [String]
        # Send the content of this string as the body of the web request.
        $Body,

        [Switch]
        # Return the raw content from the request instead of attempting to convert the response from JSON into an object.
        $Raw
    )

    Set-StrictMode -Version 'Latest'
    Use-CallerPreference -Cmdlet $PSCmdlet -SessionState $ExecutionContext.SessionState
    
    $uri = New-Object 'Uri' -ArgumentList $Session.Uri,$Path
    
    $requestContentType = 'application/json; charset=utf-8'
    $debugBody = $null

    if( $PSCmdlet.ParameterSetName -eq 'ByParameter' )
    {
        if( $ContentType -eq 'Json' )
        {
            $Body = $Parameter | ConvertTo-Json -Depth 100
            $debugBody = $Body -replace '("API_Key": +")[^"]+','$1********'
        }
        else
        {
            $Body = $Parameter.Keys | ForEach-Object { '{0}={1}' -f [Web.HttpUtility]::UrlEncode($_),[Web.HttpUtility]::UrlEncode($Parameter[$_]) }
            $Body = $Body -join '&'
            $requestContentType = 'application/x-www-form-urlencoded; charset=utf-8'
            $debugBody = $Parameter.Keys | ForEach-Object {
                $value = $Parameter[$_]
                if( $_ -eq 'API_Key' )
                {
                    $value = '********'
                }
                ' {0}={1}' -f $_,$value }
        }
    }

    $headers = @{ }

    if( $Session.ApiKey )
    {
        $headers['X-ApiKey'] = $Session.ApiKey;
    }

    if( $Session.Credential )
    {
        $bytes = [Text.Encoding]::UTF8.GetBytes(('{0}:{1}' -f $Session.Credential.UserName,$Session.Credential.GetNetworkCredential().Password))
        $creds = 'Basic ' + [Convert]::ToBase64String($bytes)
        $headers['Authorization'] = $creds
    }

    #$DebugPreference = 'Continue'
    Write-Debug -Message ('{0} {1}' -f $Method.ToString().ToUpperInvariant(),($uri -replace '\b(API_Key=)([^&]+)','$1********'))
    Write-Debug -Message (' Content-Type: {0}' -f $requestContentType)
    foreach( $headerName in $headers.Keys )
    {
        $value = $headers[$headerName]
        if( @( 'X-ApiKey', 'Authorization' ) -contains $headerName )
        {
            $value = '*' * 8
        }

        Write-Debug -Message (' {0}: {1}' -f $headerName,$value)
    }
    
    if( $debugBody )
    {
        $debugBody | Write-Verbose
    }

    $errorsAtStart = $Global:Error.Count
    try
    {
        $optionalParams = @{
                                'ContentType' = $requestContentType;
                           }
        if( $PSCmdlet.ParameterSetName -in ('ByParameter', 'ByContent') )
        {
            if( $Body )
            {
                $optionalParams['Body'] = $Body
            }
            else
            {
                $optionalParams.Remove('ContentType')
            }
        }
        elseif( $PSCmdlet.ParameterSetName -eq ('ByFile') )
        {
            $optionalParams['Infile'] = $Infile
            $requestContentType = 'multipart/form-data'
        }

        if( $Session.Credential )
        {
            $optionalParams['Credential'] = $Session.Credential
        }

        $cmdName = 'Invoke-RestMethod'
        if( $Raw )
        {
            $cmdName = 'Invoke-WebRequest'
        }

        if( (Get-Command -Name $cmdName -ParameterName 'UseBasicParsing' -ErrorAction Ignore) )
        {
            $optionalParams['UseBasicParsing'] = $true
        }

        if( $Method -eq [Microsoft.PowerShell.Commands.WebRequestMethod]::Get -or $PSCmdlet.ShouldProcess($uri,$Method) )
        {
            & $cmdName -Method $Method -Uri $uri @optionalParams -Headers $headers | 
                ForEach-Object { $_ } 
        }
    }
    catch [Net.WebException]
    {
        for( $idx = $errorsAtStart; $idx -lt $Global:Error.Count; ++$idx )
        {
            $Global:Error.RemoveAt(0)
        }

        Write-Error -ErrorRecord $_ -ErrorAction $ErrorActionPreference
    }
}


function New-ProGetFeed
{
    <#
    .SYNOPSIS
    Creates a new ProGet package feed
 
    .DESCRIPTION
    The `New-ProGetFeed` function creates a new ProGet feed. Use the `Type` parameter to specify the feed type (valid values are 'VSIX', 'RubyGems', 'Docker', 'ProGet', 'Maven', 'Bower', 'npm', 'Deployment', 'Chocolatey', 'NuGet', 'PowerShell'). The `Session` parameter controls the instance of ProGet to connect to. This function uses ProGet's Native API, so an API key is required. Use `New-ProGetSession` to create a session with your API key.
 
    .EXAMPLE
    New-ProGetFeed -Session $ProGetSession -Name 'Apps' -Type 'ProGet'
 
    Demonstrates how to call `New-ProGetFeed`. In this case, a new Universal package feed named 'Apps' will be created for the specified ProGet Uri
    #>

    [CmdletBinding()]
    param(
        # The session includes ProGet's URI and the API key. Use `New-ProGetSession` to create session objects
        [Parameter(Mandatory)]
        [pscustomobject] $Session,

        # The feed name indicates the name of the package feed that will be created.
        [Parameter(Mandatory)]
        [Alias('FeedName')]
        [string] $Name,

        # The feed type indicates the type of package feed to create.
        # Valid feed types are ('VSIX', 'RubyGems', 'Docker', 'ProGet', 'Maven', 'Bower', 'npm', 'Deployment', 'Chocolatey', 'NuGet', 'PowerShell') - check here for a latest list - https://inedo.com/support/documentation/proget/feed-types/universal
        [Parameter(Mandatory)]
        [Alias('FeedType')]
        [string] $Type
    )

    Set-StrictMode -Version 'Latest'
    Use-CallerPreference -Cmdlet $PSCmdlet -SessionState $ExecutionContext.SessionState

    if( -not $Session.ApiKey )
    {
        Write-Error -Message ('We are unable to create new package feed ''{0}/{1}'' because your ProGet session is missing an API key. This function uses ProGet''s Native API, which requires an API key. Use `New-ProGetSession` to create a session object that uses an API key.' -f $Type, $Name)
        return
    }

    if( $Type -eq 'ProGet' )
    {
        $msg = 'ProGet renamed its "ProGet" feed type name to "Universal". Please update the value of ' +
               'New-ProGetFeed''s "Type" parameter from "ProGet" to "Universal".'
        Write-Warning $msg
        $Type = 'Universal'
    }

    $Parameters = @{
                        'FeedType_Name' = $Type;
                        'Feed_Name' = $Name;
                    }

    $feedExists = Test-ProGetFeed -Session $Session -Name $Name -Type $Type
    if( $feedExists )
    {
        Write-Error -Message ('Unable to create {0} {1} feed: a feed with that name and type already exists.' -f $Type, $Name) -ErrorAction $ErrorActionPreference
        return
    }
    Write-Verbose -Message ('Creating {0} {1} feed in ProGet instance "{2}".' -f $Type, $Name, $Session.Uri)
    $null = Invoke-ProGetNativeApiMethod -Session $Session -Name 'Feeds_CreateFeed' -Parameter $Parameters

}



function New-ProGetSession
{
    <#
    .SYNOPSIS
    Creates a session object used to communicate with a ProGet instance.
 
    .DESCRIPTION
    The `New-ProGetSession` function creates and returns a session object that is required when calling any function in the ProGetAutomation module that communicates with ProGet. The session includes ProGet's URI and the credentials to use when utilizing ProGet's API.
 
    .EXAMPLE
    $session = New-ProGetSession -Uri 'https://proget.com' -Credential $credential
 
    Demonstrates how to call `New-ProGetSession`. In this case, the returned session object can be passed to other ProGetAutomation module functions to communicate with ProGet at `https://proget.com` with the credential in `$credential`.
    #>

    [CmdletBinding()]
    param(
        [Parameter(Mandatory=$true)]
        [uri]
        # The URI to the ProGet instance to use.
        $Uri,

        [pscredential]
        # The credential to use when making requests to ProGet utilizing the Universal Feed API.
        $Credential,

        [string]
        # The API key to use when making requests to ProGet utilizing the Native API
        $ApiKey
    )

    Set-StrictMode -Version 'Latest'

    return [pscustomobject]@{
                                Uri = $Uri;
                                Credential = $Credential;
                                ApiKey = $ApiKey
                            }
}



function New-ProGetUniversalPackage
{
    <#
    .SYNOPSIS
    Creates a ProGet universal package file.
 
    .DESCRIPTION
    The `New-ProGetUniversalPackage` function creates a ProGet universal package file. The file will only contain a upack.json file. Pass the path to the file to create to the `OutFile` parameter (the file must not exist or you'll get an error). You must supply a name (with the `Name` parameter) and a version (with the `Version` parameter). Names can only contain letters, numbers, periods, underscores, and hyphens. Version must be a valid semantic version. Pass
 
    `New-ProGetUniversalPackage` has the following parameters that add the appropriate metadata to the package's upack.json manifest:
 
    * GroupName
    * Title
    * ProjectUri
    * IconUri
    * Description
    * Tag (creates the `tags` property)
    * Dependency (creates the `dependencies` property)
    * Reason (creates the `createdReason` property)
    * Author (creates the `createdBy` property)
 
    You can pass additional custom metadata to the `AdditionalMetadata` property. It is recommended that all custom metadata be prefixed with an underscore to prevent collision with future standard metadata.
 
    The `New-ProGetUniversalPackage` function always adds two additional pieces of metadata:
     
    * `createdDate`, the UTC date/time this function gets called
    * `createdUsing`, a string that identifies the ProGetAutomation module as the tool used; it includes the module's version, the PowerShell version, and, if available, the PowerShell edition.
 
    A `IO.FileInfo` object is returned for the just-created package.
 
    The `Zip` PowerShell module is used to create the ZIP archive and add the upack.json file to it.
 
    By default, optimal compression is used. You can customize your compression level with the `CompressionLevel` parameter.
 
    See the [upack.json Manifest Specification page](https://inedo.com/support/documentation/upack/universal-packages/metacontent-guidance/manifest-specification) for more information about the format and contents of the upack.json file.
 
    Once you've created the package, you can then add additional files to it with the `Add-ProGetUniversalPackage` function.
 
    .EXAMPLE
    New-ProGetUniversalPackage -OutFile 'package.upack' -Version '0.0.0' -Name 'ProGetAutomation'
 
    Demonstrates how to create a minimal upack package.
 
    .EXAMPLE
    New-ProGetUniversalPackage -OutFile 'package.upack' -Version '0.0.0' -Name 'ProGetAutomation' -GroupName 'WHS/PowerShell' -Title 'ProGet Automation' -ProjectUri 'https://github.com/webmd-health-services/ProGetAutomation' -IconUri 'https://github.com/webmd-health-services/ProGetAutomation/icon.png' -Description 'A PowerShell module for automationg ProGet.' -Tag @( 'powershell', 'module', 'inedo', 'proget' ) -Dependency @( 'zip' ) -Reason 'Because the world needs more PowerShell!' -Author 'WebMD Health Services'
 
    Demonstrates how to create a upack package with all required and optional metadata. (The ProGetAutomation package doesn't have any dependencies. The example shows one for illustrative purposes only.)
 
    .EXAMPLE
    New-ProGetUniversalPackage -OutFile 'package.upack' -Version '0.0.0' -Name 'ProGetAutomation' -AdditionalMetadata @{ '_whs' = @{ 'fubar' = 'snafu' } }
 
    Demonstrates how to add custom metadata to your package.
 
    .EXAMPLE
    New-ProGetUniversalPackage -OutFile 'package.upack' -Version '0.0.0' -Name 'ProGetAutomation' -CompressionLevel Fastest
 
    Demonstrates how to change the compression level of the package.
    #>

    [CmdletBinding()]
    [OutputType([IO.FileInfo])]
    param(
        [Parameter(Mandatory)]
        [string]
        # Path to the package. The package is created here. The filename should have a .upack extension.
        $OutFile,

        [Parameter(Mandatory)]
        [string]
        # The version of the package. Semantic Version 2 supported.
        $Version,

        [Parameter(Mandatory)]
        [ValidatePattern('^[A-Za-z0-9._-]+$')]
        [string]
        # The name of the package. Must only contain letters, numbers, periods, underscores or hyphens.
        $Name,

        [ValidatePattern('(^[A-Za-z0-9._/-]+$)|(^$)')]
        [ValidatePattern('(^[^/])|(^$)')]
        [ValidatePattern('([^/]$)|(^$)')]
        [string]
        # The group name of the package. Must only contain letters, numbers, periods, underscores, forward slashes, or hyphens. Must not begin or end with forward slashes.
        $GroupName,

        [ValidateLength(1,50)]
        [string]
        # The package's title/display name. Any characters are allowed. Can't be longer than 50 characters.
        $Title,

        [uri]
        # The URI to the project.
        $ProjectUri,

        [uri]
        # The URI to the projet/package's icon. The icon may be in the package itself. If it is, pass `package://path/to/icon`.
        $IconUri,

        [string]
        # A full description of the package. Formatted as Markdown in the ProGet UI.
        $Description,

        [string[]]
        [ValidatePattern('^[A-Za-z0-9._-]+$')]
        # An array of tags. Each tag must only contain letters, numbers, periods, underscores, and hyphens.
        $Tag,

        [string[]]
        # A list of dependencies as package names. Must be formatted like:
        #
        # * �group�/�package-name�
        # * �group�/�package-name�:�version�
        # * �group�/�package-name�:�version�:�sha-hash�
        $Dependency,

        [string]
        # The reason the package is getting created.
        $Reason,

        [string]
        # The author of the package.
        $Author,

        [hashtable]
        # Any additional metadata for the package. It is recommended that you prefix custom metadata with an underscore to prevent possible collisions with future system metadata.
        #
        # If you provide a parameter and duplicate that parameter's metadata in this hashtable, the parameter value takes precedence.
        #
        #
        $AdditionalMetadata = @{ },

        [IO.Compression.CompressionLevel]
        # The compression level to use. The default is `Optimal`. Other values are `Fastest` (larger file, created faster) or `None` (nothing is compressed).
        $CompressionLevel = [IO.Compression.CompressionLevel]::Optimal
    )

    Set-StrictMode -Version 'Latest'
    Use-CallerPreference -Cmdlet $PSCmdlet -SessionState $ExecutionContext.SessionState

    $tempDir = Join-Path -Path ([IO.Path]::GetTempPath()) -ChildPath ('{0}.{1}' -f ($OutFile | Split-Path -Leaf),([IO.Path]::GetRandomFileName()))
    New-Item -Path $tempDir -ItemType 'Directory' | Out-Null

    try
    {
        $upackJsonPath = Join-Path -Path $tempDir -ChildPath 'upack.json'

        [hashtable]$upackJson = $AdditionalMetadata.Clone()

        $upackJson['name'] =  $Name
        $upackJson['version'] = $Version

        if( -not $upackJson.ContainsKey('createdUsing') )
        {
            $psEdition = ''
            if( $PSVersionTable.ContainsKey('PSEdition') )
            {
                $psEdition = '; {0}' -f $PSVersionTable['PSEdition']
            }
            $upackJson['createdUsing'] = 'ProGetAutomation/{0} (PowerShell {1}{2})' -f (Get-Module -Name 'ProGetAutomation').Version,$PSVersionTable['PSVersion'],$psEdition
        }

        if( -not $upackJson.ContainsKey('createdDate') )
        {
            $upackJson['createdDate'] = (Get-Date).ToUniversalTime().ToString('O')
        }

        $parameterToMetadataMap = @{
                                        'GroupName' = 'group';
                                        'Title' = 'title';
                                        'ProjectUri' = 'projectUri';
                                        'IconUri' = 'iconUri';
                                        'Description' = 'description';
                                        'Tag' = 'tags';
                                        'Dependency' = 'dependencies';
                                        'Reason' = 'createdReason';
                                        'Author' = 'createdBy';
                                    }
        foreach( $parameterName in $parameterToMetadataMap.Keys )
        {
            if( -not $PSBoundParameters[$parameterName] )
            {
                continue
            }
        
            $metadataName = $parameterToMetadataMap[$parameterName]
            $upackJson[$metadataName] = $PSBoundParameters[$parameterName]
        }

        $upackJson | 
            ForEach-Object { [pscustomobject]$_ } |
            ConvertTo-Json -Depth 50 | 
            Set-Content -Path $upackJsonPath

        $archive = New-ZipArchive -Path $OutFile -CompressionLevel $CompressionLevel
        $upackJsonPath | Add-ZipArchiveEntry -ZipArchivePath $archive.FullName -CompressionLevel $CompressionLevel
        $archive
    }
    finally
    {
        Remove-Item -Path $tempDir -Recurse -Force -ErrorAction Ignore
    }
}


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



function Read-ProGetUniversalPackageFile
{
    <#
    .SYNOPSIS
    Reads the contents of a file from a package in a ProGet universal feed.
 
    .DESCRIPTION
    The `Read-ProGetUniversalPackageFile` reads the contents of a file from a package in a ProGet universal feed. Use this function to read parts of a universal package directly from ProGet without downloading the entire package. Pass the name of the universal feed to the `FeedName` parameter. Pass the name of the package to the `Name` parameter. Pass the path of the file in the package to the `Path` parameter. The path should include the `package` part of the path. ProGet is sensitive to directory separator characters. Make sure you use the same kind as the tool that created your package.
     
    By default, the file is read from the latest/most recent version of the package. To read a file from a specific version, pass that version to the `Version` parameter.
 
    This function uses the [Download File Package endpoint](https://inedo.com/support/documentation/upack/feed-api/endpoints#download-package-file) of ProGet's universal API.
 
    .EXAMPLE
    Read-ProGetUniversalPackageFile -Session $session -FeedName 'Apps' -Name 'MyApp' -Path 'upack.json'
 
    Demonstrates how to read the upack.json file from a package in a ProGet universal feed without downloading the entire package. In this example, the upack.json file is read from the package.
 
    .EXAMPLE
    Read-ProGetUniversalPackageFile -Session $session -FeedName 'Apps' -Name 'MyApp' -Path 'package/readme.md'
 
    Demonstrates how to read the readme.md file that was included in a package.
    #>

    [CmdletBinding()]
    param(
        [Parameter(Mandatory=$true)]
        [object]
        # A session object that represents the ProGet instance to use. Use the `New-ProGetSession` function to create session objects.
        $Session,

        [Parameter(Mandatory=$true)]
        [string]
        # The name of the feed where the package can be found.
        $FeedName,

        [Parameter(Mandatory=$true)]
        [string]
        $Name,

        [string]
        # The package version to check. Defaults to the latest, most recent package.
        $Version,

        [Parameter(Mandatory=$true)]
        [string]
        # The relative path to the file in the package. ProGet is sensitive to directory separator characters. Make sure to use the same kind as the tool that created your package. ProGet sees "package\file" and "package/file" differently.
        $Path
    )

    Set-StrictMode -Version 'Latest'
    Use-CallerPreference -Cmdlet $PSCmdlet -SessionState $ExecutionContext.SessionState

    $uriPath = '/upack/{0}/download-file/{1}/' -f ($FeedName,$Name | ForEach-Object { [Uri]::EscapeUriString($_) })
    if( $Version )
    {
        $uriPath = '{0}{1}/?' -f $uriPath,[Uri]::EscapeUriString($Version)
    }
    else
    {
        $uriPath = '{0}?latest&' -f $uriPath
    }

    $uriPath = '{0}path={1}' -f $uriPath,[Uri]::EscapeUriString($Path)

    Invoke-ProGetRestMethod -Session $Session -Path $uriPath -Parameter @{ } -Method Get -Raw
}


function Remove-ProGetAsset
{
    <#
        .SYNOPSIS
        Removes assets from ProGet.
 
        .DESCRIPTION
        The `Remove-ProGetAsset` function removes assets from ProGet. Pass the name of the root asset directory to the `DirectoryName` parameter. If the URL to an asset directory in ProGet is `https://proget.example.com/assets/versions/subdirectory/file`, the directory parameter is the first directory after `assets/` in this example `versions`, The path parameter is the rest of the url, in this case `subdirectory/file`. If the file does not exist no error will be thrown. All the files in the asset directory that match `$filter` parameter will be deleted.
 
        .EXAMPLE
        Remove-ProGetAsset -Session $session -Path 'myAssetName' -DirectoryName 'versions'
 
        Removes asset or assets that match `myAssetName`. if `myAssetName` is a directory it will delete the files in the directory but not the directory itself.
 
        .Example
        Remove-ProGetAsset -Session $session -Path 'versions/myAssetName' -DirectoryName 'example'
 
        Removes asset or assets that match `example/versions/myAssetName` in ProGet
 
        .Example
        Remove-ProGetAsset -Session $session -Path 'versions/example' -DirectoryName 'subexample' -filter '*a*'
 
        Removes all assets that match the wildcard `subexample/versions/example/*a*`
    #>

    param(
        [Parameter(Mandatory = $true)]
        [Object]
        # A session object that represents the ProGet instance to use. Use the `New-ProGetSession` function to create session objects.
        $Session,

        [Parameter(Mandatory = $true)]
        [string]
        # The name of the root asset directory to Remove the desired asset in ProGet.
        $DirectoryName, 

        [string]
        # the asset path in the ProGet assets directory that will be removed. If the file does not exist no error will be thrown.
        $Path,

        [string]
        # Name of the assets in the ProGet assets directory that will be deleted. only files that match `$filter` in the directory will be deleted. Wildcards are supported.
        $Filter
    )

    Set-StrictMode -Version 'Latest'
    Use-CallerPreference -Cmdlet $PSCmdlet -SessionState $ExecutionContext.SessionState

    $uri = '/endpoints/{0}/content/{1}' -f $DirectoryName,$Path
    $assetList = Get-ProGetAsset -Session $Session -Path $Path -DirectoryName $DirectoryName -Filter $filter

    foreach($asset in $assetList)
    {
        $asset = $asset.Name
        if($Path)
        {
            $asset = (join-Path -Path $Path -ChildPath $asset)
        }
        $uri = '/endpoints/{0}/content/{1}' -f $DirectoryName, $asset
        Invoke-ProGetRestMethod -Session $Session -Path $uri -Method Delete
    }
}



function Remove-ProGetFeed
{
    <#
    .SYNOPSIS
    Removes a feed from ProGet.
 
    .DESCRIPTION
    The `Remove-ProGetFeed` function removes a feed from ProGet. All packages in the feed are also deleted. Pass the session to the ProGet instance from which to delete the feed to the `Session` parameter (use the `New-ProGetSession` function to create a session. Pass the ID of the feed to the `ID` parameter. You can also pipe feed IDs or feed objects returned by `Get-ProGetFeed`.
 
    Since this has the potential to be a disastrous operation (did we mention all the packages in the feed will also get deleted) and can't be undone, you'll be asked to confirm the deletion. If you don't want to be prompted, use the `-Force` switch. This is dangerous.
 
    This function uses the `Feeds_DeleteFeed` endpoint in [ProGet's native API](https://inedo.com/support/documentation/proget/reference/api/native).
 
    .EXAMPLE
    Remove-ProGetFeed -Session $session -ID 4398
 
    Demonstrates how to delete a feed by passing its ID to the `ID` parameter.
 
    .EXAMPLE
    $feed | Remove-ProGetFeed -Session $session
 
    Demonstrates that you can pipe feed objects to `Remove-ProGetFeed` to remove those feeds. Use `Get-ProGetFeed` to get a feed objects.
 
    .EXAMPLE
 
    4398 | Remove-ProGetFeed -Session $session
 
    Demonstrates that you can pipe feed IDs to `Remove-ProGetFeed` to remove those feeds.
    #>

    [CmdletBinding(SupportsShouldProcess,ConfirmImpact='High')]
    param(
        [Parameter(Mandatory)]
        [object]
        # The session to the ProGet instance to use.
        $Session,

        [Parameter(Mandatory,ValueFromPipeline,ValueFromPipelineByPropertyName)]
        [Alias('Feed_Id')]
        [int]
        # The ID of the feed to remove. You may pipe feed IDs as integers or feed objects returned by the `Get-ProGetFeed` function.
        $ID,

        [Switch]
        # Force the deletion of the feed without prompting for confirmation. This is dangerous. Deleting a feed deletes all its packages.
        $Force
    )

    process
    {
        Set-StrictMode -Version 'Latest'
        Use-CallerPreference -Cmdlet $PSCmdlet -SessionState $ExecutionContext.SessionState

        $feed = Get-ProGetFeed -Session $Session -ID $ID
        if( -not $feed )
        {
            return
        }

        $parameter = @{
                        Feed_Id = $ID
                    }

        $description = 'Are you sure you want to delete {0} feed "{1}" (ID: {2}) and all its packages? THIS ACTION CANNOT BE UNDONE.' -f $feed.FeedType_Name,$feed.Feed_Name,$ID
        if( $Force -or $PSCmdlet.ShouldProcess($description,$description,('Confirm Deletion of {0} Feed "{1}"' -f $feed.FeedType_Name,$feed.Feed_Name)) )
        {
            Invoke-ProGetNativeApiMethod -Session $Session -Name 'Feeds_DeleteFeed' -Parameter $parameter
        }
    }
}



function Remove-ProGetUniversalPackage 
{
    <#
    .SYNOPSIS
    Removes a package from a ProGet universal feed.
 
    .DESCRIPTION
    The `Remove-ProGetUniversalPackage` function removes a package from a ProGet universal feed. Pass the session to the ProGet instance from which the package should get deleted to the `Session` parmeter (use `New-ProGetSession` to create a session). Pass the feed from which the package should get deleted to the `FeedName` parameter. Pass the name of the package to the `Name` parameter. Pass the package version to delete to the `Version` parameter. If the package is in a group, pass the group name to the `GroupName` parameter.
 
    If the package doesn't exist, you'll get an error.
 
    This function uses ProGet's [universal feed API](https://inedo.com/support/documentation/upack/feed-api/endpoints).
 
    .EXAMPLE
    Remove-ProGetUniversalPackage -Session $session -FeeName 'PowerShell' -Name 'ProGetAutomation' -Version '0.7.0'
 
    Demonstrates how to delete a specific package version. In this case, package `ProGetAutomation` version `0.7.0` is deleted from the `PowerShell` feed.
 
    .EXAMPLE
    Remove-ProGetUniversalPackage -Session $session -FeeName 'PowerShell' -Name 'ProGetAutomation' -Version '0.7.0' -GroupName 'Modules'
 
    Demonstrates how to delete a specific package version when a package is in a group. In this case, package `ProGetAutomation` version `0.7.0` in the `Modules` group is deleted from the `PowerShell` feed.
    #>

    [CmdletBinding(SupportsShouldProcess)]
    param(
        [Parameter(Mandatory)]
        [object]
        # A session to the ProGet instance from which the package should get deleted. Use `New-ProGetSession` to create a session.
        $Session,

        [Parameter(Mandatory)]
        [string]
        # The name of the feed from which the package should be deleted.
        $FeedName,

        [Parameter(Mandatory)]
        [string]
        # The name of the package to delete.
        $Name,

        [Parameter(Mandatory)]
        [string]
        # The specific package version to delete.
        $Version,

        [string]
        # The package's group.
        $GroupName
    )

    Set-StrictMode -Version 'Latest'
    Use-CallerPreference -Cmdlet $PSCmdlet -SessionState $ExecutionContext.SessionState

    $groupStem = ''
    if( $GroupName )
    {
        $groupStem = '{0}/' -f [uri]::EscapeDataString($GroupName)
    }

    $path = '/upack/{0}/delete/{1}{2}/{3}' -f [uri]::EscapeDataString($FeedName),$groupStem,[uri]::EscapeDataString($Name),[uri]::EscapeDataString($Version)
    Invoke-ProGetRestMethod -Session $Session -Path $path -Method Delete

}


function Set-ProGetAsset
{
    <#
    .SYNOPSIS
    Adds and updates assets to the ProGet asset manager.
 
    .DESCRIPTION
    The `Set-ProGetAsset` adds assets to a ProGet session. A DirectoryName and Path are required. Either a FilePath or Body must be provided.
 
    A root directory needs to be created in ProGet using the `New-ProGetFeed` function with Type `Asset`.
         
    * DirectoryName - the root asset directory where the asset is currently located or will be created.
    * Path - the filepath, relative to the root asset directory, where the asset is currently located or will be created.
    * FilePath - the filepath, relative to the current working directory, of the file that will be published as an asset.
    * Content - the content that will be published as an asset.
 
    .EXAMPLE
    Set-ProGetAsset -Session $session -DirectoryName 'assetDirectory'-Path 'subdir/exampleAsset.txt' -FilePath 'path/to/file.txt'
 
    Example of publishing a file located at `path/to/file.txt` to ProGet in the `assetDirectory/subdir` folder. If `assetDirectory` is not created it will throw an error. If subdir is not created it will create the folder.
         
    .EXAMPLE
    Set-ProGetAsset -Session $session -Directory 'assetDirectory' -Path 'exampleAsset.txt' -Content $bodyContent
 
    Example of publishing content contained in the $bodyContent variable to ProGet in the `assetDirectory` folder.
    #>

    [CmdletBinding()]
    param(
        [Parameter(Mandatory = $true)]
        [Object]
        # A session object that represents the ProGet instance to use. Use the `New-ProGetSession` function to create session objects.
        $Session,

        [Parameter(Mandatory = $true)]
        [string]
        # The name of a valid root asset directory in ProGet. If no root directories exist, use the `New-ProGetFeed` with parameter `-Type 'Asset'` to create a new asset directory.
        $DirectoryName,

        [Parameter(Mandatory = $true)]
        [string]
        # The path where the asset will be published. Any directories that do not exist will be created automatically.
        $Path,

        [Parameter(Mandatory = $true, ParameterSetName = 'ByFile')]
        [string]
        # The relative path of a file to be published as an asset.
        $FilePath,

        [Parameter(Mandatory = $true, ParameterSetName = 'ByContent')]
        [string]
        # The content to be published as an asset.
        $Content
    )

    Set-StrictMode -Version 'Latest'
    Use-CallerPreference -Cmdlet $PSCmdlet -SessionState $ExecutionContext.SessionState

    $contentParam = @{ }
    switch( $PSCmdlet.ParameterSetName )
    {
        'ByFile' {
            if( !(Test-Path -Path $FilePath) )
            {
                Write-Error ('Could not find file named ''{0}''. Please pass in a valid file path.' -f $FilePath)
                return
            }

            $contentParam['Infile'] = $FilePath
        }
        'ByContent' {
            $contentParam['Body'] = $Content
        }
    }

    Invoke-ProGetRestMethod -Session $Session -Path ('/endpoints/{0}/content/{1}' -f $DirectoryName, $Path) -Method Post @contentParam
}



function Test-ProGetFeed
{
    <#
    .SYNOPSIS
    Checks if a feed exists in a ProGet instance.
 
    .DESCRIPTION
    The `Test-ProGetFeed` function tests if a feed exists in ProGet instance. Pass the session to your ProGet instance to the `Session` parameter (use `New-ProGetSession` to create a session). Pass the name of the feed to the `Name` parameter. Pass the type of the feed to the `Type` parameter. If the feed exists, the function returns `true`. Otherwise, it returns `false`.
     
    Uses the `Feeds_GetFeed` endpoint in ProGet's native API.
 
    .EXAMPLE
    Test-ProGetFeed -Session $ProGetSession -Name 'Apps' -Type 'ProGet'
 
    Demonstrates how to call `Test-ProGetFeed`. In this case, a value of `$true` will be returned if a Universal package feed named 'Apps' exists. Otherwise, `$false`
    #>

    [CmdletBinding()]
    param(
        # The session includes ProGet's URI and the API key. Use `New-ProGetSession` to create session objects
        [Parameter(Mandatory)]
        [pscustomobject] $Session,

        # The feed name indicates the name of the package feed that will be created.
        [Parameter(Mandatory)]
        [Alias('FeedName')]
        [String] $Name,

        # The feed type indicates the type of package feed to create.
        # Valid feed types are ('VSIX', 'RubyGems', 'Docker', 'ProGet', 'Maven', 'Bower', 'npm', 'Deployment', 'Chocolatey', 'NuGet', 'PowerShell') - check here for a latest list - https://inedo.com/support/documentation/proget/feed-types/universal
        [Parameter(Mandatory)]
        [Alias('FeedType')]
        [String] $Type
    )

    Set-StrictMode -Version 'Latest'

    if( !$Session.ApiKey)
    {
        Write-Error -Message ('Failed to test for package feed ''{0}/{1}''. This function uses the ProGet Native API, which requires an API key. When you create a ProGet session with `New-ProGetSession`, provide an API key via the `ApiKey` parameter' -f $FeedType, $FeedName)
        return
    }

    if( $Type -eq 'ProGet' )
    {
        $msg = 'ProGet renamed its "ProGet" feed type name to "Universal". Please update the value of ' +
               'New-ProGetFeed''s "Type" parameter from "ProGet" to "Universal".'
        Write-Warning $msg
        $Type = 'Universal'
    }

    $Parameters = @{
                        'Feed_Name' = $Name;
                    }

    $feed = Invoke-ProGetNativeApiMethod -Session $Session -Name 'Feeds_GetFeed' -Parameter $Parameters
    return ($feed -and ($feed | Get-Member -Name 'FeedType_Name') -and $feed.FeedType_Name -eq $Type)
}


# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.

function Use-CallerPreference
{
    <#
    .SYNOPSIS
    Sets the PowerShell preference variables in a module's function based on the callers preferences.
 
    .DESCRIPTION
    Script module functions do not automatically inherit their caller's variables, including preferences set by common parameters. This means if you call a script with switches like `-Verbose` or `-WhatIf`, those that parameter don't get passed into any function that belongs to a module.
 
    When used in a module function, `Use-CallerPreference` will grab the value of these common parameters used by the function's caller:
 
     * ErrorAction
     * Debug
     * Confirm
     * InformationAction
     * Verbose
     * WarningAction
     * WhatIf
     
    This function should be used in a module's function to grab the caller's preference variables so the caller doesn't have to explicitly pass common parameters to the module function.
 
    This function is adapted from the [`Get-CallerPreference` function written by David Wyatt](https://gallery.technet.microsoft.com/scriptcenter/Inherit-Preference-82343b9d).
 
    There is currently a [bug in PowerShell](https://connect.microsoft.com/PowerShell/Feedback/Details/763621) that causes an error when `ErrorAction` is implicitly set to `Ignore`. If you use this function, you'll need to add explicit `-ErrorAction $ErrorActionPreference` to every function/cmdlet call in your function. Please vote up this issue so it can get fixed.
 
    .LINK
    about_Preference_Variables
 
    .LINK
    about_CommonParameters
 
    .LINK
    https://gallery.technet.microsoft.com/scriptcenter/Inherit-Preference-82343b9d
 
    .LINK
    http://powershell.org/wp/2014/01/13/getting-your-script-module-functions-to-inherit-preference-variables-from-the-caller/
 
    .EXAMPLE
    Use-CallerPreference -Cmdlet $PSCmdlet -SessionState $ExecutionContext.SessionState
 
    Demonstrates how to set the caller's common parameter preference variables in a module function.
    #>

    [CmdletBinding()]
    param (
        [Parameter(Mandatory = $true)]
        #[Management.Automation.PSScriptCmdlet]
        # The module function's `$PSCmdlet` object. Requires the function be decorated with the `[CmdletBinding()]` attribute.
        $Cmdlet,

        [Parameter(Mandatory = $true)]
        [Management.Automation.SessionState]
        # The module function's `$ExecutionContext.SessionState` object. Requires the function be decorated with the `[CmdletBinding()]` attribute.
        #
        # Used to set variables in its callers' scope, even if that caller is in a different script module.
        $SessionState
    )

    Set-StrictMode -Version 'Latest'

    # List of preference variables taken from the about_Preference_Variables and their common parameter name (taken from about_CommonParameters).
    $commonPreferences = @{
                              'ErrorActionPreference' = 'ErrorAction';
                              'DebugPreference' = 'Debug';
                              'ConfirmPreference' = 'Confirm';
                              'InformationPreference' = 'InformationAction';
                              'VerbosePreference' = 'Verbose';
                              'WarningPreference' = 'WarningAction';
                              'WhatIfPreference' = 'WhatIf';
                          }

    foreach( $prefName in $commonPreferences.Keys )
    {
        $parameterName = $commonPreferences[$prefName]

        # Don't do anything if the parameter was passed in.
        if( $Cmdlet.MyInvocation.BoundParameters.ContainsKey($parameterName) )
        {
            continue
        }

        $variable = $Cmdlet.SessionState.PSVariable.Get($prefName)
        # Don't do anything if caller didn't use a common parameter.
        if( -not $variable )
        {
            continue
        }

        if( $SessionState -eq $ExecutionContext.SessionState )
        {
            Set-Variable -Scope 1 -Name $variable.Name -Value $variable.Value -Force -Confirm:$false -WhatIf:$false
        }
        else
        {
            $SessionState.PSVariable.Set($variable.Name, $variable.Value)
        }
    }

}