GitLabProvider.psm1

. $PSScriptRoot\HelperFunctions.ps1

$Platform = [System.Environment]::OSVersion.Platform # Win32NT / MacOSX / Unix

function Initialize-Provider {
    Write-Verbose "Initializing provider $ProviderName"
    # does not execute!
}

function Get-PackageProviderName {
    # actual initialization
    if (-not $Initialized) {
        [System.Net.ServicePointManager]::SecurityProtocol = 'Tls12'
        $ConfigFolder = if ($Platform -eq 'Win32NT') {
            'C:\ProgramData\GitLabProvider'
        } else {
            "$PSHome\GitLabProvider"
        }
        if (-not (Test-Path $ConfigFolder)) { New-Item -Type Directory -Path $ConfigFolder }
        $script:RegisteredPackageSourcesPath = "$ConfigFolder\PackageSources.json"
        [array]$script:RegisteredPackageSources = if (Test-Path $RegisteredPackageSourcesPath) {
            Get-Content $RegisteredPackageSourcesPath | ConvertFrom-Json | % {
                Add-PackageSource -Name $_.Name -Location $_.Location -Trusted $_.IsTrusted
            }
        } else { @() }
    
        $script:InstalledPackagesPath = "$ConfigFolder\InstalledPackages.json"
        $script:Initialized = $true
    }

    return 'GitLab'
}

function Get-Feature {
    New-Feature -Name 'supports-powershell-modules'
}

function Get-DynamicOptions {
    param(
        [Parameter(Mandatory)]
        [Microsoft.PackageManagement.MetaProvider.PowerShell.OptionCategory] $Category
    )
    switch ($Category) {
        Package {} # for Find-Package
        Source {} # for Add/Remove-PackageSource
        Provider {} # not used
        # for Install/Uninstall/Get-InstalledPackage
        Install {
            New-DynamicOption -Category $Category -Name Location -ExpectedType String -IsRequired $false
            New-DynamicOption -Category $Category -Name User -ExpectedType String -IsRequired $false
            #New-DynamicOption -Category $Category -Name System -ExpectedType String -IsRequired $false
        }
    }
}

function Add-PackageSource {
    [CmdletBinding()]
    param(
        [Parameter(Mandatory)]
        [string] $Name,
        [Parameter(Mandatory)]
        [string] $Location,
        [Parameter(Mandatory)]
        [bool] $Trusted
    )
    $PSBoundParameters.Registered = $true
    $PackageSource = New-PackageSource @PSBoundParameters
    $script:RegisteredPackageSources += $PackageSource
    Dump-RegisteredPackageSources
    $PackageSource
}

function Remove-PackageSource {
    param(
        [Parameter(Mandatory)]
        [string] $Name
    )
    $PackageSource = $script:RegisteredPackageSources | ? Name -eq $Name
    if (-not $PackageSource) {
        $msg = 'Package source matching the specified name is not registered'
        Write-Error -Message $msg -ErrorId PackageSourceNotFound -Category InvalidOperation -TargetObject $Name
    } else {
        $script:RegisteredPackageSources = @($script:RegisteredPackageSources) -ne $PackageSource
        Dump-RegisteredPackageSources
    }
}

function Resolve-PackageSources {
    $SourceName = $request.PackageSources
    if (-not $SourceName) {
        return $script:RegisteredPackageSources
    }
    
    $SourceName | % {
        if ($request.IsCanceled) { return }
        $PackageSource = $script:RegisteredPackageSources | ? Name -like $_
        if (-not $PackageSource) {
            $msg = "Package source matching the name $_ not registered"
            Write-Error -Message $msg -ErrorId PackageSourceNotFound -Category InvalidOperation -TargetObject $_
        } else { $PackageSource }
    }
}

