Types/OpenPackage.Source/Directory.ps1

<#
.SYNOPSIS
    Gets a Directory as a package
.DESCRIPTION
    Gets a Directory as an Open Package
#>

param(
# The list of directories
[string[]]
$Directory,

# 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.
# 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,

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

# Check for a directory
if (-not $Directory) {
    # and error out if none is present.
    Write-Error "No -Directory"
    return
}

# Get all of the resolved items.
$resolvedItems = Get-Item -Path $Directory

# Adjust our base path as needed
# (ensure it begins and ends with a slash)
$BasePath = $BasePath -replace '^/?', '/' -replace '/?$', '/'

# Go over each potential directory
foreach ($resolvedItem in $resolvedItems) {
    # If it is not a directory, continue
    if ($resolvedItem -isnot [IO.DirectoryInfo]) {
        continue
    }
    
    Push-Location -LiteralPath $resolvedItem.FullName
    $gciSplat = [Ordered]@{LiteralPath=$resolvedItem.FullName;Recurse=$true;File=$true}
    if ($IncludeHidden) { $gciSplat.Force = $true }
    $filesToArchive = @(Get-ChildItem @gciSplat)

    # This make take a sec, so let's create a progress bar
    $Progress = [Ordered]@{
        Status = " "
        Activity = "Creating Package $($resolvedItem.Name)"
        Id = Get-Random
    }
    $total = $filesToArchive.Length
    $counter = 0

    # We will use file types to provide package metadata
    
    if (-not $package) {
        $memoryStream = [IO.MemoryStream]::new()
        $package = [IO.Packaging.Package]::Open($memoryStream, 'OpenOrCreate', 'ReadWrite')
        if ($resolvedItem.Parent.Name -and 
            (
                $resolvedItem.Name -as [version] -or 
                $resolvedItem.Name -as [semver]
            ) 
        ) {
            $package.PackageProperties.Identifier = $resolvedItem.Parent.Name
            $package.PackageProperties.Version = $resolvedItem.Name
        } else {
            $package.PackageProperties.Identifier = $resolvedItem.Name
        }
        
        Add-Member -InputObject $package NoteProperty MemoryStream $memoryStream -Force
    }

    # Try to get the git app
    $gitApp = $ExecutionContext.SessionState.InvokeCommand.GetCommand('git','application')

    # Then see if get can git it.
    $canGitIt = $gitApp -and # If git is loaded
        (Test-Path ( # and a .git directory exists
            Join-Path $resolvedItem.FullName '.git'
        ))
        
    # If we can git it.
    if ($canGitIt) {
        # get it's first remote
        $gitRemote = @(& $gitApp '-C' $resolvedItem.FullName remote)[0] |
            ForEach-Object {
                & $gitApp '-C' $resolvedItem.FullName remote get-url $_
            }
            
        # and create a relationship to the repository
        $relation = $package.Relate($gitRemote,'git','repository')
        if ($VerbosePreference -notin 'silentlyContinue', 'ignore') {
            Write-Verbose "Related $(
                $relation.TargetUri
            ) as [$($relation.RelationshipType)]$($relation.id)"

        }        
    }    
    
    # So declare an oldest created file and newest write time.
    $oldestCreationTime = [DateTime]::Now
    $lastWriteTime = [DateTime]::MinValue    
                
    #region Filter Filters
    $filteredFiles = @(
        # If any exclusions are present,
        # security dictates we process them first.
        # (deny before approve)
        :filterFiles foreach ($file in $filesToArchive) {
            $relativePath = $file.FullName.Substring($resolvedItem.FullName.Length)
            # If we have not excplitly included `.git`,
            if (-not $includeGit -and $relativePath -match '[\\/].git[\\/]') {
                continue # exclude `.git`.
            }
            # If we have not explicitly included `node_modules`
            if (-not $IncludeNodeModule -and $relativePath -match '[\\/]node_modules[\\/]') {                
                continue # exclude `node_modules`.
            }
            # If we have not explicitly included `_site`
            if (-not $IncludeSite -and $relativePath -match '[\\/]_site[\\/]') {
                continue # exclude `_site`.
            }

            # If we have any exclusion wildcards
            if ($exclude) {                
                foreach ($exclusion in $Exclude) {
                    if ($file.FullName -like $exclusion) {
                        continue filterFiles # exclude any files that match
                    }
                }
            }

            # If we have any include wildcards
            if ($include) {
                $included = $false
                foreach ($inclusion in $include) {
                    if ($file.FullName -like $inclusion) {
                        $included = $true # only include things that match.
                        break
                    }
                }
                
                if (-not $included) { continue filterFiles }
            }

            $file
        }
    )
    #region Filter Filters
    
    # Go over each file we want to archive
    :packingFiles foreach ($file in $filteredFiles) {                
        # get each file as a relative uri, and then get it's bytes
        $relativeUri = (
            $BasePath, (
                $file.FullName.Substring($resolvedItem.FullName.Length) -replace '[\\/]', '/'
            ) -join '/'
        ) -replace '^/?', '/' -replace '//{2,}', '/'
        
        $fileBytes = Get-Content -AsByteStream -Raw -LiteralPath $file.FullName

        # If the file was blank
        if (-not $fileBytes) {
            # write a message to verbose indicating we are skipping the file.
            Write-Verbose "Skipping blank file $($file.FullName)"
            continue
        }

        # encode our URI,
        $encodedUri = [IO.Packaging.PackUriHelper]::CreatePartUri($relativeUri)

        # make it a root relative uri.
        $relativeUri = '/' + ($encodedUri -replace '^/')

        # Determine the right content type for the extension
        $fileContentType = $typeMap[$file.Extension]
        # and fall back to text/plain
        if (-not $fileContentType) { $fileContentType = 'text/plain'}
                        
        # Then update our creation times / last write times as needed.
        if ($file.CreationTime -lt $oldestCreationTime) {
            $oldestCreationTime = $file.CreationTime
        }
        if ($file.LastWriteTime -gt $lastWriteTime) {
            $lastWriteTime = $file.LastWriteTime
        }

        # Write our progress message
        $progress.PercentComplete = (++$counter * 100 / $total)
        $Progress.Status = "$relativeUri"
        Write-Progress @Progress

        if (-not $fileBytes) { continue }

        # Try to create a new part
        try {
            $newPart =  $package.CreatePart($relativeUri, $fileContentType, $CompressionOption)
        } catch {
            # If that didn't work,
            $ex = $_
            # at least one exception has some well known answers
            if ($ex.Exception.HResult -eq 0x80131501) {                                
                # Open Packaging Conventions do not allow case-sensitive collisions
                # (most likely because they would not extract well on all operating systems)
                # If we find an exception indicating a conflict, and multiple copies
                $multipleCopies = @($filesToArchive | 
                        Where-Object Fullname -ieq $file.FullName |
                        Select-Object -ExpandProperty Fullname)
                if ($multipleCopies.Count) {
                    # warn the user
                    Write-Warning "Skipping '$($File.Fullname)' - Case Sensitivity conflict between:$(
                        @(
                            [Environment]::NewLine
                            # and provide a helpful pair of paths so they can resolve the conflict
                            $multipleCopies
                        ) -join
                            [Environment]::NewLine
                    ) $ex"

                } else {
                    # If there were not multiple files, error (but do not return)
                    Write-Error -ErrorRecord $ex
                }                        
            } else {
                # and if the error was unknown, error (but do not return)
                Write-Error -ErrorRecord $ex
            }                        
        }
        
        # If we could not create the part, continue
        if (-not $newPart) {  continue }
        
        $newStream = $newPart.GetStream()
        $newStream.Write($fileBytes, 0, $fileBytes.Length)
        $newStream.Close()        
        $null = $newStream.DisposeAsync()        
    }    

    Pop-Location

    $Progress.Remove('PercentComplete')
    $Progress.Completed = $true
    Write-Progress @Progress
    $package.PackageProperties.Created = $oldestCreationTime
    $package.PackageProperties.Modified = $lastWriteTime
    $package
}