Private/func_New-Zip.ps1

# Based on Gist by "Araxeus", https://gist.github.com/Araxeus/a09797e84b0ef4b99f1efcc642d1da78
# TODO: Refactor, simplify
Function New-Zip {

    # Specifying no parameter will result in current working directory ($pwd) being archived into $pwd\$pwd.zip
    param (
        # The following paths can be relative or absolute:
        # path to folder/s containing the files to be archived
        [Alias("i", "input", "from")]
        [ValidateScript({ Test-Path -LiteralPath $_ })]
        [string[]]
        $FolderPaths = @($PWD),
        # path to zipfile / zipFolder if $ZipNameFromJson is specified (will be created if it doesn't exist)
        [Alias("t", "to", "output")][string]
        $ZipPath = "", # defaults to $PWD.zip
        # set $ZipPath to end with .zip or set this to an empty string to disable this feature
        [Alias("j", "json")]
        [ValidatePattern('.json$')]
        [string]
        $ZipNameFromJson = "", # set to a json file containing name and version, output will be $name_v$version.zip
        # ie "*.*" to only include files that have a .extension
        [Alias("f")][string]
        $Filter = "",
        # filterScript has more options than eclude
        [Alias("e")][string[]]
        $Exclude = @(),
        # { ($_.FullName -notlike "*\node_modules\*") -and ($_.Name -notlike "*.scss")}, # ignore .scss and nodeModules folder
        [Alias("fs", "script")][ScriptBlock]
        $FilterScript = { $_ },
        # overwrite zip (if there is a zip with the same name, delete it creating a new one)
        [Alias("o", "overwrite")][switch]
        $OverwriteZip,
        # keep sync between folder and zip (doesn"t do anything if OverwriteZip=true) - delete surplus files from zip
        [Alias("s")][switch]
        $Sync,
        # verbose output
        [Alias("v")][switch]
        $Verbose
    )

    $VerbosePreference = $Verbose ? "Continue" : "SilentlyContinue"

    if ($ZipNameFromJson -and !$ZipPath.EndsWith('.zip')) {
        $jsonFile = Get-Content $ZipNameFromJson
        $jsonObj = $jsonFile | ConvertFrom-Json
        $ZipName = "$($jsonObj.name.Trim().Replace(' ', '-'))_v$($jsonObj.version.Trim()).zip"
        if ($ZipPath) {
            try {
                [System.IO.Directory]::CreateDirectory($ZipPath) | Out-Null
                $ZipPath = [IO.Path]::Combine($ZipPath, $ZipName)
            }
            catch {
                Write-Error("`n Error creating ZipPath:`n $($_.Exception.Message)")
                $ZipPath = $ZipName
            }
        }
        else {
            $ZipPath = $ZipName
        }
    }
    elseif (!$ZipPath) {
        $ZipPath = [IO.Path]::Combine($PWD, "$(Split-Path -Path $PWD -Leaf).zip")
    }

    if ($OverwriteZip) {
        Remove-item -literalpath $ZipPath -force -ErrorAction SilentlyContinue
    }

    $AllFiles = New-Object System.Collections.Generic.List[System.Object]

    $ChangesCount = 0;

    try {
        $ZipArchive = [IO.Compression.ZipFile]::Open( $ZipPath, 2 )
        foreach ($FolderPath in $FolderPaths) {
            $FileList = (Get-ChildItem -LiteralPath $FolderPath -Filter $Filter -Exclude $Exclude -File -Recurse | Where-Object $FilterScript) #use the -File argument because empty folders can"t be stored
            foreach ($File in $FileList) {
                if ($File.FullName.endsWith($ZipPath)) { continue }
                # get relative path and trim leading .\ from it
                $File | Add-Member RelativePath ([System.IO.Path]::GetRelativePath($FolderPath, $File.FullName) -replace "^.\\")
                $AllFiles.Add($File)
                try {
                    # zip will store multiple copies of the exact same file - prevent this by checking if already archived.
                    if (!$OverwriteZip) {
                        $AlreadyArchivedFile = $ZipArchive.GetEntry($File.RelativePath)
                        # $AlreadyArchivedFile = ($ZipArchive.Entries | Where-Object { $_.FullName -eq $File.RelativePath })
                        if ($AlreadyArchivedFile) {
                            if (($AlreadyArchivedFile.Length -eq $File.Length) -and
                                #ZipFileExtensions timestamps are only precise within 2 seconds.
                            ([math]::Abs(($AlreadyArchivedFile.LastWriteTime.UtcDateTime - $File.LastWriteTimeUtc).Seconds) -le 2)) {
                                continue
                            }
                            $AlreadyArchivedFile.Delete()
                        }
                    }
                    $ZipArchiveEntry = [IO.Compression.ZipFileExtensions]::CreateEntryFromFile($ZipArchive, $File.FullName, $File.RelativePath, 'Optimal')
                    $ChangesCount++
                    Write-Verbose "Archived \$($ZipArchiveEntry.FullName)"
                }
                catch {
                    # single file failed - usually inaccessible or in use
                    Write-Warning  "`n $($File.FullName) could not be archived.`n $($_.Exception.Message)"
                }
            }
        }
        if ($Sync -and !$OverwriteZip) {
            $UnsyncedFiles = $ZipArchive.Entries | Where-Object -Property FullName -NotIn ($AllFiles | ForEach-Object { $_.RelativePath })
            foreach ($File in $UnsyncedFiles) {
                try {
                    $File.Delete()
                    $ChangesCount++
                    Write-Verbose "Deleted $($ZipPath)\$($File.FullName)"
                }
                catch {
                    Write-Warning "$($ZipPath)\$($File.FullName) is not in sync but couldn't be deleted"
                }
            }
        }
    }
    catch {
        # failure to open the zip file
        Write-Error $_.Exception
    }
    finally {
        # always close the zip file so it can be read later
        $ZipArchive.Dispose()
        Write-Host "$(Resolve-Path $ZipPath) was succesfully updated ($($ChangesCount) files changed)" -ForegroundColor Green
    }
}