Types/OpenPackage.Source/Url.ps1

<#
.SYNOPSIS
    Packages urls.
.DESCRIPTION
    Gets one or more urls and stores them in Open Packages
#>

param(
# An array of urls
[Alias('Uri')]
[uri[]]
$url,

# A list of file wildcards to include.
[Parameter(ValueFromPipelineByPropertyName)]
[SupportsWildcards()]
[string[]]
$Include,

# A list of file wildcards to exclude.
[Parameter(ValueFromPipelineByPropertyName)]
[SupportsWildcards()]
[string[]]
$Exclude,

# The base path within the package.
# Content should be added beneath this base path.
[string]
$BasePath = '/',

# A content type map.
# This maps extensions and URIs to a content type.
[Collections.IDictionary]
$TypeMap = $(
    ([PSCustomObject]@{PSTypeName='OpenPackage.ContentTypeMap'}).TypeMap
),

# The compression option.
[IO.Packaging.CompressionOption]
[Alias('CompressionLevel')]
$CompressionOption = 'Superfast',
        
# If set, will force the redownload of various resources and remove existing files or directories
[switch]
$Force,

# If set, will include hidden files and folders, except for files beneath `.git`
[Alias('IncludeDotFiles')]
[switch]
$IncludeHidden,

# If set, will include the `.git` directory contents if found.
[Alias('IncludeGitFile','IncludeGitFiles','IncludeGitDirectory')]
[switch]
$IncludeGit,

# If set, will include any content found in `/node_modules`.
[Alias('IncludeNodeModules')]
[switch]
$IncludeNodeModule,

# The personal data server. This is used in At Protocol requests.
[string]
$PDS,

# Any additional headers to pass into a web request.
[Alias('Header')]
[Collections.IDictionary]
$Headers = [Ordered]@{},

# The current package
[IO.Packaging.Package]
$Package
)

$namedParameters = [Ordered]@{}
foreach ($key in $MyInvocation.MyCommand.Parameters.Keys) {
    $var = $ExecutionContext.SessionState.PSVariable.Get($key)
    if (-not [String]::IsNullOrEmpty($var.Value)) {
        $namedParameters[$key] = $var.value
    }
}
$namedParameters.Remove('url')
$namedParameters.Remove('inputObject')

