Private/Install-Module.ps1

<#
.SYNOPSIS
    PowerShell module installation stuff.
    URL: https://github.com/psget/psget
    Based on http://poshcode.org/1875 Install-Module by Joel Bennett
#>

#requires -Version 2.0

#region Setup

Write-Debug 'Set up the global scope config variables.'
if ([Environment]::GetFolderPath('MyDocuments')) {
    $global:UserModuleBasePath = Join-Path -Path ([Environment]::GetFolderPath('MyDocuments')) -ChildPath 'WindowsPowerShell\Modules'
}
else {
    # Support scenarios where PSGet is running without a MyDocuments special folder (e.g. executing within a DSC resource)
    $global:UserModuleBasePath = Join-Path -Path $env:ProgramFiles -ChildPath 'WindowsPowerShell\Modules'
}

# NOTE: Path changed to align with current MS conventions
$global:CommonGlobalModuleBasePath = Join-Path -Path $env:ProgramFiles -ChildPath 'WindowsPowerShell\Modules'

if (-not (Test-Path -Path:variable:global:PsGetDirectoryUrl)) {
    $global:PsGetDirectoryUrl = 'https://github.com/psget/psget/raw/master/Directory.xml'
}
# NOTE: $global:PsGetDestinationModulePath is used by Install-Module as configuration if set by user.

Write-Debug 'Set up needed constants.'
Set-Variable -Name PSGET_ZIP -Value 'ZIP' -Option Constant -Scope Script
Set-Variable -Name PSGET_PSM1 -Value 'PSM1' -Option Constant -Scope Script
Set-Variable -Name PSGET_PSD1 -Value 'PSD1' -Option Constant -Scope Script

#endregion

#region Exported Cmdlets

<#
    .SYNOPSIS
        Installs PowerShell modules from a variety of sources including: Nuget, PsGet module directory, local directory, zipped folder and web URL.
 
    .DESCRIPTION
        Supports installing modules for the current user or all users (if elevated).
 
    .PARAMETER Module
        Name of the module to install.
 
    .PARAMETER ModuleUrl
        URL to the module to install; Can be direct link to PSM1 file or ZIP file. Can be a shortened link.
 
    .PARAMETER ModulePath
        Local path to the module to install.
 
    .PARAMETER ModuleName
       In context with -ModuleUrl or -ModulePath it is not always possible to interfere the right ModuleName, eg. the filename is unknown or the zip archive contains multiple modules.
 
    .PARAMETER Type
        When ModuleUrl or ModulePath specified, allows specifying type of the package. Can be ZIP or PSM1.
 
    .PARAMETER NuGetPackageId
        NuGet package name containing the module to install.
 
    .PARAMETER PackageVersion
        Allows a specific version of the specified NuGet package to used, if not specified then the latest stable version will be used.
 
    .PARAMETER NugetSource
        URL to the NuGet feed containing the package.
 
    .PARAMETER PreRelease
        If PackageVersion is not specified, then this switch allows the latest prerelease package to be used.
 
    .PARAMETER PreReleaseTag
        If PackageVersion is not specified, then this parameter allows the latest version of a particular prerelease tag to be used
 
    .PARAMETER Destination
        When specified the module will be installed below this path. Defaults to '$global:PsGetDestinationModulePath' if defined.
 
    .PARAMETER ModuleHash
        When ModuleHash is specified the chosen module will only be installed if its contents match the provided hash.
 
    .PARAMETER Global
        If set, attempts to install the module to the all users location in C:\Program Files\Common Files\Modules...
 
        NOTE: If the -Destination directory is specified, then -Global will only have an effect in combination with '-PersistEnvironment'. This is also the case if '$global:PsGetDestinationModulePath' is defined.
 
    .PARAMETER DoNotImport
        Indicates that command should not import module after installation
 
    .PARAMETER AddToProfile
        Adds Import-Module statement for installed module to the profile.ps1
 
    .PARAMETER Update
        Forces module to be updated
 
    .PARAMETER DirectoryUrl
        URL to central directory. By default it uses the value in the $global:PsGetDirectoryUrl variable
 
    .PARAMETER PersistEnvironment
        If this switch is specified, the installation destination path will be added to either the User's PSModulePath environment variable or Machine's PSModulePath environment variable (if -Global specified)
 
    .PARAMETER InstallWithModuleName
        Allows to specify the name of the module and override the ModuleName normally used.
        NOTE: This parameter allows to install a module from the PsGet-Directory more than once and PsGet does not remember that this module is installed with a different name.
 
    .PARAMETER DoNotPostInstall
        If defined, the PostInstallHook is not executed.
 
    .PARAMETER PostInstallHook
        Defines the name of a script inside the installed module folder which should be executed after installation.
        Default: definition in directory file or 'Install.ps1'
 
    .PARAMETER Force
        OBSOLETE
        Alternative name for 'Update'.
 
    .PARAMETER Startup
        OBSOLETE
        Alternative name for 'AddToProfile'.
 
    .LINK
        http://psget.net
 
    .EXAMPLE
        # Install-Module PsConfig -DoNotImport
 
        Description
        -----------
        Installs the module witout importing it to the current session
 
    .EXAMPLE
        # Install-Module PoshHg -AddToProfile
 
        Description
        -----------
        Installs the module and then adds impoer of the given module to your profile.ps1 file
 
    .EXAMPLE
        # Install-Module PsUrl
 
        Description
        -----------
        This command will query module information from central registry and install required stuff.
 
    .EXAMPLE
        # Install-Module -ModulePath .\Authenticode.psm1 -Global
 
        Description
        -----------
        Installs the Authenticode module to the System32\WindowsPowerShell\v1.0\Modules for all users to use.
 
    .EXAMPLE
        # Install-Module -ModuleUrl https://github.com/chaliy/psurl/raw/master/PsUrl/PsUrl.psm1
 
        Description
        -----------
        Installs the PsUrl module to the users modules folder
 
    .EXAMPLE
        # Install-Module -ModuleUrl http://bit.ly/e1X4BO -ModuleName "PsUrl"
 
        Description
        -----------
        Installs the PsUrl module with name specified, because command will not be able to guess it
 
    .EXAMPLE
        # Install-Module -ModuleUrl https://github.com/psget/psget/raw/master/TestModules/HelloWorld.zip
 
        Description
        -----------
        Downloads HelloWorld module (module can have more than one file) and installs it
 
    .EXAMPLE
        # Install-Module -NugetPackageId SomePackage
 
        Description
        -----------
        Downloads the latest stable version of the 'SomePackage' module from the NuGet Gallery
 
    .EXAMPLE
        # Install-Module -NugetPackageId SomePackage -PackageVersion 1.0.2-beta
 
        Description
        -----------
        Downloads the specified version of the 'SomePackage' module from the NuGet Gallery
 
    .EXAMPLE
        # Install-Module -NugetPackageId SomePackage -PreRelease
 
        Description
        -----------
        Downloads the latest pre-release version of the 'SomePackage' module from the NuGet Gallery
 
    .EXAMPLE
        # Install-Module -NugetPackageId SomePackage -PreReleaseTag beta -NugetSource http://myget.org/F/myfeed
 
        Description
        -----------
        Downloads the latest 'beta' pre-release version of the 'SomePackage' module from a custom NuGet feed
#>

