Commands/Install-OpenPackage.ps1

function Install-OpenPackage
{
    <#
    .SYNOPSIS
        Installs an OpenPackage
    .DESCRIPTION
        Installs an OpenPackage into a destination on disk.
    .NOTES
        Will also install a `.zip` and `.op` of the package to the parent directory of the installation
    .LINK
        Expand-Archive
    #>

    [CmdletBinding(SupportsShouldProcess,PositionalBinding=$false)]
    [Alias(
        'Install-OP', 'inop', 'inOpenPackage',
        'Expand-OpenPackage','Expand-OP', 'enop','enOpenPackage'
    )]
    param(
    # The arguments to Get-OpenPackage.
    [Parameter(ValueFromRemainingArguments)]
    [Alias('Arguments','Args','At','Url', 'AtUri', 'FilePath','Repository','Nuget')]
    [PSObject[]]
    $ArgumentList,

    <#
    
    The destination path.
    
    If provided, this should be a directory, but can be a file.

    If multiple packages will be installed and a -DestinationPath was provided,
    all packages will be installed into that destination path.
    
    If no destination path is provided,
    only packages with an identifier will be installed.

    Packages will install beneath the first `$env:OpenPackagePath`.
    
    If the package has a version, it will install into a versioned subdirectory.

    #>
    
    [Parameter()]
    [ValidateNotNullOrEmpty()]
    [string]
    $DestinationPath,

    <#
    Includes the specified parts.
    
    Enter a wildcard pattern, such as `*.txt`
    #>

    [ValidateNotNullOrEmpty()]
    [SupportsWildcards()]
    [string[]]
    $Include,
    
    <#
    Excludes the specified parts.
    
    Enter a wildcard pattern, such as `*.txt`
    #>

    [ValidateNotNullOrEmpty()]
    [SupportsWildcards()]
    [string[]]
    $Exclude,

    <#
    Includes the specified content types.
    
    Enter a wildcard pattern, such as `text/*`
    #>

    [ValidateNotNullOrEmpty()]
    [SupportsWildcards()]
    [string[]]
    $IncludeContentType,
    
    <#
    Excludes the specified content types.
    
    Enter a wildcard pattern, such as `text/*`
    #>

    [ValidateNotNullOrEmpty()]
    [SupportsWildcards()]
    [string[]]
    $ExcludeContentType,

    # The input object. If this is not a package, it will be passed thru.
    [Parameter(ValueFromPipeline)]
    [Alias('Package')]
    [PSObject]
    $InputObject,    

    # If set, will overwrite existing files.
    [switch]
    $Force,

    # If set, will clear the destination directory before installing.
    [Alias('Clean')]
    [switch]
    $Clear,

    # If set, will output the files that are expanded from the package.
    [switch]
    $PassThru
    )
    
    # We want to collect all piped input first,
    # so we can use the implicit `end` block.
    # And collect all the piped input by enumerating `$input`.
    $allInput = @($input)
    
    # We will be using Select-OpenPackage for filtering, so get a reference to it now.
    $selectOpenPackage = $ExecutionContext.SessionState.InvokeCommand.GetCommand('Select-OpenPackage','Function')
        
    # Get our packages
    # Each server can have any number of packages
    # The order packages are defined is the order they are resolved
    # This allows us to have any number of layers, in any order we want.
    $packages = @(
        # First up, lets process our input objects
        # (piped in objects come first)
        $remainingInput = @()                        
        foreach ($in in $allInput) {
            # Anything that is a package works
            if ($in -is [IO.Packaging.Package]) {
                $in                    
            } 
            # so does anything that has a .Package property
            elseif (
                $in.Package -is [IO.Packaging.Package]
            ) {
                $in.Package
            }
            # anything else we will pipe to Get-OpenPackage
            else
            {
                $remainingInput += $in
            }
        }
        # Now lets check a bound -InputObject
        # If piped in, this will potentially be a duplicated
        # (because `$InputObject` will contain the last bound value)
        foreach ($in in $InputObject) {
            # Skip any input we already have
            if ($allInput -contains $in) {
                continue
            } 
            # If the -InputObject was a package
            if ($in -is [IO.Packaging.Package]) {
                $in # this works
            }
            # Otherwise, if the -InputObject has a .Package
            elseif (
                $in.Package -is [IO.Packaging.Package] -and 
                # and it is not a package we already have collected
                ($allInput.Package -notcontains $in.Package)
            ) {
                # then .Package works.
                $in.Package
            }
            # Otherwise, we will pipe remaining input to Get-OpenPackage
            elseif ($remainingInput -notcontains $in) {
                $remainingInput += $in
            }
        }
        # If there was remaining input
        if ($remainingInput) {
            # pipe it to Get-OpenPackage
            $remainingInput | Get-OpenPackage @ArgumentList
        }
        # If we had arguments,
        elseif ($ArgumentList) {            
            # call Get-OpenPackage.
            Get-OpenPackage @ArgumentList
        }
    )    

    # Packages can only be installed once per execution of this function.
    # So we will need to keep track of already installed packages
    $alreadyInstalled = @()
    
    # We will also want to keep track of what we have cleared, so we can install layers.
    $Cleared = @()

    # Keep track of any files we might overwrite
    $existingFiles = @()

    # Go over each package we may have
    foreach ($package in $packages) {
        # skip anything that is not a package
        if ($package -isnot [IO.Packaging.Package]) {
            continue
        }
        # or that has already been installed
        if ($alreadyInstalled -contains $package) {
            continue
        }
        # If no DestinationPath was provided
        if (-not $PSBoundParameters['DestinationPath']) {
            # Check for $env:OpenPackagePath
            if (-not $env:OpenPackagePath) {
                # error out if missing.
                Write-Error '$env:OpenPackagePath not defined'
                return
            }
            # If there is no identifier
            if (-not $package.Identifier) {
                # error out
                Write-Error "Must provide -DestinationPath or have a package identifier"
                return
            } 
            
            # Set the destionation path
            $PSBoundParameters['DestinationPath'] = $destinationPath =
                Join-Path (
                    # based off of the $env:OpenPackagePath
                    @($env:OpenPackagePath -split $(
                        if (-not ($IsLinux -or $IsMacOS)) { ';' }
                        else { ':' }
                    ))[0]
                ) $package.Identifier # and the identifier

            # If the package had a version
            if ($package.Version) {
                # put it within the versioned directory
                $PSBoundParameters['DestinationPath'] = $destinationPath =
                    Join-Path $DestinationPath $package.Version
            }
        }

        # Copy our parameters to Select-OpenPackage
        $selectSplat = [Ordered]@{InputObject=$package}
        foreach ($key in $PSBoundParameters.Keys) {
            if ($selectOpenPackage.Parameters[$key]) {
                $selectSplat[$key] = $PSBoundParameters[$key]
            }
        }
            
        # Get all of the package parts
        $inputParts = @(Select-OpenPackage @selectSplat)                                
                    
        # Now let's prepare our progress bars
        $total = $inputParts.Length
        $counter = 0 
        $Progress = [Ordered]@{
            Activity = "Expanding $($package.PackageProperties.Identifier)"
            Id = Get-Random            
        }
                    

        # If the destination path exist and has not been cleared
        if (
            (Test-Path $DestinationPath) -and 
            $Cleared -notcontains $DestinationPath
        ) {
            # Clear it if we want to
            if ($Clear -and $psCmdlet.ShouldProcess("Clear $destinationPath")) {
                Remove-Item -ErrorAction Ignore -Path $DestinationPath -Recurse -Force:$Clear
            } else {
                # and warn if we do not.
                Write-Warning "$DestinationPath exists. Use -Clear to clear the directory."
            }
            # Add it to the cleared directories either way, so we do not over warn.
            $cleared += $DestinationPath
        }        
        
        # Go over each part
        :nextPart foreach ($part in $inputParts) {
            # Find their destination on disk
            $partDestination = Join-Path $DestinationPath ([Web.HttpUtility]::UrlDecode($part.Uri))
            $counter++
            $Progress.Status = $part.Uri
            $Progress.PercentComplete = $counter * 100 / $total
            # and write progress.
            Write-Progress @Progress
            # Then check if it exists.
            $fileInfo = 
                if ((Test-Path -LiteralPath $partDestination)) {
                    if (-not $Force) {
                        # We will warn when we're done,
                        # but don't -Force the point by warning each time.
                        # Add it to the list of existing files
                        $existingFile = [IO.FileInfo]"$partDestination"
                        if ($existingFile) {
                            $existingFiles += $existingFile
                            if ($passThru) {
                                $PSCmdlet.WriteObject($existingFiles[-1])
                            }
                        }                        
                        continue nextPart # (and continue to the next part).
                    }                    
                    New-Item -ItemType File -Path $partDestination -Force
                } else {
                    # create a file if it did not exist.
                    New-Item -ItemType File -Path $partDestination -Force
                }
            
            # If we do not have a file,
            if ($fileInfo -isnot [IO.FileInfo]) {
                # continue to the next part
                continue nextPart
            }
                        
            # Open the file for write
            $fileStream = $fileInfo.OpenWrite()
            # and continue if that did not work for any reason (for example, the file being locked)
            if (-not $?) { continue nextPart }
            # Get the part stream
            $partStream = $part.GetStream()
            # copy it to the file
            $partStream.CopyTo($fileStream)
            # and close and dispose of them both
            $fileStream.Close()
            $fileStream.Dispose()

            $partStream.Close()
            $partStream.Dispose()

            # If we are passing thru
            if ($PassThru) {
                # get the exported files.
                Get-Item -LiteralPath $fileInfo.FullName |
                    Add-Member NoteProperty Package $InputObject -Force -PassThru |
                    Add-Member NoteProperty PartUri $part.Uri -Force -PassThru |
                    Add-Member NoteProperty PartContentType $part.ContentType -Force -PassThru
            }                        
        }        

        # After we have expanded all of the parts
        $Progress.Remove('PercentComplete')
        $Progress.Completed = $true
        # complete our progress
        Write-Progress @Progress

        # Mark this package as installed
        $alreadyInstalled += $package         
    }

    if ($existingFiles) {
        Write-Warning "$($existingFiles.Length) Files Exist (Use ``-Force`` to overwrite):$(
            [Environment]::NewLine
            $existingFiles -join [Environment]::NewLine
        )"

    }
}