function Find-Package { 
    param(
        #[Parameter(Mandatory)
        [string[]] $Name,
        [string] $RequiredVersion,
        [string] $MinimumVersion,
        [string] $MaximumVersion = "$([int]::MaxValue).0"
    )
    if (-not $MinimumVersion) {
        $MinimumVersion = '0.0'
    }
    if (-not $MaximumVersion) {
        $MaximumVersion = "$([int]::MaxValue).0"
    }
    $Options = $request.Options
    $Sources = Get-PackageSources $request
    foreach ($Source in $Sources) {
        if ($request.IsCanceled) { return }
        $h = @{Headers = $Source.Headers}
        if (-not $Name) { $Name = '*' }
        $Name | % {
            if ($_ -eq '*') {
                $Projects = Invoke-RestMethod @h ($Source.Location + '/projects')
            } else {
                $Projects = Invoke-RestMethod @h ($Source.Location + "/projects?search=$_")
            }
            
            foreach ($Project in $Projects) {
                $ProjectId = $Project.id
                $Tags = Invoke-RestMethod @h ($Source.Location + "/projects/$ProjectId/repository/tags")
                $Tags | Sort-Object name -Descending | ? { [System.Version]($_.name) -ge $MinimumVersion -and
                            [System.Version]($_.name) -le $MaximumVersion -and
                            (-not $RequiredVersion -or $_.name -eq $RequiredVersion)
                } -pv Tag | % {
                    $TagName = $Tag.name
                    $CommitId = $Tag.commit.id

                    # retrieve dependencies
                    $RepositoryTree = Invoke-RestMethod @h ($Source.Location + "/projects/$ProjectId/repository/tree?ref_name=$CommitId")
                    
                    $ManifestFileBlobId = ($RepositoryTree | ? Name -like *.psd1).id
                    $ManifestFilePath = [System.IO.Path]::GetTempFileName()
                    Invoke-WebRequest @h ($Source.Location + "/projects/$ProjectId/repository/blobs/$ManifestFileBlobId/raw") -OutFile $ManifestFilePath
                    $ModuleManifest = Invoke-Expression (Get-Content $ManifestFilePath -Raw)
                    rm $ManifestFilePath

                    $SubmodulesFileBlobId = ($RepositoryTree | ? Name -eq .gitmodules).id
                    if ($SubmodulesFileBlobId) {
                        $SubmodulesFilePath = [System.IO.Path]::GetTempFileName()
                        Invoke-WebRequest @h ($Source.Location + "/projects/$ProjectId/repository/blobs/$SubmodulesFileBlobId/raw") -OutFile $SubmodulesFilePath
                        $Submodules = Get-GitSubmodules $SubmodulesFilePath
                        rm $SubmodulesFilePath
                    }

                    # GitLab / PSGallery / chocolatey / nuget
                    $Dependencies = New-Object System.Collections.ArrayList
                    @($ModuleManifest.PrivateData.RequiredPackages) -ne $null | % {
                        $Dependency = $_.CanonicalId.Split(':/#') # 'nuget:Microsoft.Exchange.WebServices/2.2#nuget.org'
                        [void]$Dependencies.Add((New-Dependency @Dependency))
                    }
                    $Swid = @{
                        Name = $Project.name
                        Version = $TagName #[System.Version]$Tag
                        VersionScheme = 'MultiPartNumeric'
                        Source = $Source.Name
                        Summary = $Project.description
                        FullPath = $Source.Location + "/projects/$ProjectId/repository/archive.zip?sha=$TagName" # zip download link
                        FromTrustedSource = $true
                        Filename = ''
                        SearchKey = ''
                        Details = @{
                            CommitId = $CommitId
                            Submodules = @($Submodules)
                        }
                        Entities = @()
                        Links = @()
                        Dependencies = $Dependencies # array of json
                        #TagId <string>
                    }
                    $Swid.FastPackageReference = $Swid | ConvertTo-Json -Depth 3
                    New-SoftwareIdentity @Swid
                    if (-not $Options.AllVersions) { continue }
                }
            }
        }
    }
}

function Download-Package {
    param(
        [Parameter(Mandatory)]
        [ValidateNotNullOrEmpty()]
        [string] $FastPackageReference,
        [Parameter(Mandatory)]
        [ValidateNotNullOrEmpty()]
        [string] $Location
    )

    $Options = $request.Options
    $Sources = Get-PackageSources $request
    $PackageInfo = $FastPackageReference | ConvertFrom-Json
    $Source = $Sources | ? Name -eq $PackageInfo.Source
    $h = @{Headers = $Source.Headers}

    if (-not (Test-Path $Location)) { New-Item -Type Directory -Path $Location }
    Push-Location $Location
    New-Item -Type Directory -Path $PackageInfo.Name -ea SilentlyContinue
    Invoke-WebRequest @h -Uri $PackageInfo.FullPath -OutFile package.zip
    Expand-Archive -Path package.zip -DestinationPath .
    $UncompressedPath = "$($PackageInfo.Name)-$($PackageInfo.Version)-$($PackageInfo.Details.CommitId)"
    # Submodule handling (from the same source)
    $PackageInfo.Details.Submodules -ne $null | % {
        $RepositoryTreeUrl = $PackageInfo.FullPath -replace [regex]::Escape("/archive.zip?sha=$($PackageInfo.Version)"),"/tree?ref=$($PackageInfo.Details.CommitId)"
        $RepositoryTree = Invoke-RestMethod @h -Uri $RepositoryTreeUrl
        $SubmoduleCommitId = ($RepositoryTree | ? name -eq $_.path).id
        Invoke-WebRequest @h -Uri ($_.url + "/repository/archive.zip?ref=$SubmoduleCommitId") -OutFile submodule.zip
        Expand-Archive -Path submodule.zip -DestinationPath .
        $UncompressedSubmodulePath = (Resolve-Path "*$SubmoduleCommitId").Path
        Move-Item -Path ($UncompressedSubmodulePath + '\*') -Destination (Join-Path $UncompressedPath $_.path)
        rm submodule.zip
        rm $UncompressedSubmodulePath
    }
    Rename-Item -Path $UncompressedPath -NewName $PackageInfo.Version -PassThru |
    Move-Item -Destination $PackageInfo.Name
    rm package.zip
    Pop-Location
    
    $Swid = $PackageInfo | ConvertTo-Hashtable
    $Swid.FastPackageReference = $FastPackageReference
    New-SoftwareIdentity @Swid
}