function Install-Module {
    [CmdletBinding()]
    param (
        [Parameter(Position=0, ValueFromPipeline=$true, ValueFromPipelineByPropertyName=$true, Mandatory=$true, ParameterSetName='CentralDirectory')]
        [String] $Module,

        [Parameter(ValueFromPipelineByPropertyName=$true, Mandatory=$true, ParameterSetName='Web')]
        [String] $ModuleUrl,

        [Parameter(ValueFromPipelineByPropertyName=$true, Mandatory=$true, ParameterSetName='Local')]
        [String] $ModulePath,

        [Parameter(ValueFromPipelineByPropertyName=$true, ParameterSetName='Web')]
        [Parameter(ValueFromPipelineByPropertyName=$true, ParameterSetName='Local')]
        [String] $ModuleName,

        [Parameter(ValueFromPipelineByPropertyName=$true, ParameterSetName='Web')]
        [Parameter(ValueFromPipelineByPropertyName=$true, ParameterSetName='Local')]
        [ValidateSet('ZIP', 'PSM1', 'PSD1', '')] # $script:PSGET_ZIP, $script:PSGET_PSM1 or $script:PSGET_PSD1
        [String] $Type,

        [Parameter(ValueFromPipelineByPropertyName=$true, Mandatory=$true, ParameterSetName='NuGet')]
        [ValidatePattern('^\w+([_.-]\w+)*$')] # regex from NuGet.PackageIdValidator._idRegex
        [ValidateLength(1,100)] # maximum length from NuGet.PackageIdValidator.MaxPackageIdLength
        [String] $NuGetPackageId,

        [Parameter(ValueFromPipelineByPropertyName=$true, ParameterSetName='NuGet')]
        [String] $PackageVersion,

        [Parameter(ValueFromPipelineByPropertyName=$true, ParameterSetName='NuGet')]
        [String] $NugetSource = 'https://nuget.org/api/v2/',

        [Parameter(ValueFromPipelineByPropertyName=$true, ParameterSetName='NuGet')]
        [Switch] $PreRelease,

        [Parameter(ValueFromPipelineByPropertyName=$true, ParameterSetName='NuGet')]
        [String] $PreReleaseTag,

        [Parameter(ValueFromPipelineByPropertyName=$true)]
        [String] $Destination = $global:PsGetDestinationModulePath,

        [Parameter(ValueFromPipelineByPropertyName=$true)]
        [String] $ModuleHash,

        [Parameter(ValueFromPipelineByPropertyName=$true)]
        [Switch] $Global,

        [Parameter(ValueFromPipelineByPropertyName=$true)]
        [Switch] $DoNotImport,

        [Parameter(ValueFromPipelineByPropertyName=$true)]
        [Switch] $AddToProfile,

        [Parameter(ValueFromPipelineByPropertyName=$true)]
        [Switch] $Update,

        [Parameter(ValueFromPipelineByPropertyName=$true)]
        [String] $DirectoryUrl = $global:PsGetDirectoryUrl,

        [Parameter(ValueFromPipelineByPropertyName=$true)]
        [Switch] $PersistEnvironment,

        [Parameter(ValueFromPipelineByPropertyName=$true)]
        [String] $InstallWithModuleName,

        [Parameter(ValueFromPipelineByPropertyName=$true)]
        [Switch] $DoNotPostInstall,

        [Parameter(ValueFromPipelineByPropertyName=$true)]
        [String] $PostInstallHook,

        [Parameter(ValueFromPipelineByPropertyName=$true)]
        [Switch] $Force,

        [Parameter(ValueFromPipelineByPropertyName=$true)]
        [Switch] $Startup
    )
    process {

        if ($Force) {
            Write-Verbose 'Force parameter is considered obsolete. Please use Update instead.'
            $Update = $true
        }

        if ($Startup) {
            Write-Verbose 'Startup parameter is considered obsolete. Please use AddToProfile instead.'
            $AddToProfile = $true
        }

        if (-not $Destination) {
            $Destination = if ($Global) { $global:CommonGlobalModuleBasePath } else { $global:UserModuleBasePath }

            #Because we are using the default location, always ensure it is persisted
            $PersistEnvironment = $true
        }

        if (-not $Destination) {
            throw 'The destination path was not added to the PSModulePath environment variable, ensure you have the rights to modify environment variables'
        }

        $Destination = ConvertTo-CanonicalPath -Path $Destination

        Write-Debug "Execute installation for '$($PSCmdlet.ParameterSetName)' type."

        switch($PSCmdlet.ParameterSetName) {
            CentralDirectory {
                Install-ModuleFromDirectory -Module:$Module -Destination:$Destination -ModuleHash:$ModuleHash -Global:$Global -PersistEnvironment:$PersistEnvironment -DoNotImport:$DoNotImport -AddToProfile:$AddToProfile -Update:$Update -DirectoryUrl:$DirectoryUrl -InstallWithModuleName:$InstallWithModuleName -DoNotPostInstall:$DoNotPostInstall -PostInstallHook:$PostInstallHook
            }
            Web {
                Install-ModuleFromWeb -ModuleUrl:$ModuleUrl -ModuleName:$ModuleName -Type:$Type -Destination:$Destination -ModuleHash:$ModuleHash -Global:$Global -PersistEnvironment:$PersistEnvironment -DoNotImport:$DoNotImport -AddToProfile:$AddToProfile -Update:$Update -InstallWithModuleName:$InstallWithModuleName -DoNotPostInstall:$DoNotPostInstall -PostInstallHook:$PostInstallHook
            }
            Local {
                Install-ModuleFromLocal -ModulePath:$ModulePath -ModuleName:$ModuleName -Type:$Type -Destination:$Destination -ModuleHash:$ModuleHash -Global:$Global -PersistEnvironment:$PersistEnvironment -DoNotImport:$DoNotImport -AddToProfile:$AddToProfile -Update:$Update -InstallWithModuleName:$InstallWithModuleName -DoNotPostInstall:$DoNotPostInstall -PostInstallHook:$PostInstallHook
            }
            NuGet {
                Install-ModuleFromNuGet -NuGetPackageId:$NuGetPackageId -PackageVersion:$PackageVersion -NugetSource:$NugetSource -PreRelease:$PreRelease -PreReleaseTag:$PreReleaseTag -Destination:$Destination -ModuleHash:$ModuleHash -Global:$Global -PersistEnvironment:$PersistEnvironment -DoNotImport:$DoNotImport -AddToProfile:$AddToProfile -Update:$Update -InstallWithModuleName:$InstallWithModuleName -DoNotPostInstall:$DoNotPostInstall -PostInstallHook:$PostInstallHook
            }
            default {
                throw "Unknown ParameterSetName '$($PSCmdlet.ParameterSetName)'"
            }
        }
    }
}

<#
    .SYNOPSIS
        Updates a module.
 
    .DESCRIPTION
        Supports updating modules for the current user or all users (if elevated).
 
    .PARAMETER Module
        Name of the module to update.
 
    .PARAMETER All
        If -All is defined. all to PsGet known modules will be updated.
 
    .PARAMETER Destination
        When specified the module will be updated below this path.
 
    .PARAMETER ModuleHash
        When ModuleHash is specified the chosen module will only be installed if its contents match the provided hash.
 
    .PARAMETER Global
        If set, attempts to install the module to the all users location in Windows\System32...
 
    .PARAMETER DoNotImport
        Indicates that command should not import module after installation.
 
    .PARAMETER AddToProfile
        Adds installed module to the profile.ps1.
 
    .PARAMETER Update
        Forces module to be updated.
 
    .PARAMETER DirectoryUrl
        URL to central directory. By default it uses the value in the $PsGetDirectoryUrl global variable.
 
    .PARAMETER DoNotPostInstall
        If defined, the PostInstallHook is not executed.
 
    .PARAMETER PostInstallHook
        Defines the name of a script inside the installed module folder which should be executed after installation.
        Will not be check in combination with -All switch.
        Default: 'Install.ps1'
 
    .LINK
        http://psget.net
 
    .LINK
        Install-Module
 
    .EXAMPLE
        # Update-Module PsUrl
 
        Description
        -----------
        Updates the module
#>

function Update-Module {
    [CmdletBinding()]
    param (
        [Parameter(Position=0, ValueFromPipeline=$true, ValueFromPipelineByPropertyName=$true, Mandatory=$true)]
        [String] $Module,

        [Parameter(ValueFromPipelineByPropertyName=$true)]
        [Switch] $All,

        [Parameter(ValueFromPipelineByPropertyName=$true)]
        [String] $Destination = $global:PsGetDestinationModulePath,

        [Parameter(ValueFromPipelineByPropertyName=$true)]
        [String] $ModuleHash,

        [Parameter(ValueFromPipelineByPropertyName=$true)]
        [Switch] $Global,

        [Parameter(ValueFromPipelineByPropertyName=$true)]
        [Switch] $DoNotImport,

        [Parameter(ValueFromPipelineByPropertyName=$true)]
        [Switch] $AddToProfile,

        [Parameter(ValueFromPipelineByPropertyName=$true)]
        [String] $DirectoryUrl = $global:PsGetDirectoryUrl,

        [Parameter(ValueFromPipelineByPropertyName=$true)]
        [Switch] $DoNotPostInstall,

        [Parameter(ValueFromPipelineByPropertyName=$true)]
        [String] $PostInstallHook
    )
    process {
        if ($All) {
            Install-Module -Module PSGet -Force -DoNotImport

            Get-PsGetModuleInfo -ModuleName '*' | Where-Object {
                    if ($_.Id -ne 'PSGet') {
                        Get-Module -Name:($_.ModuleName) -ListAvailable
                    }
                } | Install-Module -Update

            Import-Module -Name PSGet -Force -DoNotPostInstall:$DoNotPostInstall

        }
        else {
            Install-Module -Module:$Module -Destination:$Destination -ModuleHash:$ModuleHash -Global:$Global -DoNotImport:$DoNotImport -AddToProfile:$AddToProfile -DirectoryUrl:$DirectoryUrl -Update -DoNotPostInstall:$DoNotPostInstall -PostInstallHook:$PostInstallHook
        }
    }
}

<#
    .SYNOPSIS
        Retrieve information about module from central directory
 
    .DESCRIPTION
        Command will query central directory to get information about module specified.
 
    .PARAMETER ModuleName
        Name of module to look for in directory. Supports wildcards.
 
    .PARAMETER DirectoryUrl
        URL to central directory. By default it uses the value in the $PsGetDirectoryUrl global variable.
 
    .LINK
        http://psget.net
 
    .EXAMPLE
        Get-PsGetModuleInfo PoshCo*
 
        Description
        -----------
        Retrieves information about all registerd modules that starts with PoshCo.
#>

function Get-PsGetModuleInfo {
    [CmdletBinding()]
    param (
        [Parameter(Position=0, ValueFromPipeline=$true, ValueFromPipelineByPropertyName=$true, Mandatory=$true)]
        [String] $ModuleName,
        [Parameter(ValueFromPipelineByPropertyName=$true)]
        [String] $DirectoryUrl = $global:PsGetDirectoryUrl
    )
    begin {
        $client = (new-object Net.WebClient)
        $client.Proxy.Credentials = [System.Net.CredentialCache]::DefaultNetworkCredentials

        $PsGetDataPath = Join-Path -Path $Env:APPDATA -ChildPath psget
        $DirectoryCachePath = Join-Path -Path $PsGetDataPath -ChildPath directorycache.clixml
        $DirectoryCache = @()
        $CacheEntry = $null
        if (Test-Path -Path $DirectoryCachePath) {
            $DirectoryCache = Import-Clixml -Path $DirectoryCachePath
            $CacheEntry = $DirectoryCache | Where-Object { $_.Url -eq $DirectoryUrl } | Select-Object -First 1
        }
        if (-not $CacheEntry) {
            $CacheEntry = @{
                Url = $DirectoryUrl
                File = '{0}.xml' -f [Guid]::NewGuid().Tostring()
                ETag = $null
            }
            $DirectoryCache += @($CacheEntry)
        }
        $CacheEntryFilePath = Join-Path -Path $PsGetDataPath -ChildPath $CacheEntry.File
        if ($CacheEntry -and $CacheEntry.ETag -and (Test-Path -Path $CacheEntryFilePath)) {
            if ((Get-Item -Path $CacheEntryFilePath).LastWriteTime.AddDays(1) -gt (Get-Date)) {
                # use cached directory if it is less than 24 hours old
                $client.Headers.Add('If-None-Match', $CacheEntry.ETag)
            }
        }

        try {
            Write-Verbose "Downloading modules repository from $DirectoryUrl"
            $stream = $client.OpenRead($DirectoryUrl)
            $repoXmlTemp = New-Object -TypeName System.Xml.XmlDocument
            $repoXmlTemp.Load($stream)
            $StatusCode = 200
        }
        catch [System.Net.WebException] {
            $Response = $_.Exception.Response
            if ($Response) { $StatusCode = [int]$Response.StatusCode }
        }

        if ($StatusCode -eq 200) {
            $repoXml = [xml]$repoXmlTemp

            $CacheEntry.ETag = $client.ResponseHeaders['ETag']
            if (-not (Test-Path -Path $PsGetDataPath)) {
                New-Item -Path $PsGetDataPath -ItemType Container | Out-Null
            }
            $repoXml.Save($CacheEntryFilePath)
            Export-Clixml -InputObject $DirectoryCache -Path $DirectoryCachePath
        }
        elseif (Test-Path -Path $CacheEntryFilePath) {
            if ($StatusCode -ne 304) {
                Write-Warning "Could not retrieve modules repository from '$DirectoryUrl'. Status code: $StatusCode"
            }
            Write-Verbose 'Using cached copy of modules repository'
            $repoXml = [xml](Get-Content -Path $CacheEntryFilePath)
        }
        else {
            throw "Could not retrieve modules repository from '$DirectoryUrl'. Status code: $StatusCode"
        }

        $nss = @{ a = 'http://www.w3.org/2005/Atom';
                  pg = 'urn:psget:v1.0' }

        $feed = $repoXml.feed
        $title = $feed.title.innertext
        Write-Verbose "Processing $title feed..."
    }
    process {
        # Very naive, ignoring namespaces and so on.
        $feed.entry | Where-Object { $_.id -like $ModuleName } |
            ForEach-Object {
                $Type = ''
                switch -regex ($_.content.type) {
                    'application/zip' { $Type = $PSGET_ZIP  }
                    default { $Type = $PSGET_PSM1  }
                }

                $Verb = if ($_.properties.Verb -imatch 'POST') { 'POST' } else { 'GET' }

                New-Object PSObject -Property @{
                    Title = $_.title.innertext
                    Description = $_.summary.'#text'
                    Updated = [DateTime]$_.updated
                    Author= $_.author.name
                    Id = $_.id
                    ModuleName = if ($_.properties.ModuleName) { $_.properties.ModuleName } else { $_.id }
                    Type = $Type
                    DownloadUrl = $_.content.src
                    Verb = $Verb
                    #This was changed from using the $_.properties.ProjectUrl because the value for ModuleUrl needs to be the full path to the module file
                    #This change was required to get the tests to pass
                    ModuleUrl = $_.content.src
                    NoPostInstallHook = if ($_.properties.NoPostInstallHook -eq 'true') { $true } else { $false }
                    PostInstallHook = $_.properties.PostInstallHook
                    PostUpdateHook = $_.properties.PostUpdateHook
                } |
                    Select-Object Title, ModuleName, Id, Description, Updated, Type, Verb, ModuleUrl, DownloadUrl, NoPostInstallHook, PostInstallHook, PostUpdateHook
            }
    }
}