foreach ($uri in $url) {
    # If the uri is an at:// uri
    if ($uri.Scheme -eq 'at') {    
        # call ourself
        Get-OpenPackage -AtUri $uri @namedParameters
        continue
    }

    # If the URI is a git uri, or is a well-known git domain,
    if (
        $uri.Scheme -eq 'git' -or                
        $uri.DnsSafeHost -match '^git(?>hub|lab)\.com$' -or
        $uri.DnsSafeHost -match 'tangled\.(org|sh)$' -or
        $uri.DnsSafeHost -match 'codeberg\.org$' -and
        -not ($uri.DnsSafeHost -match '^(raw|api)') -and
        -not ($uri.Segments -match '^releases/?$')
    ) {
        # pack a repository
        Get-OpenPackage -Repository $uri @namedParameters
        continue
    }


    # If the URI is a nuget.org or powershellgallery.com link.
    if ($uri.DnsSafeHost -in 'nuget.org','www.nuget.org' -or
        $uri.DnsSafeHost -in 'powershellgallery.com','www.powershellgallery.com' -or
        $uri.DnsSafeHost -in 'community.chocolatey.org') {
        # If so, call ourselves with the Nuget uri
        Get-OpenPackage -NuGet $uri @namedParameters
        continue
    }

    # If the URI is a PyPi.org project link
    if ($uri.DnsSafeHost -in 'pypi.org', 'www.pypi.org' -and 
        $uri.Segments[1] -eq '/project') {
        # call ourselves with -PythonPackage
        Get-OpenPackage -PythonPackage $uri @namedParameters
        continue
    }

    # If the URI is a Node Package link
    if ($uri.DnsSafeHost -in 
        'npmjs.com', 'www.npmjs.com', 'npmx.dev', 'www.npmx.dev' -and
        $uri.Segments[1] -eq '/package'
    ) {
        # call ourselves with -NodePackage
        Get-OpenPackage -NodePackage $uri @namedParameters
        continue
    }


    # If the uri is relative
    if (
        (-not $uri.IsAbsoluteUri)
    ) {
        $slashKey = "$uri" -replace "^\.?/?", '/'
        # try to find it in the package if we have one
        if ($Package -and $Package -is [IO.Packaging.Package] -and
            $Package.PartExists($slashKey)) {            
            $Package.GetContent($slashKey)
        }

        continue
    }

    # At this point lets just poke at the uri and make a package.
    try {
        if ($headers) {
            $WebResponse = Invoke-WebRequest -Uri $Uri -Headers $Headers
        } else {
            $WebResponse = Invoke-WebRequest -Uri $Uri
        }
        
    } catch {
        Write-Verbose "$uri - $_"
        continue
    }    

    # If the web response is a byte array
    if ($WebResponse.Content -is [byte[]] -and 
        # and starts with the magic pair of bytes indicate it might be a zip
        ($WebResponse.Content[0] -eq 80 -and $WebResponse.Content[1] -eq 75)
    ) {
        # Create a stream from the response
        $memoryStream = [IO.MemoryStream]::new($WebResponse.Content)
        # and open the package
        $currentPackage = [IO.Packaging.Package]::Open($memoryStream, 'OpenOrCreate', 'ReadWrite')
        # If that did not work, it will error,
        if (-not $currentPackage) {                    
            continue # and we should continue.
        }
        # Attach the memory stream to the package
        $currentPackage | Add-Member NoteProperty MemoryStream $memoryStream -Force
        # and emit the package.
        $currentPackage
    } else {
        # Ok, the response was not a package.

        # Let's turn it into a new package, or put it in the current package.
        if (-not $package) {
            $memoryStream = [IO.MemoryStream]::new()
            $package = [IO.Packaging.Package]::Open($memoryStream, 'OpenOrCreate','ReadWrite')
            $package.pstypenames.insert(0, 'OP')
            $package.pstypenames.insert(0, 'OpenPackage')
            Add-Member NoteProperty MemoryStream $memoryStream -Force -InputObject $package
        }    
        
        # Let's use the domain as the identifier
        if (-not $package.PackageProperties.Identifier) {
            $package.PackageProperties.Identifier = $uri.DnsSafeHost
        }
        
        # Pick out the content type from the response
        $responseType, $responseTypeOptions = $WebResponse.Headers['content-type'] -split ';'
        # And get the major and minor type
        $majorType, $minorType = $responseType -split '/', 2
        $minorType = $minorType -replace '^.+?\+' # replace anything in the minor type before a plus
        # (this is effectively the extension)
        $localPath =
            # If we ask for a root uri
            if ($uri.LocalPath -match '/$') {
                # make it an index of the replied content type
                $uri.LocalPath + 'index.' + $minorType
            } else {
                # otherwise, use the path they provided.
                $uri.LocalPath
            }
        # Now that we know where we're putting it, let's create a part
        $newPart = $package.CreatePart($localPath, $WebResponse.Headers['content-type'], $CompressionOption)
        # and get the content stream
        $newStream = $newPart.GetStream()

        # If the response was a byte array
        if ($WebResponse.Content -is [byte[]]) { 
            # write that to the stream
            $newStream.Write($WebResponse.Content, 0, $WebResponse.Content.Length)                    
        } else {
            # otherwise, turn the stringified content into bytes
            $buffer = $OutputEncoding.GetBytes("$($WebResponse.Content)")
            # and write that
            $newStream.Write($buffer, 0, $buffer.Length)
        }
        
        $newStream.Close()
        $newStream.Dispose()        
    }    
}

# If there is a current package
if ($package) {
    $package # output it at the end
    # (this avoids returning the same package multiple times)
}