Commands/Get-OpenPackage.ps1
|
function Get-OpenPackage { <# .SYNOPSIS Gets Open Packages .LINK https://en.wikipedia.org/wiki/Open_Packaging_Conventions .INPUTS Almost anything .OUTPUTS Open Packages .DESCRIPTION Gets Open Packages from almost anything. Anything can be a package. This command helps you make anything into a package. The following types of packages are currently supported: * Any [Open Packaging Convention](https://en.wikipedia.org/wiki/Open_Packaging_Conventions) files * Any directory * Any `*.zip` file * Any `*.tar.gz` file * Any url * Any public nuget package * Any git repository * Any public at protocol URI * Any dictionary (including nested dictionaries) * Any single file Anything can be a package. Once we start to treat anything as a package, we can do amazing things with packages. Like: * Inspect any packages before we work with them. * Modify the packages to customize their content. * Split packages * Filter our components. * Join them back together. * Search package content. * Work with compressed trees of data. * Have an in-memory containerized virtual filesystem. * Serve a package from memory. * Store data to N package layers. .EXAMPLE # Make the current directory into a package # (do not try this at `$home`) Get-OpenPackage . .EXAMPLE # Make the module into a package $opPackage = Get-Module OP | Get-OpenPackage .EXAMPLE $opPackage = Get-Module OP | Get-OpenPackage -Include *.ps1 .EXAMPLE # Another way to make the current directory into a package # (do not try this at `$home`) Get-Item . | Get-OpenPackage .EXAMPLE # Get a package from nuget $Avalonia = OP https://www.nuget.org/packages/Avalonia/ .EXAMPLE # Get a package from Chocolatey $chocoPackage = op https://community.chocolatey.org/packages/chocolatey .EXAMPLE # Get a package from the PowerShell gallery $turtlePackage = op https://powershellgallery.com/packages/Turtle .EXAMPLE # Get a package from a single URL $imagePackage = op https://MrPowerShell.com/MrPowerShell.png .EXAMPLE # Create a package from multiple URLs by piping back to ourself $svgAndPng = op https://MrPowerShell.com/MrPowerShell.png | op https://MrPowerShell.com/MrPowerShell.svg .EXAMPLE # Get a package from an at protocol URI $atPost = op at://mrpowershell.com/app.bsky.feed.post/3k4hf5dy6nf2g .EXAMPLE # Get the most recent 50 posts $atLast50 = op at://mrpowershell.com/app.bsky.feed.post/ -First 50 .EXAMPLE # Get all standard.site.documents for a user $standardSiteDocuments = op at://mrpowershell.com/site.standard.document/ .EXAMPLE $MrPowerShellStandardSite = op @( 'at://mrpowershell.com/site.standard.document/' 'at://mrpowershell.com/site.standard.publication/' ) .EXAMPLE $MrPowerShellStrings = op 'at://mrpowershell.com/sh.tangled.string/' .EXAMPLE # Get [feather icons](https://feathericons.com/) as an open package. $featherIcons = Get-OpenPackage https://github.com/feathericons/feather/tree/main/icons .EXAMPLE # Get the [itermColorSchemes](https://iterm2colorschemes.com/) for windows terminal $iTermPalettes = op 'https://github.com/mbadolato/iTerm2-Color-Schemes/blob/master/windowsterminal/' #> [CmdletBinding(PositionalBinding=$false,DefaultParameterSetName='Any',SupportsPaging)] [Alias( 'Get-OP', 'OP', 'OpenPackage','gOpenPackage', 'Open-OpenPackage', 'Open-OP', 'opop', 'opOpenPackage' )] param( # Any unnamed arguments to the command. # Each argument will be treated as a potential -FilePath or -Uri. # Once the first related verb is detected, these will become arguments to that verb # (For example, `op . start` will get an open package and then start a server for that package) [Parameter(ValueFromRemainingArguments)] [PSObject[]] $ArgumentList, # The path of a file to import [Parameter(Mandatory,ParameterSetName='FilePath',ValueFromPipelineByPropertyName)] [Alias('Fullname')] [string] $FilePath, # Gets Open Packages with `@` syntax. # Without a domain, `@` will be presumed to be a github # With a domain, `@` will look for an https url, and check at protocol [Parameter(Mandatory,ParameterSetName='At',ValueFromPipelineByPropertyName)] [string[]] $At, # A URI to package. # If this URI is a git repository, will make a package out of the repository # If this URI is a nuget package url or powershell gallery url, will download the package. [Parameter(Mandatory,ParameterSetName='Uri',ValueFromPipelineByPropertyName)] [Alias('Url')] [uri[]] $Uri, # Any additional headers to pass into a web request. [Alias('Header')] [Collections.IDictionary] $Headers = [Ordered]@{}, # A Repository to package. # This can be the root of a repo or a link to a portion of the tree. # If a portion of the tree is provided, will perform a sparse clone of the repository [Parameter(Mandatory,ParameterSetName='Repository',ValueFromPipelineByPropertyName)] [Alias('clone_url')] [string] $Repository, # The github branch name. [Parameter(ValueFromPipelineByPropertyName)] [string] $Branch, # One or more optional sparse filters to a repository. # If these are provided, only files matching these filters will be downloaded. [Parameter(ValueFromPipelineByPropertyName)] [string[]] $SparseFilter, # An At Uri to package. # This can be a single post or a collection of all posts of a type. [Parameter(Mandatory,ParameterSetName='AtUri',ValueFromPipelineByPropertyName)] [string[]] $AtUri, # The personal data server. This is used in At Protocol requests. [Parameter(ValueFromPipelineByPropertyName)] [string] $PDS, # Adds a dictionary of content to the package [Parameter(Mandatory,ParameterSetName='Dictionary',ValueFromPipelineByPropertyName)] [Collections.IDictionary] $Dictionary, # The base path within the package. # Content should be added beneath this base path. [string] $BasePath, # A Nuget Uri to package. # The package at this location will be downloaded and opened directly. [Parameter(Mandatory,ParameterSetName='NugetPackage',ValueFromPipelineByPropertyName)] [Alias('NugetPackage','PowerShellGallery','ChocolateyGallery')] [uri] $NuGet, # One or more Node Packages. [Parameter(Mandatory,ParameterSetName='NodePackage',ValueFromPipelineByPropertyName)] [Alias('npm','npmx')] [string[]] $NodePackage, # One or more Python Packages. [Parameter(Mandatory,ParameterSetName='PythonPackage',ValueFromPipelineByPropertyName)] [Alias('pip','whl')] [string[]] $PythonPackage, # A module to package # A loaded module name or moduleinfo object to package. # The loaded module must have a path property. # The files in this path will be packaged. [Parameter(Mandatory,ParameterSetName='Module',ValueFromPipelineByPropertyName)] [PSObject] $Module, # A list of file wildcards to include. [Parameter(ValueFromPipelineByPropertyName)] [SupportsWildcards()] [string[]] $Include, # A list of file wildcards to exclude. [Parameter(ValueFromPipelineByPropertyName)] [SupportsWildcards()] [string[]] $Exclude, # 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', # One or more input objects. [Parameter(ValueFromPipeline)] [Alias('Package')] [PSObject] $InputObject, # Gets the packages that are currently installed [Parameter(Mandatory,ParameterSetName='Installed')] [switch] $Installed, # Gets packages that are currently running in a server [Parameter(Mandatory,ParameterSetName='Running')] [switch] $Running, # 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. # By default, this content will be excluded. [Alias('IncludeGitFile','IncludeGitFiles','IncludeGitDirectory')] [switch] $IncludeGit, # If set, will include any content found in `/node_modules`. # By default, this content will be excluded. [Alias('IncludeNodeModules')] [switch] $IncludeNodeModule, # If set, will include any content found in `/_site`. # By default, this content will be excluded. [Alias('IncludeWebsite')] [switch] $IncludeSite ) begin { # First, set output encoding to UTF8 # This should ensure things work consistently regardless of operating system and user preference. $OutputEncoding = [Text.Encoding]::UTF8 # And we want to keep track of our command name, so we can semi-anonymously recurse. $myCommandName = $MyInvocation.MyCommand.Name # Next up we initialize a type map. # This maps extensions and uris to a content type, and is used when creating parts. $typeMap = ([PSCustomObject]@{PSTypeName='OpenPackage.ContentTypeMap'}).TypeMap # Gets the open package type data, as we will need this $OpTypeData = Get-TypeData -TypeName OpenPackage.Source function InvokeOpMethod { param([string]$Name, [Collections.IDictionary]$Parameter) if (-not $OpTypeData.Members[$Name].Script) { return } $bindableParameters = [Ordered]@{} $function:func = $OpTypeData.Members[$Name].Script $func = $ExecutionContext.SessionState.InvokeCommand.GetCommand('func', 'Function') :nextParameter foreach ($parameterName in $func.Parameters.Keys) { if ($null -ne $parameter[$parameterName]) { $bindableParameters[$parameterName] = $Parameter[$parameterName] continue nextParameter } foreach ($aliasName in $func.Parameters[$parameterName].Aliases) { if ($null -ne $Parameter[$aliasName]) { $bindableParameters[$aliasName] = $Parameter[$aliasName] continue nextParameter } } } $ExecutionContext.SessionState.PSVariable.Remove('function:func') try { & $OpTypeData.Members[$Name].Script @bindableParameters } catch { $PSCmdlet.WriteError($_) } } # The full default type map is much more robust. # If this was being used without a module context, we still want to work for _most_ scenarios # So this a secondary default, declared inline, that captures a fairly short list of common content types. if (-not $typeMap -or -not $typeMap.Count) { $typeMap = [Ordered]@{ ".css" = "text/css"; ".html" = "text/html"; ".svg" = "image/svg+xml"; '.js' = 'text/javascript';".jsm" = "text/javascript"; ".png" = "image/png"; ".gif" = "image/gif"; ".jpg" = "image/jpeg"; ".jpeg" = "image/jpeg"; ".md" = "text/markdown" ".mp3" = "audio/mpeg"; ".mp4" = "video/mp4"; ".xml" = "application/xml" } } # Now declare several internal filters to turn various things into packages. # These are in alphabetical order, not in order of likely use. filter getCurrentPack { # Gets the current package # If the input object was a package, get it if ($inputObject -is [IO.Packaging.Package]) { $currentPackage = $InputObject } else { # Otherwise, $memoryStream = [IO.MemoryStream]::new() $currentPackage = [IO.Packaging.Package]::Open($memoryStream, 'OpenOrCreate','ReadWrite') Add-Member NoteProperty MemoryStream $memoryStream -Force -InputObject $currentPackage } $currentPackage } filter packFile { # If we are creating a package from a file $resolvedItem = $_ $peekMagicBytes = Get-Content -AsByteStream -LiteralPath $resolvedItem.FullName -First 5 if ($peekMagicBytes[0,1] -as 'char[]' -join '' -eq 'PK') { return $resolvedItem.FullName | packZip } elseif ( $peekMagicBytes[0,1] -as 'char[]' -join '' -eq './' ) { return $resolvedItem | packTar } elseif ($peekMagicBytes[0] -eq 31 -and $peekMagicBytes[1] -eq 139 -and $peekMagicBytes[2] -eq 8 ) { return $resolvedItem | packTar } # Read the file content $fileBytes = Get-Content -AsByteStream -Raw -LiteralPath $resolvedItem.FullName # Get our current package $currentPackage = getCurrentPack # Make the file name an encoded URI $relativeUri = [Web.HttpUtility]::UrlEncode($resolvedItem.Name) $relativeUri = '/' + ($relativeUri -replace '^/') # $relativeUri = $relativeUri -replace '\s', '%20' # If the package has no identifier, if (-not $currentPackage.PackageProperties.Identifier) { # set it to the file name $currentPackage.PackageProperties.Identifier = $resolvedItem.Name } $currentPackage.PackageProperties.Identifier = $resolvedItem.Name # And determine the right extension content type $fileContentType = $typeMap[$resolvedItem.Extension] # and create a part $newPart = $currentPackage.CreatePart($relativeUri, $fileContentType, $CompressionOption) if (-not $newPart) { continue } # then write the file to the part $newStream = $newPart.GetStream() $newStream.Write($fileBytes, 0, $fileBytes.Length) $newStream.Close() # and set the package properties based off of the resolved info. $currentPackage.PackageProperties.Created = $resolvedItem.CreationTime $currentPackage.PackageProperties.Modified = $resolvedItem.LastWriteTime $currentPackage } filter packTar { $namedParameters['TarFile'] = $_ InvokeOpMethod 'Tar' $NamedParameters } filter packZip { $namedParameters['ZipFile'] = $_ InvokeOpMethod 'Zip' $NamedParameters } $generateEvent = [Runspace]::DefaultRunspace.Events.GenerateEvent } process { $namedParameters = [Ordered]@{} + $PSBoundParameters if ($PSCmdlet.PagingParameters.First -lt 1mb) { $namedParameters['First'] = $psCmdlet.PagingParameters.First } if ($PSCmdlet.PagingParameters.Skip -lt 1mb) { $namedParameters['Skip'] = $psCmdlet.PagingParameters.Skip } $messageData = [Ordered]@{} + $namedParameters # Generate an event $getOpenPackageEvent = $generateEvent.Invoke( 'Get-OpenPackage', # for Get-OpenPackage $MyInvocation.MyCommand, # sent by this command @( # containing MyInvocation and MessageData $MyInvocation, $messageData ), # And sending the message data dictionary along $messageData, # process in the current thread $true, # and wait for completion. $true ) # If the event was processed, and they said any form of "no" if ($getOpenPackageEvent.MessageData.Rejected -or $getOpenPackageEvent.MessageData.Reject -or $getOpenPackageEvent.MessageData.No -or $getOpenPackageEvent.MessageData.Deny ) { Write-Warning "Will not $($MyInvocation.Line)" return } if ($InputObject) { $namedParameters.Remove('InputObject') switch ($InputObject) { {$_ -is [Management.Automation.PSModuleInfo] -and $_.Path} { & $myCommandName -Module $InputObject @namedParameters return } {$_ -is [IO.FileInfo] -or $_ -is [IO.DirectoryInfo]} { & $myCommandName -FilePath $InputObject.FullName @namedParameters return } {$_ -is [Collections.IDictionary]} { & $myCommandName -Dictionary $InputObject @namedParameters } {$_ -is [uri]} { & $myCommandName -Uri $InputObject @namedParameters } } $namedParameters.InputObject = $InputObject } # If arguments were provided, we are trying to be as natural with syntax as we can. # Each string can map to a parameter, or to a verb of a related command to run. if ($ArgumentList) { $packages = @() $loadedModules = @(Get-Module) $outputPackages = $true $namedParameters.Remove('ArgumentList') # Walk over each argument :nextArgument for ($argNumber = 0; $argNumber -lt $ArgumentList.Length; $argNumber++) { # If we have already output a package if ($packages -and # but it was not explicitly mapped -not $namedParameters.'package' -and $packages[0] -is [IO.Packaging.Package] ) { # map the package to the named parameters. $namedParameters.'package' = $packages[0] } $arg = $ArgumentList[$argNumber] if ($arg -is [IO.Packaging.Package]) { if ($packages -notcontains $arg) { $packages += $arg } continue nextArgument } # If it is a string, path info, or uri if ($arg -is [Management.Automation.PSModuleInfo]) { $modulePackage = Get-OpenPackage -Module $arg @namedParameters if ($packages -notcontains $modulePackage) { $packages += $modulePackage } # and continue to the next argument. continue nextArgument } if ($arg -is [Collections.IDictionary]) { $packages += & $myCommandName -Dictionary $arg @namedParameters continue nextArgument } if ($arg -is [string] -or $arg -is [Management.Automation.PathInfo] -or $arg -is [uri] ) { # see if the path exists $slashKey = $arg -replace '^\.?/?', '/' -replace '/$' # If the input was a package, and it exists in the package if ( $InputObject -is [IO.Packaging.Package] -and $InputObject.PartExists($slashKey) ) { # read the contents of the part and emit them to output Get-OpenPackage -Uri $slashKey @namedParameters -InputObject $InputObject # and continue to the next argument. continue nextArgument } # If the argument is a path if (Test-Path $arg -ErrorAction Ignore) { # turn it into a package. $filePackage = Get-OpenPackage -FilePath $arg @namedParameters if ($packages -notcontains $filePackage) { $packages += $filePackage } continue nextArgument } # If the argument started with `@` if ($arg -match '^@') { # treat it as open-ended at syntax $atPackages = Get-OpenPackage -At $arg @namedParameters foreach ($atPackage in $atPackages) { if ($atPackage -isnot [IO.Packaging.Package]) { continue } if ($packages -notcontains $atPackage) { $packages += $atPackage } } continue nextArgument } # If the argument started with `at://` if ($arg -match '^at://') { # treat it as an at uri and turn it into a package. $atPackage = Get-OpenPackage -AtUri $arg @namedParameters if ($packages -notcontains $atPackage) { $packages += $atPackage } continue nextArgument } # If the argument could be a URI $argUri = $arg -as [uri] # and that URI is absolute if ($argUri.IsAbsoluteUri) { # get that uri as a package $uriPackage = Get-OpenPackage -Uri $argUri @namedParameters if ($packages -notcontains $uriPackage) { $packages += $uriPackage } continue nextArgument } # The the argument is a string and the name of a loaded module if ($arg -is [string] -and $loadedModules.Name -contains $arg) { $packages += Get-OpenPackage -Module $arg @namedParameters continue nextArgument } continue nextArgument } } # If we did not run any additional commands, if ($outputPackages) { $packages # we want to output our packages now } return # and return. } if ($inputObject -is [IO.Packaging.Package]) { $namedParameters['Package'] = $InputObject } if ($Installed) { if (-not $env:OpenPackagePath) { Write-Error '$env:OpenPackagePath not defined' return } Get-ChildItem -Path ($env:OpenPackagePath -split $( if ($isLinux -or $IsMacOs) { ':' } else { ';' } )) -ErrorAction Ignore return } if ($running) { Get-Job | Where-Object { $job = $_ if ($job.JobStateInfo.State -ne 'Running') { return $false } foreach ($pack in $job.Package) { if ($pack -is [IO.Packaging.Package]) { return $true } } } return } # If we are passed a uri if ($Uri) { # pack it up $namedParameters['Url'] = $uri $namedParameters.Remove('Uri') InvokeOpMethod 'Url' $NamedParameters return } #region Open Package in At Syntax if ($At) { $NamedParameters['At'] = $At InvokeOpMethod 'At' $NamedParameters return } #endregion Open Package in At Syntax #region Open Package from Repository if ($Repository) { $NamedParameters['Repository'] = $Repository InvokeOpMethod 'Repository' $NamedParameters return } #endregion Open Package from Repository #region Open Package from At Uri if ($AtUri) { $NamedParameters['AtUri'] = $AtUri InvokeOpMethod 'AtProtocol' $NamedParameters return } #endregion Open Package From At Uri if ($Module) { if ($module -isnot [Management.Automation.PSModuleInfo]) { $loadedModules = @(Get-Module) foreach ($moduleInfo in $loadedModules) { if ($moduleInfo.Name -eq $module) { $module = $moduleInfo break } } } if ($module -is [Management.Automation.PSModuleInfo] -and $module.Path) { $namedParameters.Remove('Module') $namedParameters.FilePath = $module.Path | Split-Path & $myCommandName @namedParameters return } } if ($Dictionary) { $namedParameters['DictionaryList'] = $_ InvokeOpMethod 'Dictionary' $NamedParameters return } #region Open Package from Nuget if ($Nuget) { # Try to call our GetNuget method, and pass the `$Nuget` InvokeOpMethod 'Nuget' $PSBoundParameters return } #endregion Open Package from Nuget #region Open Package from Node if ($NodePackage) { # Try to call our node method InvokeOpMethod 'Node' $PSBoundParameters return } #endregion Open Package from Node #region Open Package from Python if ($PythonPackage) { # Try to call our Python method InvokeOpMethod 'Python' $PSBoundParameters return } #endregion Open Package from Python if ($filePath) { $namedParameters.Remove('FilePath') # Try to resolve the file path $resolvedPath = $ExecutionContext.SessionState.Path.GetUnresolvedProviderPathFromPSPath($FilePath) # If we could not resolve the path, exit if (-not $resolvedPath ) { return } # Get each file beneath the path foreach ($resolved in $resolvedPath) { # (watch our for escaped characters and hidden files) $resolvedItem = Get-Item -LiteralPath ($resolved -replace '`') -Force:$IncludeHidden # If we could not resolve the item, continue if (-not $resolvedItem) { continue } # If the item is a file if ($resolvedItem -is [IO.FileInfo]) { # make packages from the file $resolvedItem | packFile } # If the item is a directory elseif ($resolvedItem -is [IO.DirectoryInfo]) { # We want a package from a directory # Push into that location, for it will make operations easier Push-Location -LiteralPath $resolvedItem.FullName # Get all files beneath this point $namedParameters['Directory'] = $resolvedItem.FullName InvokeOpMethod 'Directory' $NamedParameters Pop-Location # make packages from the directory } } return } getCurrentPack } } |