<#
    .SYNOPSIS
        Calculate the hash value of a module.
 
    .DESCRIPTION
        Calculate the hash value of the specified module directory for usage with the 'ModuleHash' parameter for validation.
 
    .PARAMETER Path
        Path to the module directory
 
    .EXAMPLE
        Get-PsGetModuleHash $global:UserModuleBasePath\PsGet
 
        Description
        -----------
        Returns the hash value usable with the 'ModuleHash' parameter of 'Install-Module'
 
    .LINK
        Install-Module
 
    .LINK
        http://psget.net
#>

function Get-PsGetModuleHash {
    [CmdletBinding()]
    param (
        [Parameter(Mandatory=$true)]
        [Alias('ModuleBase')]
        [String] $Path
    )
    process {
        Get-FolderHash -Path (Resolve-Path -Path $Path).Path
    }
}

#endregion

#region Sub-Cmdlets

<#
    .SYNOPSIS
        Install a module from the defined PsGet directory.
 
    .PARAMETER Module
        Name of the module to install from PsGet directory.
 
    .PARAMETER Destination
        When specified the module will be installed below this path. Defaults to '$global:PsGetDestinationModulePath' if defined.
 
    .PARAMETER ModuleHash
        When ModuleHash is specified the chosen module will only be installed if its contents match the provided hash.
 
    .PARAMETER Global
        If set, attempts to install the module to the all users location in C:\Program Files\Common Files\Modules...
 
        NOTE: If the -Destination directory is specified, then -Global will only have an effect in combination with '-PersistEnvironment'. This is also the case if '$global:PsGetDestinationModulePath' is defined.
 
    .PARAMETER DoNotImport
        Indicates that command should not import module after installation
 
    .PARAMETER AddToProfile
        Adds Import-Module statement for installed module to the profile.ps1
 
    .PARAMETER Update
        Forces module to be updated
 
    .PARAMETER DirectoryUrl
        URL to central directory. By default it uses the value in the $global:PsGetDirectoryUrl variable
 
    .PARAMETER PersistEnvironment
        If this switch is specified, the installation destination path will be added to either the User's PSModulePath environment variable or Machine's PSModulePath environment variable (if -Global specified)
 
    .PARAMETER InstallWithModuleName
        Allows to specify the name of the module and override the ModuleName normally used.
        NOTE: This parameter allows to install a module from the PsGet-Directory more than once and PsGet does not remember that this module is installed with a different name.
 
    .PARAMETER DoNotPostInstall
        If defined, the PostInstallHook is not executed.
 
    .PARAMETER PostInstallHook
        Defines the name of a script inside the installed module folder which should be executed after installation.
        Default: definition in directory file or 'Install.ps1'
#>

function Install-ModuleFromDirectory {
    [CmdletBinding()]
    param (
        [Parameter(Position=0, ValueFromPipeline=$true, ValueFromPipelineByPropertyName=$true, Mandatory=$true)]
        [String] $Module,

        [Parameter(ValueFromPipelineByPropertyName=$true)]
        [String] $Destination = $global:PsGetDestinationModulePath,

        [Parameter(ValueFromPipelineByPropertyName=$true)]
        [String] $ModuleHash,

        [Parameter(ValueFromPipelineByPropertyName=$true)]
        [Switch] $Global,

        [Parameter(ValueFromPipelineByPropertyName=$true)]
        [Switch] $DoNotImport,

        [Parameter(ValueFromPipelineByPropertyName=$true)]
        [Switch] $AddToProfile,

        [Parameter(ValueFromPipelineByPropertyName=$true)]
        [Switch] $Update,

        [Parameter(ValueFromPipelineByPropertyName=$true)]
        [String] $DirectoryUrl = $global:PsGetDirectoryUrl,

        [Parameter(ValueFromPipelineByPropertyName=$true)]
        [Switch] $PersistEnvironment,

        [Parameter(ValueFromPipelineByPropertyName=$true)]
        [String] $InstallWithModuleName,

        [Parameter(ValueFromPipelineByPropertyName=$true)]
        [Switch] $DoNotPostInstall,

        [Parameter(ValueFromPipelineByPropertyName=$true)]
        [String] $PostInstallHook
    )
    process {
        $testModuleName = if ($InstallWithModuleName) { $InstallWithModuleName } else { $Module }
        if (Test-ModuleInstalledAndImport -ModuleName:$testModuleName -Destination:$Destination -Update:$Update -DoNotImport:$DoNotImport -ModuleHash:$ModuleHash) {
            return
        }

        Write-Verbose "Module $Module will be installed from central repository"
        $moduleData = Get-PsGetModuleInfo -ModuleName:$Module -DirectoryUrl:$DirectoryUrl | select -First 1
        if (-not $moduleData) {
            throw "Module $Module was not found in central repository"
        }

        # $Module and $moduleData.Id are not equally by garantee, so we have to test again.
        if (Test-ModuleInstalledAndImport -ModuleName:$moduleData.ModuleName -Destination:$Destination -Update:$Update -DoNotImport:$DoNotImport -ModuleHash:$ModuleHash) {
            return
        }

        if (-not $DoNotPostInstall) {
            $DoNotPostInstall = $moduledata.NoPostInstallHook
        }

        if (-not $PostInstallHook) {
            if ($Update) {
                $PostInstallHook = $moduleData.PostUpdateHook
            }
            else {
                $PostInstallHook = $moduleData.PostInstallHook
            }

            if (-not $PostInstallHook) {
                $PostInstallHook = 'Install.ps1'
            }
        }

        $result = Invoke-DownloadModuleFromWeb -DownloadUrl:$moduleData.DownloadUrl -ModuleName:$moduleData.ModuleName -Type:$moduleData.Type -Verb:$moduleData.Verb
        Install-ModuleToDestination -ModuleName:$result.ModuleName -InstallWithModuleName:$InstallWithModuleName -ModuleFolderPath:$result.ModuleFolderPath -TempFolderPath:$result.TempFolderPath -Destination:$Destination -ModuleHash:$ModuleHash -Global:$Global -PersistEnvironment:$PersistEnvironment -DoNotImport:$DoNotImport -AddToProfile:$AddToProfile -Update:$Update -DoNotPostInstall:$DoNotPostInstall -PostInstallHook:$PostInstallHook
    }
}

<#
    .SYNOPSIS
        Install a module from a provided download location.
 
    .PARAMETER ModuleUrl
        URL to the module to install; Can be direct link to PSM1 file or ZIP file. Can be a shortened link.
 
    .PARAMETER ModuleName
        It is not always possible to interfere the right ModuleName, eg. the filename is unknown or the zip archive contains multiple modules.
 
    .PARAMETER Type
        When ModuleUrl or ModulePath specified, allows specifying type of the package. Can be ZIP or PSM1.
 
    .PARAMETER Destination
        When specified the module will be installed below this path. Defaults to '$global:PsGetDestinationModulePath' if defined.
 
    .PARAMETER ModuleHash
        When ModuleHash is specified the chosen module will only be installed if its contents match the provided hash.
 
    .PARAMETER Global
        If set, attempts to install the module to the all users location in C:\Program Files\Common Files\Modules...
 
        NOTE: If the -Destination directory is specified, then -Global will only have an effect in combination with '-PersistEnvironment'. This is also the case if '$global:PsGetDestinationModulePath' is defined.
 
    .PARAMETER DoNotImport
        Indicates that command should not import module after installation
 
    .PARAMETER AddToProfile
        Adds Import-Module statement for installed module to the profile.ps1
 
    .PARAMETER Update
        Forces module to be updated
 
    .PARAMETER PersistEnvironment
        If this switch is specified, the installation destination path will be added to either the User's PSModulePath environment variable or Machine's PSModulePath environment variable (if -Global specified)
 
    .PARAMETER InstallWithModuleName
        Allows to specify the name of the module and override the ModuleName normally used.
        NOTE: This parameter allows to install a module from the PsGet-Directory more than once and PsGet does not remember that this module is installed with a different name.
 
    .PARAMETER DoNotPostInstall
        If defined, the PostInstallHook is not executed.
 
    .PARAMETER PostInstallHook
        Defines the name of a script inside the installed module folder which should be executed after installation.
        Default: 'Install.ps1'
#>