function Install-Package {
    param(
        [Parameter(Mandatory)]
        [string] $FastPackageReference
    )
    $Location = if ($request.Options.Location) {
        $request.Options.Location
    } elseif ($request.Options.User) {
        if ($Platform -eq 'Win32NT') {
            "$env:USERPROFILE\Documents\WindowsPowerShell"
        } else {
            "$env:HOME/.local/share/powershell"
        }
    } else {
        if ($Platform -eq 'Win32NT') {
            'C:\Program Files\WindowsPowerShell\Modules'
        } else {
            '/usr/local/share/powershell/Modules'
        }
    }
    if (-not (Test-Path $Location)) { New-Item -Type Directory -Path $Location }
    Download-Package @PSBoundParameters -Location $Location
    $Swid = $FastPackageReference | ConvertFrom-Json
    $Param = @{
        MemberType = 'NoteProperty'
        Name = 'Location'
        Value = $Location
        TypeName = 'string'
    }
    [array]$InstalledPackages = if (Test-Path $InstalledPackagesPath) {
        Get-Content $InstalledPackagesPath | ConvertFrom-Json
    } else { @() }
    $InstalledPackages += $Swid | Add-Member @Param -PassThru
    Dump-InstalledPackages $InstalledPackages
}

function Uninstall-Package {
    param(
        [Parameter(Mandatory)]
        [string] $FastPackageReference
    )
    $Swid = $FastPackageReference | ConvertFrom-Json
    #[array]$InstalledPackages = Get-Content $InstalledPackagesPath | ConvertFrom-Json
    $Package = $script:InstalledPackages | ? { $_.Name -eq $Swid.Name -and $_.Version -eq $Swid.Version }
    $Location = Join-Path $Package.Location $Swid.Name
    Remove-Item "$Location\$($Swid.Version)" -Recurse -Force
    if (-not (Test-Path "$Location\*")) {
        Remove-Item $Location
    }
    $InstalledPackages = $InstalledPackages -ne $Package
    Dump-InstalledPackages $InstalledPackages
}

function Get-InstalledPackage {
    param(
        [string] $Name,
        [string] $RequiredVersion,
        [string] $MinimumVersion,
        [string] $MaximumVersion = "$([int]::MaxValue).0"
    )
    if (-not $MinimumVersion) {
        $MinimumVersion = '0.0'
    }
    if (-not $MaximumVersion) {
        $MaximumVersion = "$([int]::MaxValue).0"
    }


    [array]$script:InstalledPackages = if (Test-Path $InstalledPackagesPath) {
        Get-Content $InstalledPackagesPath | ConvertFrom-Json
    } else { @() }
    $InstalledPackages | ? Name -match $Name | Sort-Object Version -Descending | ? {
        [System.Version]($_.Version) -ge $MinimumVersion -and
        [System.Version]($_.Version) -le $MaximumVersion -and
        (-not $RequiredVersion -or $_.Version -eq $RequiredVersion)
    } | ? Location -match ([regex]::Escape($request.Options.Location)) |
    Select * -ExcludeProperty Location | % {
        $Swid = ConvertTo-Hashtable $_
        $Swid.FastPackageReference = $_ | ConvertTo-Json -Depth 3
        New-SoftwareIdentity @Swid
    }
}

function Get-PackageDependencies {
    param(
        [Parameter(Mandatory)]
        [string] $FastPackageReference
    )
    $Swid = $FastPackageReference | ConvertFrom-Json
    $Swid.Dependencies | % {
        Find-Package -Name $_.PackageName -RequiredVersion $_.Version
        #ProviderName,Source
    }
}