Pentia.Publish-RuntimeDependencyPackage.psm1

<#
.SYNOPSIS
Publishes the contents of a runtime dependency package to a website. Requires -RunAsAdministrator.
 
.DESCRIPTION
Publishes the contents of a runtime dependency package to a website, using Windows PackageManagement (https://docs.microsoft.com/en-us/powershell/module/PackageManagement/?view=powershell-5.0).
 
Packages are expected to be NuGet-packages and contain any of the following folders:
 
- <package>/Webroot
- <package>/Data
 
All of the above are optional.
 
The following steps are performed during package publishing:
 
1. Check if the required package is cached locally.
1.1 If the package isn't found locally, it's installed from a registered package source, or from the $PackageSource parameter.
2. Copy the contents of the "<package>\Webroot"-folder to the "<WebrootOutputPath>".
3. Copy the contents of the "<package>\Data"-folder to the "<DataOutputPath>".
 
.PARAMETER Package
Optional package definition, as found in a NuGet packages.config file.
If set, "$Package.id" will be used instead of "$PackageName", and "$Package.version" instead of "$PackageVersion".
 
.PARAMETER PackageName
The name of the package to install.
 
.PARAMETER PackageVersion
The exact version of the package to install.
 
.PARAMETER PackageSource
The URI where the package is located. Can be a file path as well.
 
.PARAMETER Username
Optional username required to access the package source.
 
.PARAMETER Password
Optional password required to access the package source.
 
.PARAMETER WebrootOutputPath
The path where the contents of "<package>\Webroot" will be copied to.
 
.PARAMETER DataOutputPath
The path where the contents of "<package>\Data" will be copied to.
 
.EXAMPLE
Publish-RuntimeDependencyPackage -Verbose -PackageName "Sitecore.Full" -PackageVersion "8.2.170407" -PackageSource "http://tund/nuget/nuget/FullSitecore" -WebrootOutputPath "C:\my-website\www" -DataOutputPath "C:\my-website\SitecoreDataFolder"
#>

function Publish-RuntimeDependencyPackage {
    [CmdletBinding()]
    param (
        [Parameter(Mandatory = $true)]
        [string]$PackageName,
        
        [Parameter(Mandatory = $true)]
        [string]$PackageVersion,
        
        [Parameter(Mandatory = $false)]
        [string]$PackageSource,
        
        [Parameter(Mandatory = $false)]
        [string]$Username = [string]::Empty,
        
        [Parameter(Mandatory = $false)]
        [SecureString]$Password = [SecureString]::Empty,
        
        [Parameter(Mandatory = $true)]
        [string]$WebrootOutputPath,

        [Parameter(Mandatory = $true)]
        [string]$DataOutputPath
    )
    Write-Verbose "Publishing package '$PackageName'."
    Write-Verbose "Searching for package '$PackageName' version '$PackageVersion'."
    $package = Get-RuntimeDependencyPackageFromCache -PackageName $PackageName -PackageVersion $PackageVersion
    if (-not $package) {
        Write-Verbose "Package '$PackageName' version '$PackageVersion' not found locally. Installing from '$PackageSource'."
        Install-RuntimeDependencyPackage -PackageName $PackageName -PackageVersion $PackageVersion -PackageSource $PackageSource -Username $Username -Password $Password
        $package = Get-RuntimeDependencyPackageFromCache -PackageName $PackageName -PackageVersion $PackageVersion
    }
    if (-not $package) {
        throw "Unable to install package '$PackageName' version '$PackageVersion' from source '$PackageSource'."
    }
    Copy-RuntimeDependencyPackageContent -Package $package -WebrootOutputPath $WebrootOutputPath -DataOutputPath $DataOutputPath
}

function Get-RuntimeDependencyPackageFromCache {
    [CmdletBinding()]
    param (
        [Parameter(Mandatory = $true)]
        [string]$PackageName,
        
        [Parameter(Mandatory = $true)]
        [string]$PackageVersion
    )
    $packageProvider = "NuGet"
    if (!(Test-PackageProvider $packageProvider)) {
        throw "The package provider '$packageProvider' isn't installed. Run 'Install-PackageProvider -Name $packageProvider' from an elevated PowerShell prompt."
    }
    # The "custom filtering" has been added specifically as a workaround for https://github.com/OneGet/oneget/issues/321.
    Get-Package -ProviderName $packageProvider -AllVersions | Where-Object { ($_.Name -eq $PackageName) -and ($_.Version -eq $PackageVersion) } | Select-Object -First 1
}

function Test-PackageProvider {
    [CmdletBinding()]
    [OutputType([bool])]        
    param (
        [Parameter(Mandatory = $true)]
        [string]$Name
    )
    (Get-PackageProvider | Select-Object -ExpandProperty "Name") -contains $Name
}

function Install-RuntimeDependencyPackage {   
    [CmdletBinding()] 
    param (
        [Parameter(Mandatory = $true)]
        [string]$PackageName,
    
        [Parameter(Mandatory = $true)]
        [string]$PackageVersion,
    
        [Parameter(Mandatory = $false)]
        [string]$PackageSource,
    
        [Parameter(Mandatory = $false)]
        [string]$Username = [string]::Empty,
    
        [Parameter(Mandatory = $false)]
        [SecureString]$Password = [SecureString]::Empty
    )
    $credentials = [System.Management.Automation.PSCredential]::Empty
    if ([string]::IsNullOrWhiteSpace($Username) -eq $false -and [string]::IsNullOrWhiteSpace($Password) -eq $false) {
        $credentials = New-Object System.Management.Automation.PSCredential($Username, $Password)
    }
    try {
        if ([string]::IsNullOrWhiteSpace($PackageSource)) {
            # There's currently a bug in the Find-Package cmdlet which requires us to manually pipe in all available package sources.
            # See https://github.com/OneGet/oneget/issues/270 for details.
            $package = Get-PackageSource -ProviderName "NuGet" | Find-Package -Name $PackageName -RequiredVersion $PackageVersion -Credential $credentials -ErrorAction Stop | Select-Object -First 1
        }
        else {
            $package = Find-Package -Source $PackageSource -Name $PackageName -RequiredVersion $PackageVersion -Credential $credentials -ErrorAction Stop
        }
        Write-Verbose "Installing package '$PackageName'."
        $package | Install-Package -Scope "CurrentUser" -Credential $credentials -Force
    }
    catch {
        if ($_.Exception.Message -match "No match was found for the specified search criteria and package name") {
            throw "The package '$PackageName' version '$PackageVersion' couldn't be found in the source '$PackageSource'. " + 
            "Make sure that all required package sources are set up correctly, e.g. 'Register-PackageSource -Name ""Pentia NuGet"" -Location ""http://tund/nuget/Nuget"" -Trusted -ProviderName ""NuGet""'."
        }
        else {
            throw $_.Exception
        }
    }
}

function Copy-RuntimeDependencyPackageContent {
    [CmdletBinding()]
    param (
        [Parameter(Mandatory = $true)]
        [Microsoft.PackageManagement.Packaging.SoftwareIdentity]$Package,

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

        [Parameter(Mandatory = $true)]
        [string]$DataOutputPath
    )
    
    $packageName = $Package.Name
    Write-Verbose "Copying package contents of '$packageName'."

    $packageDirectory = Get-PackageDirectory -Package $Package

    $webrootSourcePath = [System.IO.Path]::Combine($packageDirectory, "Webroot")
    Copy-PackageFolder -SourceFriendlyName "webroot" -Source $webrootSourcePath -Target $WebrootOutputPath
    
    $dataSourcePath = [System.IO.Path]::Combine($packageDirectory, "Data")
    Copy-PackageFolder -SourceFriendlyName "data" -Source $dataSourcePath -Target $DataOutputPath
}

function Get-PackageDirectory {
    [CmdletBinding()]
    [OutputType([string])]        
    param (
        [Parameter(Mandatory = $true)]
        [Microsoft.PackageManagement.Packaging.SoftwareIdentity]$Package
    )
    Write-Verbose "Determining directory where package '$($Package.Name)' was unpacked."
    # The "FullPath" points to the "unpack directory", e.g. "<package root>\My-Package.1.0.0\".
    if ([System.IO.Path]::IsPathRooted($Package.FullPath) -and [System.IO.Directory]::Exists($Package.FullPath)) {
        $packageDirectory = $Package.FullPath
        Write-Verbose "Package directory determined via `$Package.FullPath ('$packageDirectory')."
        return $packageDirectory
    }
    # The "Source" points to the NuGet package file *inside* the "unpack directory", e.g. "<package root>\My-Package.1.0.0\My-Package.1.0.0.nupgk".
    if ([System.IO.Path]::IsPathRooted($Package.Source) -and [System.IO.File]::Exists($Package.Source)) {
        $packageDirectory = [System.IO.Path]::GetDirectoryName($Package.Source)
        Write-Verbose "Package directory determined via `$Package.Source ('$packageDirectory')."
        return $packageDirectory
    }
    throw "Unable to determine unpack directory of package '$($Package.Name)'. Source: '$($Package.Source)'. FullPath: '$($Package.FullPath)'."
}

function Copy-PackageFolder {
    [CmdletBinding()]
    param (        
        [Parameter(Mandatory = $true)]
        [string]$SourceFriendlyName,
                
        [Parameter(Mandatory = $true)]
        [string]$Source,
    
        [Parameter(Mandatory = $true)]
        [string]$Target
    )

    Write-Verbose "Checking if package has a $SourceFriendlyName folder '$Source'."
    if (Test-Path -Path $Source -PathType Container) {
        Write-Verbose "Copying $SourceFriendlyName files from '$Source' to '$Target'."
        robocopy "$Source" "$Target" *.* /E /MT 64 /NFL /NP /NDL /NJH | Write-Verbose
    }
    else {
        Write-Verbose "No $SourceFriendlyName folder found."
    }
}

Export-ModuleMember -Function Publish-RuntimeDependencyPackage