function Install-ModuleFromWeb {
    [CmdletBinding()]
    param (
        [Parameter(Position=0, ValueFromPipeline=$true, ValueFromPipelineByPropertyName=$true, Mandatory=$true)]
        [String] $ModuleUrl,

        [Parameter(ValueFromPipelineByPropertyName=$true)]
        [String] $ModuleName,

        [Parameter(ValueFromPipelineByPropertyName=$true)]
        [ValidateSet('ZIP', 'PSM1', 'PSD1', '')] # $script:PSGET_ZIP, $script:PSGET_PSM1 or $script:PSGET_PSD1
        [String] $Type,

        [Parameter(ValueFromPipelineByPropertyName=$true)]
        [String] $Destination = $global:PsGetDestinationModulePath,

        [Parameter(ValueFromPipelineByPropertyName=$true)]
        [String] $ModuleHash,

        [Parameter(ValueFromPipelineByPropertyName=$true)]
        [Switch] $Global,

        [Parameter(ValueFromPipelineByPropertyName=$true)]
        [Switch] $DoNotImport,

        [Parameter(ValueFromPipelineByPropertyName=$true)]
        [Switch] $AddToProfile,

        [Parameter(ValueFromPipelineByPropertyName=$true)]
        [Switch] $Update,

        [Parameter(ValueFromPipelineByPropertyName=$true)]
        [Switch] $PersistEnvironment,

        [Parameter(ValueFromPipelineByPropertyName=$true)]
        [String] $InstallWithModuleName,

        [Parameter(ValueFromPipelineByPropertyName=$true)]
        [Switch] $DoNotPostInstall,

        [Parameter(ValueFromPipelineByPropertyName=$true)]
        [String] $PostInstallHook
    )
    process {
        Write-Verbose "Module will be installed from $ModuleUrl"

        if ($InstallWithModuleName) {
            if (Test-ModuleInstalledAndImport -ModuleName:$InstallWithModuleName -Destination:$Destination -Update:$Update -DoNotImport:$DoNotImport -ModuleHash:$ModuleHash) {
                return
            }
        }

        $result = Invoke-DownloadModuleFromWeb -DownloadUrl:$ModuleUrl -ModuleName:$ModuleName -Type:$Type -Verb:'GET'

        if (-not $PostInstallHook) {
            $PostInstallHook = 'Install.ps1'
        }

        Install-ModuleToDestination -ModuleName:$result.ModuleName -InstallWithModuleName:$InstallWithModuleName -ModuleFolderPath:$result.ModuleFolderPath -TempFolderPath:$result.TempFolderPath -Destination:$Destination -ModuleHash:$ModuleHash -Global:$Global -PersistEnvironment:$PersistEnvironment -DoNotImport:$DoNotImport -AddToProfile:$AddToProfile -Update:$Update -DoNotPostInstall:$DoNotPostInstall -PostInstallHook:$PostInstallHook
    }
}

<#
    .SYNOPSIS
        Install a module from a provided local path.
 
    .PARAMETER ModulePath
        Local path to the module to install.
 
    .PARAMETER ModuleName
        It is not always possible to interfere the right ModuleName, eg. the filename is unknown or the zip archive contains multiple modules.
 
    .PARAMETER Type
        When ModuleUrl or ModulePath specified, allows specifying type of the package. Can be ZIP or PSM1.
 
    .PARAMETER Destination
        When specified the module will be installed below this path. Defaults to '$global:PsGetDestinationModulePath' if defined.
 
    .PARAMETER ModuleHash
        When ModuleHash is specified the chosen module will only be installed if its contents match the provided hash.
 
    .PARAMETER Global
        If set, attempts to install the module to the all users location in C:\Program Files\Common Files\Modules...
 
        NOTE: If the -Destination directory is specified, then -Global will only have an effect in combination with '-PersistEnvironment'. This is also the case if '$global:PsGetDestinationModulePath' is defined.
 
    .PARAMETER DoNotImport
        Indicates that command should not import module after installation
 
    .PARAMETER AddToProfile
        Adds Import-Module statement for installed module to the profile.ps1
 
    .PARAMETER Update
        Forces module to be updated
 
    .PARAMETER PersistEnvironment
        If this switch is specified, the installation destination path will be added to either the User's PSModulePath environment variable or Machine's PSModulePath environment variable (if -Global specified)
 
    .PARAMETER InstallWithModuleName
        Allows to specify the name of the module and override the ModuleName normally used.
        NOTE: This parameter allows to install a module from the PsGet-Directory more than once and PsGet does not remember that this module is installed with a different name.
 
    .PARAMETER DoNotPostInstall
        If defined, the PostInstallHook is not executed.
 
    .PARAMETER PostInstallHook
        Defines the name of a script inside the installed module folder which should be executed after installation.
        Default: 'Install.ps1'
#>

function Install-ModuleFromLocal {
    [CmdletBinding()]
    param (
        [Parameter(Position=0, ValueFromPipeline=$true, ValueFromPipelineByPropertyName=$true, Mandatory=$true)]
        [String] $ModulePath,

        [Parameter(ValueFromPipelineByPropertyName=$true)]
        [String] $ModuleName,

        [Parameter(ValueFromPipelineByPropertyName=$true)]
        [ValidateSet('ZIP', 'PSM1', 'PSD1', '')] # $script:PSGET_ZIP, $script:PSGET_PSM1 or $script:PSGET_PSD1
        [String] $Type,

        [Parameter(ValueFromPipelineByPropertyName=$true)]
        [String] $Destination = $global:PsGetDestinationModulePath,

        [Parameter(ValueFromPipelineByPropertyName=$true)]
        [String] $ModuleHash,

        [Parameter(ValueFromPipelineByPropertyName=$true)]
        [Switch] $Global,

        [Parameter(ValueFromPipelineByPropertyName=$true)]
        [Switch] $DoNotImport,

        [Parameter(ValueFromPipelineByPropertyName=$true)]
        [Switch] $AddToProfile,

        [Parameter(ValueFromPipelineByPropertyName=$true)]
        [Switch] $Update,

        [Parameter(ValueFromPipelineByPropertyName=$true)]
        [Switch] $PersistEnvironment,

        [Parameter(ValueFromPipelineByPropertyName=$true)]
        [String] $InstallWithModuleName,

        [Parameter(ValueFromPipelineByPropertyName=$true)]
        [Switch] $DoNotPostInstall,

        [Parameter(ValueFromPipelineByPropertyName=$true)]
        [String] $PostInstallHook
    )
    process {
        Write-Verbose 'Module will be installed from local path'

        $InstallWithModuleName = if ($InstallWithModuleName) { $InstallWithModuleName } else { $ModuleName }
        if ($InstallWithModuleName) {
            if (Test-ModuleInstalledAndImport -ModuleName:$InstallWithModuleName -Destination:$Destination -Update:$Update -DoNotImport:$DoNotImport -ModuleHash:$ModuleHash) {
                return
            }
        }

        $tempFolderPath = Join-Path ([IO.Path]::GetTempPath()) ([Guid]::NewGuid().ToString())
        New-Item $tempFolderPath -ItemType Directory | Out-Null
        Write-Debug "Temporary work directory created: $tempFolderPath"

        trap { Remove-Item -Path $tempFolderPath -Recurse -Force ; break }

        $newModulePath = Join-Path -Path $tempFolderPath -ChildPath 'module'
        New-Item $newModulePath -ItemType Directory | Out-Null

        if (Test-Path -Path $ModulePath -PathType Leaf) {
            $extension = (Get-Item $ModulePath).Extension
            if ($extension -eq '.psm1') {
                $Type = if ($Type) { $Type } else { $PSGET_PSM1 }
            } elseif ($extension -eq '.zip') {
                $Type = if ($Type) { $Type } else { $PSGET_ZIP }
            }

            if ($Type -eq $PSGET_ZIP) {
                Expand-ZipModule $ModulePath $newModulePath
            }
            else {
                Copy-Item -Path $ModulePath -Destination $newModulePath
            }
        }
        elseif (Test-Path -Path $ModulePath -PathType Container) {
            Copy-Item -Path $ModulePath -Destination $newModulePath -Force -Recurse
        }
        else {
            throw "ModulePath '$ModulePath' does not point to an module."
        }

        $foundResult = Find-ModuleNameAndFolder -Path $newModulePath -ModuleName $ModuleName

        if (-not $PostInstallHook) {
            $PostInstallHook = 'Install.ps1'
        }

        Install-ModuleToDestination -ModuleName:$foundResult.ModuleName -InstallWithModuleName:$InstallWithModuleName -ModuleFolderPath:$foundResult.ModuleFolderPath -TempFolderPath:$tempFolderPath -Destination:$Destination -ModuleHash:$ModuleHash -Global:$Global -PersistEnvironment:$PersistEnvironment -DoNotImport:$DoNotImport -AddToProfile:$AddToProfile -Update:$Update -DoNotPostInstall:$DoNotPostInstall -PostInstallHook:$PostInstallHook
    }
}

<#
    .SYNOPSIS
        Install a module from a NuGet source.
 
    .PARAMETER NuGetPackageId
        NuGet package name containing the module to install.
 
    .PARAMETER PackageVersion
        Allows a specific version of the specified NuGet package to used, if not specified then the latest stable version will be used.
 
    .PARAMETER NugetSource
        URL to the NuGet feed containing the package.
 
    .PARAMETER PreRelease
        If PackageVersion is not specified, then this switch allows the latest prerelease package to be used.
 
    .PARAMETER PreReleaseTag
        If PackageVersion is not specified, then this parameter allows the latest version of a particular prerelease tag to be used
 
    .PARAMETER Destination
        When specified the module will be installed below this path. Defaults to '$global:PsGetDestinationModulePath' if defined.
 
    .PARAMETER ModuleHash
        When ModuleHash is specified the chosen module will only be installed if its contents match the provided hash.
 
    .PARAMETER Global
        If set, attempts to install the module to the all users location in C:\Program Files\Common Files\Modules...
 
        NOTE: If the -Destination directory is specified, then -Global will only have an effect in combination with '-PersistEnvironment'. This is also the case if '$global:PsGetDestinationModulePath' is defined.
 
    .PARAMETER DoNotImport
        Indicates that command should not import module after installation
 
    .PARAMETER AddToProfile
        Adds Import-Module statement for installed module to the profile.ps1
 
    .PARAMETER Update
        Forces module to be updated
 
    .PARAMETER PersistEnvironment
        If this switch is specified, the installation destination path will be added to either the User's PSModulePath environment variable or Machine's PSModulePath environment variable (if -Global specified)
 
    .PARAMETER InstallWithModuleName
        Allows to specify the name of the module and override the ModuleName normally used.
        NOTE: This parameter allows to install a module from the PsGet-Directory more than once and PsGet does not remember that this module is installed with a different name.
 
    .PARAMETER DoNotPostInstall
        If defined, the PostInstallHook is not executed.
 
    .PARAMETER PostInstallHook
        Defines the name of a script inside the installed module folder which should be executed after installation.
        Default: 'Install.ps1'
#>

function Install-ModuleFromNuGet {
    [CmdletBinding()]
    param (
        [Parameter(Position=0, ValueFromPipeline=$true, ValueFromPipelineByPropertyName=$true, Mandatory=$true)]
        [ValidatePattern('^\w+([_.-]\w+)*$')] # regex from NuGet.PackageIdValidator._idRegex
        [ValidateLength(1,100)] # maximum length from NuGet.PackageIdValidator.MaxPackageIdLength
        [String] $NuGetPackageId,

        [Parameter(ValueFromPipelineByPropertyName=$true)]
        [String] $PackageVersion,

        [Parameter(ValueFromPipelineByPropertyName=$true)]
        [String] $NugetSource = 'https://nuget.org/api/v2/',

        [Parameter(ValueFromPipelineByPropertyName=$true)]
        [Switch] $PreRelease,

        [Parameter(ValueFromPipelineByPropertyName=$true)]
        [String] $PreReleaseTag,

        [Parameter(ValueFromPipelineByPropertyName=$true)]
        [String] $Destination = $global:PsGetDestinationModulePath,

        [Parameter(ValueFromPipelineByPropertyName=$true)]
        [String] $ModuleHash,

        [Parameter(ValueFromPipelineByPropertyName=$true)]
        [Switch] $Global,

        [Parameter(ValueFromPipelineByPropertyName=$true)]
        [Switch] $DoNotImport,

        [Parameter(ValueFromPipelineByPropertyName=$true)]
        [Switch] $AddToProfile,

        [Parameter(ValueFromPipelineByPropertyName=$true)]
        [Switch] $Update,

        [Parameter(ValueFromPipelineByPropertyName=$true)]
        [Switch] $PersistEnvironment,

        [Parameter(ValueFromPipelineByPropertyName=$true)]
        [String] $InstallWithModuleName,

        [Parameter(ValueFromPipelineByPropertyName=$true)]
        [Switch] $DoNotPostInstall,

        [Parameter(ValueFromPipelineByPropertyName=$true)]
        [String] $PostInstallHook
    )
    process {
        Write-Verbose 'Module will be installed from NuGet'
        $InstallWithModuleName = if ($InstallWithModuleName) { $InstallWithModuleName } else { $NuGetPackageId }

        if (Test-ModuleInstalledAndImport -ModuleName:$InstallWithModuleName -Destination:$Destination -Update:$Update -DoNotImport:$DoNotImport -ModuleHash:$ModuleHash) {
            return
        }

        if (-not $PostInstallHook) {
            $PostInstallHook = 'Install.ps1'
        }

        try {
            $result = Invoke-DownloadNugetPackage -NuGetPackageId $NuGetPackageId -PackageVersion $PackageVersion -Source $NugetSource -PreRelease:$PreRelease -PreReleaseTag $PreReleaseTag
            Install-ModuleToDestination -ModuleName:$result.ModuleName -InstallWithModuleName:$InstallWithModuleName -ModuleFolderPath:$result.ModuleFolderPath -TempFolderPath:$result.TempFolderPath -Destination:$Destination -ModuleHash:$ModuleHash -Global:$Global -PersistEnvironment:$PersistEnvironment -DoNotImport:$DoNotImport -AddToProfile:$AddToProfile -Update:$Update -DoNotPostInstall:$DoNotPostInstall -PostInstallHook:$PostInstallHook
        }
        catch {
            Write-Error $_.Exception.Message
            return
        }
    }
}

#endregion

#region Internal Cmdlets
#region Module Installation
<#
    .SYNOPSIS
        Adds value to a "Path" type of environment variable (PATH or PSModulePath). Path type of variables munge the User and Machine values into the value for the current session.
 
    .PARAMETER Global
        The System.EnvironmentVariableTarget of what type of environment variable to modify ("Machine","User" or "Session")
 
    .PARAMETER PathToAdd
        The actual path to add to the environment variable
 
    .PARAMETER PersistEnvironment
        If specified, will permanently store the variable in registry
 
    .EXAMPLE
        AddPathToPSModulePath -Scope "Machine" -PathToAdd "$env:CommonProgramFiles\Modules"
 
        Description
        -----------
        This command add the path "$env:CommonProgramFiles\Modules" to the Machine PSModulePath environment variable
#>

function Add-PathToPSModulePath {
    [CmdletBinding()]
    param (

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

        [switch] $PersistEnvironment,

        [switch] $Global
    )
    process {
        $PathToAdd = ConvertTo-CanonicalPath -Path $PathToAdd

        if(-not $PersistEnvironment) {
            if (-not ($env:PSModulePath.Contains($PathToAdd))) {
                Write-Warning "Module install destination `"$PathToAdd`" is not included in the PSModulePath environment variable."
            }
            return
        }

        $scope = 'User'
        if ($Global) {
            Write-Verbose 'The Machine environment variable PSModulePath will be modified.'
            $scope = 'Machine'
        }

        $pathValue = '' + [Environment]::GetEnvironmentVariable('PSModulePath', $scope)

        if (-not ($pathValue.Contains($PathToAdd))) {
            if ($pathValue -eq '') {
                Write-Debug "PSModulePath for scope '$scope' was read empty. Setting PowerShell default instead."
                if ($scope -eq 'User') {
                    $pathValue = Join-Path -Path ([Environment]::GetFolderPath('MyDocuments')) -ChildPath 'WindowsPowerShell\Modules'
                }
                else {
                    $pathValue = Join-Path -Path $PSHOME -ChildPath 'Modules'
                }
            }

            if (-not ($pathValue.Contains($PathToAdd))) {
                $pathValue = "$pathValue;$PathToAdd"
            }

            [Environment]::SetEnvironmentVariable('PSModulePath', $pathValue, $scope)

            Update-PSModulePath

            Write-Host """$PathToAdd"" is added to the PSModulePath environment variable"
        }
        else {
            Write-Verbose """$PathToAdd"" already exists in PSModulePath environment variable"
        }
    }
}

<#
    .SYNOPSIS
        Standardize the provided path.
 
    .DESCRIPTION
        A simple routine to standardize path formats.
 
    .PARAMETER Path
#>

function ConvertTo-CanonicalPath {
    [CmdletBinding()]
    param(
        [Parameter(Mandatory=$true)]
        [String] $Path
    )
    process {
        return [IO.Path]::GetFullPath(($Path.Trim()))
    }
}

<#
    .SYNOPSIS
        Find the module file in the given path.
 
    .PARAMETER Path
        Path of module
 
    .PARAMETER ModuleName
        Name of the Module
#>

function Get-ModuleFile {
    [CmdletBinding()]
    param(
        [Parameter(Position=0, Mandatory=$true)]
        [String] $Path,

        [String] $ModuleName = '*'
    )
    process {
        $Includes = Get-PossibleModuleFileNames -ModuleName $ModuleName

        # Sort by folder length ensures that we use one from root folder(Issue #12)
        $DirectoryNameLengthProperty = @{
            E = { $_.DirectoryName.Length }
        }

        # sort by Includes to give PSD1 preference over PSM1, etc
        $IncludesPreferenceProperty = @{
            E = {
                for ($Index = 0; $Index -lt $Includes.Length; $Index++) {
                    if ($_.Name -like $Includes[$Index]) { break }
                }
                $Index
            }
        }

        Get-ChildItem -Path $Path -Include $Includes -Recurse |
            Where-Object { -not $_.PSIsContainer } |
            Sort-Object -Property $DirectoryNameLengthProperty, $IncludesPreferenceProperty |
            Select-Object -ExpandProperty FullName -First 1
    }
}

<#
    .SYNOPSIS
        Get list of possible names for the module file.
 
    .PARAMETER ModuleName
        Name of the module
#>

function Get-PossibleModuleFileNames {
    [CmdletBinding()]
    param(
        [Parameter(Position=0, Mandatory=$true)]
        [String] $ModuleName
    )
    process {
        'psd1','psm1','ps1','dll','cdxml','xaml' | ForEach-Object { "$ModuleName`.$_" }
    }
}

<#
    .SYNOPSIS
        Search in the provided folder for a module, if possible with the provided name.
 
    .PARAMETER Path
        Path to search in for the module.
 
    .PARAMETER ModuleName
        ModuleName which is expected.
#>

function Find-ModuleNameAndFolder {
    [CmdletBinding()]
    param(
        [Parameter(Mandatory=$true)]
        [String] $Path,

        [String] $ModuleName
    )
    process {
        if ($ModuleName) {
            $moduleFile = Get-ModuleFile -Path $Path -ModuleName $ModuleName
            if (-not $moduleFile) {
                throw "Could not find a module with name '$ModuleName' in the provided file."
            }
        }
        else {
            $moduleFile = Get-ModuleFile -Path $Path
            if (-not $moduleFile) {
                throw 'Could not find any module in the provided file.'
            }
            $ModuleName = [IO.Path]::GetFileNameWithoutExtension($moduleFile)
        }

        $moduleFolderPath = Split-Path -Path $moduleFile

        return @{
            ModuleName = $ModuleName
            ModuleFolderPath = $moduleFolderPath
        }
    }
}

<#
    .SYNOPSIS
        Import given modele
 
    .DESCRIPTION
        Import given module with switch -Global (functions available to other modules) and avoid
        a Powershell bug related to binary modules.
 
    .$
#>

function Import-ModuleGlobally {
    [CmdletBinding()]
    param (
        [String] $ModuleName,
        [String] $ModuleBase,
        [Switch] $Force
    )
    process {
        Write-Verbose "Importing installed module '$ModuleName' from '$($installedModule.ModuleBase)'"
        Import-Module -Name $ModuleBase -Global -Force:$Force

        # For psget no further checks are needed and their execution cause
        # an error for the update process of 'psget'
        # https://github.com/psget/psget/issues/186
        if ($ModuleName -eq 'PsGet') {
            return
        }

        $IdentityExtension = [System.IO.Path]::GetExtension((Get-ModuleFile -Path $ModuleBase -ModuleName $ModuleName))
        if ($IdentityExtension -eq '.dll') {
            # import module twice for binary modules to workaround PowerShell bug:
            # https://connect.microsoft.com/PowerShell/feedback/details/733869/import-module-global-does-not-work-for-a-binary-module
            Import-Module -Name $ModuleBase -Global -Force:$Force
        }
    }
}

<#
    .SYNOPSIS
        Download module from URL
 
    .DESCRIPTION
        Download module from URL and try to interfere unknown parameter.
        If download target is a zip-archive it will be extracted.
 
        Returns a map containing the TempFolderPath, ModuleFolderPath and ModuleName.
        The TempFolderPath should be removed after processing the result.
 
    .PARAMETER DownloadUrl
        URL to the module delivery file.
 
    .PARAMETER ModuleName
        Name of the module.
 
    .PARAMETER Type
        Type of the module delivery file.
 
    .PARAMETER Verb
        Http method used for download.
#>

function Invoke-DownloadModuleFromWeb {
    [CmdletBinding()]
    param (
        [Parameter(Mandatory=$true)]
        [String] $DownloadUrl,

        [String] $ModuleName,

        [String] $Type,

        [String] $Verb
    )

    $tempFolderPath = Join-Path -Path ([IO.Path]::GetTempPath()) -ChildPath ([Guid]::NewGuid().ToString())
    New-Item -Path $tempFolderPath -ItemType Directory | Out-Null
    Write-Debug "Temporary work directory created: $tempFolderPath"

    # make certain that the tempFolder will be deleted if there is an error
    trap { Remove-Item -Path $tempFolderPath -Recurse -Force; break }

    Write-Verbose "Downloading module from $DownloadUrl"
    $client = (new-object Net.WebClient)
    $client.Proxy.Credentials = [System.Net.CredentialCache]::DefaultNetworkCredentials
    $downloadFilePath = Join-Path -Path $tempfolderPath -ChildPath 'download'
    if ($Verb -eq 'POST') {
        $client.Headers['Content-type'] = 'application/x-www-form-urlencoded'
        [IO.File]::WriteAllBytes($downloadFilePath, $client.UploadData($DownloadUrl, ''))
    }
    else {
        $client.DownloadFile($DownloadUrl, $downloadFilePath)
    }

    $candidateName = '{undefined}'
    $contentDisposition = $client.ResponseHeaders['Content-Disposition']
    Write-Debug "Try to get module file name from content disposition header: Content-Disposition = '$contentDisposition'"

    if ($contentDisposition -match '\bfilename="?(?<name>[^/]+)\.(?<ext>psm1|zip)"?') {
        $candidateName = $Matches.name
        $Type = if ($Type) { $Type } elseif ($Matches.ext -eq 'psm1') { $PSGET_PSM1 } elseif ($Matches.ext -eq 'zip') { $PSGET_ZIP }
    }
    else {
        Write-Debug "Try to get module file name from url: '$DownloadUrl'"
        if ($DownloadUrl -match '\b(?<name>[^/]+)\.(?<ext>psm1|zip)[\#\?]*') {
            $candidateName = $Matches.name
            $Type = if ($Type) { $Type } elseif ($Matches.ext -eq 'psm1') { $PSGET_PSM1 } elseif ($Matches.ext -eq 'zip') { $PSGET_ZIP }
        }
        else {
            $locationHeader = $client.ResponseHeaders['Location']
            Write-Debug "Check location header in case of redirect: '$locationHeader'"
            if ($locationHeader -match '\b(?<name>[^/]+)\.(?<ext>psm1|zip)[\#\?]*') {
                $candidateName = $Matches.name
                $Type = if ($Type) { $Type } elseif ($Matches.ext -eq 'psm1') { $PSGET_PSM1 } elseif ($Matches.ext -eq 'zip') { $PSGET_ZIP }
            }
        }
    }

    Write-Debug "Invoke-DownloadModuleFromWeb: CandidateName = '$candidateName'"

    if (-not $Type) {
        $contentType = $client.ResponseHeaders['Content-Type']
        Write-Debug "Download header Content-Type: '$contentType'"
        if ($contentType -eq 'application/zip') {
            $type = $PSGET_ZIP
        }
        # check downloaded file for the PKZip header
        elseif ((Get-Item -Path $downloadFilePath).Length -gt 4) {
            Write-Debug 'Search for PKZipHeader'
            $knownPKZipHeader = 0x50, 0x4b, 0x03, 0x04
            $fileHeader = Get-Content -Path $downloadFilePath -Encoding Byte -TotalCount 4
            if ([System.BitConverter]::ToString($knownPKZipHeader) -eq [System.BitConverter]::ToString($fileHeader)) {
                Write-Debug 'Found PKZipHeader => Type = ZIP'
                $type = $PSGET_ZIP
            }
            else {
                Write-Debug 'No PKZipHeader found => Type -ne ZIP'
            }
        }

        if (-not $Type) {
            Write-Debug 'If its most likely no zip it has to be an PSM1 file.'
            $Type = $PSGET_PSM1
        }
    }

    $moduleFolderPath = Join-Path -Path $tempFolderPath -ChildPath 'module'
    New-Item -Path $moduleFolderPath -ItemType Directory | Out-Null

    switch ($Type) {
        $PSGET_ZIP {
            $zipFilePath = $downloadFilePath + '.zip'
            Move-Item -Path $downloadFilePath -Destination $zipFilePath
            Expand-ZipModule -Path $zipFilePath -Destination $moduleFolderPath
        }
        $PSGET_PSM1 {
            if (-not $ModuleName) {
                if ($candidateName -eq '{undefined}') {
                    throw 'Cannot guess module name. Try specifying ModuleName argument!'
                }
                $ModuleName = $candidateName
            }

            $psmFilePath = Join-Path -Path $moduleFolderPath -ChildPath "$ModuleName.psm1"
            Move-Item -Path $downloadFilePath -Destination $psmFilePath
        }
        default {
            throw "Type $Type is not supported yet"
        }
    }

    $foundResult = Find-ModuleNameAndFolder -Path $moduleFolderPath -ModuleName $ModuleName

    Write-Debug "Invoke-DownloadModuleFromWeb: ModuleName = '$ModuleName'"

    return @{
        TempFolderPath = $tempFolderPath
        ModuleFolderPath = $foundResult.ModuleFolderPath
        ModuleName = $foundResult.ModuleName
    }
}

<#
    .SYNOPSIS
        Install the provided module into the defined destination.
 
    .DESCRIPTION
        Install the module inside of the provided directory into the defined destination
        and perform the following steps:
 
        * Rename module if requestes by provided InstallWithModuleName
        * If a ModuleHash is provided, check if it matches.
        * Add the destination path to the PSModulePath if necessary (depends on provided parameters)
        * Place the conventions-matching module folder in the destination folder
        * Import the module if necessary
        * Add the profile import to profile if necessary
 
    .PARAMETER ModuleName
        The name of the module.
 
    .PARAMETER InstallWithModuleName
        The name the module should get.
 
    .PARAMETER ModuleFolderPath
        The path to the module data, which contains the module main file, named according to ModuleName
 
    .PARAMETER TempFolderPath
        TempPath used by PsGet for doing the work. Contains the ModuleFolderPath and will be deleted after processing,
 
    .PARAMETER Destination
        Path to which the module will be installed.
 
    .PARAMETER ModuleHash
        When ModuleHash is specified the chosen module will only be installed if its contents match the provided hash.
 
    .PARAMETER Global
        Influence the PSModulePath changes and profile changes.
 
    .PARAMETER PersistEnvironment
        Defines if the PSModulePath changes should be persistent.
 
    .PARAMETER DoNotImport
        Defines if the installed module should be imported.
 
    .PARAMETER AddToProfile
        Defines if an 'Import-Module' statement should be added to the profile.
 
    .PARAMETER Update
        Defines if an already existing folder in the target may be deleted for installation of the module.
 
    .PARAMETER DoNotPostInstall
        If defined, the PostInstallHook is not executed.
 
    .PARAMETER PostInstallHook
        Defines the name of a script inside the installed module folder which should be executed after installation.
#>

function Install-ModuleToDestination {
    [CmdletBinding()]
    param (
        [Parameter(Mandatory=$true)]
        [String] $ModuleName,

        [Parameter(Mandatory=$true)]
        [String] $ModuleFolderPath,

        [Parameter(Mandatory=$true)]
        [String] $TempFolderPath,

        [Parameter(Mandatory=$true)]
        [String] $Destination,

        [String] $InstallWithModuleName,

        [String] $ModuleHash,

        [Switch] $Global,

        [Switch] $PersistEnvironment,

        [Switch] $DoNotImport,

        [Switch] $AddToProfile,

        [Switch] $Update,

        [Switch] $DoNotPostInstall,

        [String] $PostInstallHook
    )
    process {
        # Make certain the temp folder is deleted
        trap { Remove-Item -Path $TempFolderPath -Recurse -Force; break }

        $InstallWithModuleName = if ($InstallWithModuleName) { $InstallWithModuleName } else { $ModuleName }
        # Case: no $InstallWithModuleName and module name interfered from install files
        if (Test-ModuleInstalledAndImport -ModuleName:$InstallWithModuleName -Destination:$Destination -Update:$Update -DoNotImport:$DoNotImport -ModuleHash:$ModuleHash) {
            Remove-Item -Path $TempFolderPath -Recurse -Force
            return
        }

        $moduleFilePath = Get-ModuleFile -Path $ModuleFolderPath -ModuleName $ModuleName
        # sanity checks
        if (-not $moduleFilePath) {
            throw 'BUG! Module installation failed in step Install-ModuleToDestination. Please report this issue including your command line.'
        }
        if ($ModuleFolderPath -ne (Split-Path -Path $moduleFilePath)) {
            throw 'BUG! Module installation failed in step Install-ModuleToDestination. Please report this issue including your command line.'
        }

        if ($InstallWithModuleName -ne $ModuleName) {
            Rename-Item -Path $moduleFilePath -NewName ($InstallWithModuleName + (Get-Item $moduleFilePath).Extension)
        }

        $targetFolderPath = Join-Path -Path $Destination -ChildPath $InstallWithModuleName

        if ($ModuleHash) {
            Write-Verbose 'Ensure that the hash of the module matches the specified hash'

            $newModuleHash = Get-PsGetModuleHash -Path $ModuleFolderPath
            Write-Verbose "Hash of module in '$ModuleFolderPath' is: $newModuleHash"
            if ($ModuleHash -ne $newModuleHash) {
                throw 'Module contents do not match specified module hash. Ensure the expected hash is correct and the module source is trusted.'
            }

            if ( Test-Path $targetFolderPath ) {
                Write-Verbose 'Module already exists in destination path. Check if hash in destination is correct. If not replace with to be installed module.'
                $destinationModuleHash = Get-PsGetModuleHash -Path $targetFolderPath
                if ($destinationModuleHash -ne $ModuleHash ) {
                    $Update = $true
                }
            }
        }

        #Add the Destination path to the User or Machine environment
        Add-PathToPSModulePath -PathToAdd:$Destination -PersistEnvironment:$PersistEnvironment -Global:$Global

        if (-not (Test-Path $targetFolderPath)) {
            New-Item $targetFolderPath -ItemType Directory -ErrorAction Continue -ErrorVariable FailMkDir | Out-Null
            ## Handle the error if they asked for -Global and don't have permissions
            if ($FailMkDir -and @($FailMkDir)[0].CategoryInfo.Category -eq 'PermissionDenied') {
                throw "You do not have permission to install a module to '$Destination'. You may need to be elevated."
            }
            Write-Verbose "Create module folder at $targetFolderPath"
        }

        Write-Debug 'Empty existing module folder before copying new files.'
        Get-ChildItem -Path $targetFolderPath -Force | Remove-Item -Force -Recurse -ErrorAction Stop

        Write-Debug 'Copy module files to destination folder'
        Get-ChildItem -Path $ModuleFolderPath | Copy-Item -Destination $targetFolderPath -Force -Recurse

        if (-not $DoNotPostInstall) {
            Write-Verbose "PostInstallHook $PostInstallHook"
            if ($PostInstallHook -like '*.ps1') {
                $postInstallScript = Join-Path -Path $targetFolderPath -ChildPath $PostInstallHook
                if (Test-Path -Path $postInstallScript -PathType Leaf) {
                    Write-Verbose "'$PostInstallHook' found in module. Let's execute it."
                    & $postInstallScript
                }
                else {
                    Write-Verbose "PostInstallHook '$PostInstallHook' not found."
                }
            }
        }

        $isDestinationInPSModulePath = $env:PSModulePath.Contains($Destination)
        if ($isDestinationInPSModulePath) {
            if (-not (Get-Module $InstallWithModuleName -ListAvailable)) {
                throw 'For some unexpected reasons module was not installed.'
            }
        }
        else {
            if (-not (Get-ModuleFile -Path $targetFolderPath)) {
                throw 'For some unexpected reasons module was not installed.'
            }
        }

        if ($Update) {
            Write-Host "Module $InstallWithModuleName was successfully updated." -Foreground Green
        }
        else {
            Write-Host "Module $InstallWithModuleName was successfully installed." -Foreground Green
        }

        if (-not $DoNotImport) {
            Import-ModuleGlobally -ModuleName:$InstallWithModuleName -ModuleBase:$targetFolderPath -Force:$Update
        }

        if ($isDestinationInPSModulePath -and $AddToProfile) {
            # WARNING $Profile is empty on Win2008R2 under Administrator
            if ($PROFILE) {
                if (-not (Test-Path $PROFILE)) {
                    Write-Verbose "Creating PowerShell profile...`n$PROFILE"
                    New-Item $PROFILE -Type File -Force -ErrorAction Stop
                }

                if (Select-String $PROFILE -Pattern "Import-Module $InstallWithModuleName") {
                    Write-Verbose "Import-Module $InstallWithModuleName command already in your profile"
                }
                else {
                    $signature = Get-AuthenticodeSignature -FilePath $PROFILE

                    if ($signature.Status -eq 'Valid') {
                        Write-Error "PsGet cannot modify code-signed profile '$PROFILE'."
                    }
                    else {
                        Write-Verbose "Add Import-Module $InstallWithModuleName command to the profile"
                        "`nImport-Module $InstallWithModuleName" | Add-Content $PROFILE
                    }
                }
            }

        }

        Write-Debug "Cleanup temporary work folder '$TempFolderPath'"
        Remove-Item -Path $TempFolderPath -Recurse -Force
    }
}

<#
    .SYNOPSIS
        Test if module is installed and import it then.
 
    .DESCRIPTION
        Test if module with provided name is installed in the target destination.
        If it is installed, it will be imported. Returns '$true' if installed.
 
    .PARAMETER ModuleName
        Name of the module
 
    .PARAMETER Destination
        Installation destination
 
    .PARAMETER Update
        If 'Update'-switch is set, this returns always '$true'.
 
    .PARAMETER DoNotImport
        Switch suppress the import of module.
 
    .PARAMETER ModuleHash
        If a hash is provided an installed module will only be accepted as installed if the hash match.
#>

function Test-ModuleInstalledAndImport {
    [CmdletBinding()]
    param (
        [Parameter(Mandatory=$true)]
        [String] $ModuleName,

        [Parameter(Mandatory=$true)]
        [String] $Destination,

        [Switch] $Update,

        [Switch] $DoNotImport,

        [String] $ModuleHash
    )
    process {
        if ($Update) {
            #TODO: This implementation is more like the old -Force flag, because this will force an installation also if no installation in destination exists.
            Write-Verbose "Ignoring if module with name '$ModuleName' is already installed because of update mode."
            return $false
        }

        $installedModule = Get-Module -Name $ModuleName -ListAvailable

        if ($installedModule) {
            if ($installedModule.Count -gt 1) {
                $targetModule = $installedModule | Where-Object { (ConvertTo-CanonicalPath -Path (Split-Path $_.ModuleBase)) -eq $Destination } | Select-Object -First 1

                if (-not $targetModule) {
                    Write-Warning "Module with name '$ModuleName' was not found in '$Destination'. But it was found in:`n $($installedModule.ModuleBase | Format-List | Out-String)"
                    return $false
                }

                Write-Warning "The module '$ModuleName' was installed at more than one location. Installed paths:`n`t$($installedModule.ModuleBase | Format-List | Out-String)`n'$($firstInstalledModule.ModuleBase)' is the searched destination."
                $installedModule = $targetModule
            }
            elseif ((Split-Path $installedModule.ModuleBase) -ne $Destination) {
                Write-Verbose "Module with name '$ModuleName' was found in '$($installedModule.ModuleBase)' but not in '$Destination'."
                return $false
            }
        }
        else {
            $candidateModulePath = Join-Path -Path $Destination -ChildPath $ModuleName
            $possibleModuleFileNames = Get-PossibleModuleFileNames -ModuleName $ModuleName

            if (Test-Path -Path $candidateModulePath\* -Include $possibleModuleFileNames -PathType Leaf) {
                Write-Verbose "Module with name '$ModuleName' found in '$Destination' (note: destination is not in PSModulePath)"
                $installedModule = @{ ModuleBase = $CandidateModulePath }
            }
            else {
                Write-Verbose "Module with name '$ModuleName' is not installed."
                return $false
            }
        }

        if ($ModuleHash) {
            $installedModuleHash = Get-PsGetModuleHash -Path $installedModule.ModuleBase
            Write-Verbose "Hash of module in '$($installedModule.ModuleBase)' is: $InstalledModuleHash"
            if ($ModuleHash -ne $installedModuleHash) {
                Write-Verbose "Expected '$ModuleHash' but calculated '$installedModuleHash'."
                return $false
            }
        }

        Write-Verbose "'$ModuleName' already installed. Use -Update if you need update"

        if ($DoNotImport -eq $false) {
            Import-ModuleGlobally -ModuleName $ModuleName -ModuleBase $installedModule.ModuleBase -Force:$Update
        }

        return $true
    }
}

<#
    .SYNOPSIS
        Extract the content of the referenced zip file to the defind destination
 
    .PARAMATER Path
        Path to a zip file with the file extension '.zip'
 
    .Parameter Destination
        Path to which the zip content is extracted
#>

function Expand-ZipModule {
    [CmdletBinding()]
    param (
        [Parameter(Position=0, Mandatory=$true)]
        [String] $Path,

        [Parameter(Position=1, Mandatory=$true)]
        [String] $Destination
    )
    process {
        Write-Debug "Unzipping $Path to $Destination..."

        # Check if powershell v3+ and .net v4.5 is available
        $netFailed = $true
        if ( $PSVersionTable.PSVersion.Major -ge 3 -and (Get-ChildItem -Path 'HKLM:\SOFTWARE\Microsoft\NET Framework Setup\NDP\v4' -Recurse | Get-ItemProperty -Name Version | Where-Object { $_.Version -like '4.5*' -Or $_.Version -ge '4.5' }) ) {
            Write-Debug 'Attempting unzip using the .NET Framework...'

            try {
                [System.Reflection.Assembly]::LoadWithPartialName("System.IO.Compression.FileSystem")
                [System.IO.Compression.ZipFile]::ExtractToDirectory($Path, $Destination)
                $netFailed = $false
            }
            catch {
            }
        }

        if ($netFailed) {
            try {
                Write-Debug 'Attempting unzip using the Windows Shell...'
                $shellApp = New-Object -Com Shell.Application
                $shellZip = $shellApp.NameSpace([String]$Path)
                $shellDest = $shellApp.NameSpace($Destination)
                $shellDest.CopyHere($shellZip.items())
            }
            catch {
                $shellFailed = $true
            }
        }

        # if failure already registered or no result
        if (($netFailed -and $shellFailed) -or ((Get-ChildItem $Destination | Measure-Object | Where-Object { $_.Count -eq 0}))) {
            Write-Warning 'We were unable to decompress the downloaded module. This tends to mean both of the following are true:'
            Write-Warning '1. You''ve disabled Windows Explorer Zip file integration or are running on Windows Server Core.'
            Write-Warning '2. You don''t have the .NET Framework 4.5 installed.'
            Write-Warning 'You''ll need to correct at least one of the above issues depending on your installation to proceed.'
            throw 'Unable to unzip downloaded module file!'
        }
    }
}

<#
    .SYNOPSIS
        Update '$env:PSModulePath' from 'User' and 'Machine' scope envrionment variables
#>

function Update-PSModulePath {
    process {
        # powershell default
        $psModulePath = "$env:ProgramFiles\WindowsPowershell\Modules\"

        $machineModulePath = [Environment]::GetEnvironmentVariable('PSModulePath', 'Machine')
        if (-not $machineModulePath) {
            # powershell default
            $machineModulePath = Join-Path -Path $PSHOME -ChildPath 'Modules'
        }

        $userModulePath = [Environment]::GetEnvironmentVariable('PSModulePath', 'User')
        if (-not $userModulePath) {
            # powershell default
            $userModulePath = Join-Path -Path ([Environment]::GetFolderPath('MyDocuments')) -ChildPath 'WindowsPowerShell\Modules'
        }

        $newSessionValue = "$userModulePath;$machineModulePath;$psModulePath"

        #Set the value in the current process
        [Environment]::SetEnvironmentVariable('PSModulePath', $newSessionValue, 'Process')
    }
}
#endregion

#region NuGet Handling
<#
    .SYNOPSIS
        Download a module of type NuGet package
 
    .PARAMETER NuGetPackageId
        NuGet package id
 
    .PARAMETER PackageVersion
        Specific version to be installed. If not defined, install newest.
 
    .PARAMETER Source
        NuGet source url
 
    .PARAMETER PreRelease
        If no PackageVersion is defined, may PreReleases be used?
 
    .PARAMETER PreReleaseTag
        If PreReleases may be used, also use prereleases of a special tag?
#>

function Invoke-DownloadNuGetPackage {
    [CmdletBinding()]
    param (
        [Parameter(Mandatory=$true)]
        [String] $NuGetPackageId,

        [String] $PackageVersion,

        [Parameter(Mandatory=$true)]
        [String] $Source,

        [Switch] $PreRelease,

        [String] $PreReleaseTag
    )
    process {
        $WebClient = New-Object -TypeName System.Net.WebClient
        $WebClient.Proxy.Credentials = [System.Net.CredentialCache]::DefaultNetworkCredentials

        if (-not $Source.EndsWith('/')) {
            $Source += '/'
        }

        Write-Verbose "Querying '$Source' repository for package with Id '$NuGetPackageId'"
        try {
            $Url = "{1}Packages()?`$filter=tolower(Id)+eq+'{0}'&`$orderby=Id" -f $NuGetPackageId.ToLower(), $Source
            Write-Debug "Trying NuGet query url: $Url"
            $XmlDoc = [xml]$WebClient.DownloadString($Url)
        }
        catch {
            try {
                $Url = "{1}Packages(Id='{0}')?`$orderby=Id" -f $NuGetPackageId, $Source
                Write-Debug "Trying NuGet query url: $Url"
                $XmlDoc = [xml]$WebClient.DownloadString($Url)
            }
            catch {
                throw "Unable to download from NuGet feed: $($_.Exception.InnerException.Message)"
            }
        }

        if ($PackageVersion) {
            # version regexs can be found in the NuGet.SemanticVersion class
            $Entry = $XmlDoc.feed.entry |
                Where-Object { $_.properties.Version -eq $PackageVersion } |
                Select-Object -First 1
        }
        else {
            $Entry = Find-LatestNugetPackageFromFeed -Feed:$XmlDoc.feed.entry -PreRelease:$PreRelease -PreReleaseTag:$PreReleaseTag
        }

        if ($Entry) {
            $PackageVersion = $Entry.properties.Version
            Write-Verbose "Found NuGet package version '$PackageVersion'"
        }
        else {
            throw ("Cannot find NuGet package '$NuGetPackageId $PackageVersion' [PreRelease='{0}', PreReleaseTag='{1}']" -f $PreRelease, $PreReleaseTag)
        }

        $DownloadUrl = $Entry.content.src
        Write-Verbose "Downloading NuGet package from '$DownloadUrl'"
        $DownloadResult = Invoke-DownloadModuleFromWeb -DownloadUrl:$DownloadUrl -ModuleName:$NugetPackageId
        return $DownloadResult
    }
}

<#
    .SYNOPSIS
        Find the latest release in the provided NuGet feed for the NuGet package id.
 
    .PARAMETER Feed
        Xml feed node for NuGet package
 
    .PARAMETER PreRelease
        If no PackageVersion is defined, may PreReleases be used?
 
    .PARAMETER PreReleaseTag
        If PreReleases may be used, also use prereleases of a special tag?
#>

function Find-LatestNugetPackageFromFeed {
    [CmdletBinding()]
    param
    (
        [Object[]] $Feed,

        [Switch] $PreRelease,

        [String] $PreReleaseTag
    )
    process {
        # From NuGet.SemanticVersion - https://github.com/Haacked/NuGet/blob/master/src/Core/SemanticVersion.cs
        $semVerRegex = "^(?<Version>\d+(\s*\.\s*\d+){0,3})(?<Release>-[a-z][0-9a-z-]*)?$"
        $semVerStrictRegex = "^(?<Version>\d+(\.\d+){2})(?<Release>-[a-z][0-9a-z-]*)?$"

        # find only stable versions
        $stableRegex = "^(\d+(\s*\.\s*\d+){0,3})?$"
        # find stable and prerelease versions
        $preReleaseRegex = "^(\d+(\s*\.\s*\d+){0,3})(-[a-z][0-9a-z-]*)?$"
        # find only a specific prerelease versions
        $specificPreReleaseRegex = "^(\d+(\s*\.\s*\d+){{0,3}}-{0}[0-9a-z-]*)?$" -f $preReleaseTag

        # Set the required search expression
        $searchRegex = $stableRegex
        if ($preRelease) { $searchRegex = $preReleaseRegex }
        if ($preReleaseTag) { $searchRegex = $specificPreReleaseRegex }

        $packages = $feed | Where-Object {

            ($_.properties.Version) -match $searchRegex
        }

        return ($packages | Select -Last 1)
    }
}

#endregion

#region Module Hashing
<#
    .SYNOPSIS
        Calculate a hash for the given file
 
    .PARAMETER Path
        File path for hasing
#>

function Get-FileHash {
    [CmdletBinding()]
    param (
        [Parameter(Mandatory=$true, ValueFromPipelineByPropertyName = $true)]
        [Alias('FullName')]
        [String] $Path
    )
    begin {
        $Algorithm = New-Object -TypeName System.Security.Cryptography.SHA256Managed
    }
    process {
        if (-not (Test-Path -Path $Path -PathType Leaf)) {
            Write-Error "Cannot find file: $Path"
            return
        }

        $Stream = [System.IO.File]::OpenRead($Path)
        try {
            $HashBytes = $Algorithm.ComputeHash($Stream)
            [BitConverter]::ToString($HashBytes) -replace '-',''
        }
        finally {
            $Stream.Close()
        }
    }
}

<#
    .SYNOPSIS
        Calculate a hash for the given directory.
 
    .PARAMETER Path
        Path to the folder which should be hashed.
#>

function Get-FolderHash {
    [CmdletBinding()]
    param (
        [Parameter(Mandatory=$true)]
        [String] $Path
    )
    process {
        if (-not (Test-Path -Path $Path -PathType Container)) {
            throw "Cannot find folder: $Path"
        }

        $Path = $Path + '\' -replace '\\\\$','\\'
        $PathPattern = '^' + [Regex]::Escape($Path)

        $ChildHashes = Get-ChildItem -Path $Path -Recurse -Force |
            Where-Object { -not $_.PSIsContainer } |
            ForEach-Object {
                New-Object -TypeName PSObject -Property @{
                    RelativePath = $_.FullName -replace $PathPattern, ''
                    Hash = Get-FileHash -Path $_.FullName
                }
            }

        $Text = @($ChildHashes |
            Sort-Object -Property RelativePath |
            ForEach-Object {
                '{0} {1}' -f $_.Hash, $_.RelativePath
            }) -join '`r`n'

        Write-Debug "TEXT>$Text<TEXT"

        $Algorithm = New-Object -TypeName System.Security.Cryptography.SHA256Managed
        $HashBytes = $Algorithm.ComputeHash([System.Text.Encoding]::UTF8.GetBytes($Text))
        [BitConverter]::ToString($HashBytes) -replace '-',''
    }
}

#endregion

#endregion

#region TabExpansion
# Back Up TabExpansion if needed
# Idea is stolen from posh-git + ps-get
$tabExpansionBackup = 'PsGet_DefaultTabExpansion'
if ((Test-Path -Path Function:\TabExpansion -ErrorAction SilentlyContinue) -and -not (Test-Path -Path Function:\$tabExpansionBackup -ErrorAction SilentlyContinue)) {
    Rename-Item -Path Function:\TabExpansion $tabExpansionBackup -ErrorAction SilentlyContinue
}

# Revert old tabexpnasion when module is unloaded
# this does not cover all paths, but most of them
# Idea is stolen from PowerTab
$Module = $MyInvocation.MyCommand.ScriptBlock.Module
$Module.OnRemove = {
    Write-Debug 'Revert tab expansion back'
    Remove-Item -Path Function:\TabExpansion -ErrorAction SilentlyContinue
    if (Test-Path -Path Function:\$tabExpansionBackup) {
        Rename-Item -Path Function:\$tabExpansionBackup Function:\TabExpansion
    }
}

function TabExpansion {
    [CmdletBinding()]
    param(
        [String] $line,
        [String] $lastWord
    )
    process {
        if ($line -eq "Install-Module $lastword" -or $line -eq "inmo $lastword" -or $line -eq "ismo $lastword" -or $line -eq "upmo $lastword" -or $line -eq "Update-Module $lastword") {
            Get-PsGetModuleInfo -ModuleName "$lastword*" | % { $_.Id } | sort -Unique
        }
        elseif ( Test-Path -Path Function:\$tabExpansionBackup ) {
            & $tabExpansionBackup $line $lastWord
        }
    }
}
#endregion

#region Module Interface
Set-Alias -Name inmo -Value Install-Module #Obsolete
Set-Alias -Name ismo -Value Install-Module
Set-Alias -Name upmo -Value Update-Module

Export-ModuleMember Install-Module
Export-ModuleMember Update-Module
Export-ModuleMember Get-PsGetModuleInfo
Export-ModuleMember Get-PsGetModuleHash
Export-ModuleMember TabExpansion
Export-ModuleMember -Alias inmo
Export-ModuleMember -Alias ismo
Export-ModuleMember -Alias upmo
#endregion