diff --git a/README.md b/README.md index d3acf8b..8d6427d 100644 --- a/README.md +++ b/README.md @@ -1,5 +1,122 @@ -# WinEventLogCustomization +# ![logo][] WinEventLogCustomization -# Description -Description for the WinEventLogCustomization project. +| Plattform | Information | +| --------- | ----------- | +| PowerShell gallery | [![PowerShell Gallery](https://img.shields.io/powershellgallery/v/WinEventLogCustomization?label=psgallery)](https://www.powershellgallery.com/packages/WinEventLogCustomization) [![PowerShell Gallery](https://img.shields.io/powershellgallery/p/WinEventLogCustomization)](https://www.powershellgallery.com/packages/WinEventLogCustomization) [![PowerShell Gallery](https://img.shields.io/powershellgallery/dt/WinEventLogCustomization?style=plastic)](https://www.powershellgallery.com/packages/WinEventLogCustomization) | +| GitHub | [![GitHub release](https://img.shields.io/github/release/AndiBellstedt/WinEventLogCustomization.svg)](https://github.com/AndiBellstedt/WinEventLogCustomization/releases/latest) ![GitHub](https://img.shields.io/github/license/AndiBellstedt/WinEventLogCustomization?style=plastic)
![GitHub issues](https://img.shields.io/github/issues-raw/AndiBellstedt/WinEventLogCustomization?style=plastic)
![GitHub last commit (branch)](https://img.shields.io/github/last-commit/AndiBellstedt/WinEventLogCustomization/main?label=last%20commit%3A%20master&style=plastic)
![GitHub last commit (branch)](https://img.shields.io/github/last-commit/AndiBellstedt/WinEventLogCustomization/Development?label=last%20commit%3A%20development&style=plastic) | +

+ +## Description + +A PowerShell module helping you build custom eventlog channels and registering them into Windows Event Viewer. +The build logs appear under "Application and Services", even like the "Windows PowerShell" or the "PowerShellCore/Operational" EventLog.
+
+All cmdlets are build with +- powershell regular verbs +- pipeline availabilities wherever it makes sense +- comprehensive logging on verbose and debug channel by the logging system of PSFramework
+
+ +## Prerequisites + +- Windows PowerShell 5.1 +- PowerShell 6 or 7 +- Administrative Priviledges are required for registering or unregistering EventChannels
+
+ +## Installation + +Install the module from the PowerShell Gallery (systemwide): +```PowerShell +Install-Module WinEventLogCustomization +``` +
+ +## Quick start +### Creating a manifest for a EventChannel +For a quick start you can just execute: +```PowerShell +New-WELCEventChannelManifest -ChannelFullName "AndiBellstedt/MyPersonalLog" +``` +another way is the following command style, if you are not familiar with the notation on ChannelFullNames: +```PowerShell +New-WELCEventChannelManifest -RootFolderName "AndiBellstedt" -FolderSecondLevel "PowerShell" -FolderThirdLevel "Tasks" -ChannelName "Operational" +``` +This will create a manifest- and a dll file (*AndiBellstedt.man & AndiBellstedt.dll*) within you current directory.
+With the manifest file, the dll file can be registered to Windows EventLog system.
+**Attention**, the manifest file contains the paths to the dll and should not be moved in the Windows Explorer. *There is a command in the module to move the manifest with it's dll file consistently.*
+
+### Register the EventChannel +Registering a manifest and its dll file is also easy: +```PowerShell +Register-WELCEventChannelManifest -Path .\AndiBellstedt.man +``` +**Attention, executing this command will require admninistrative priviledges.**
+Due to the fact, that changes on the Windows EventLog system are a administrative task.
+
+Following this, results in a new folder "AndiBellstedt" with two subfolders ("PowerShell" & "Tasks") and a EventLog "Operational" under "Application and Services Logs" withing the Event Viewer.
+ +![EventChannel][] +
+
+### Remove the EventChannel +If the EventChannel is no longer needed, it can be removed by unregistering the manifest: +```PowerShell +UnRegister-WELCEventChannelManifest -Path .\AndiBellstedt.man +``` +
+ +### Show registered EventChannels +After registering a manifest, the defined EventChannel can be queried
+To query a EventChannel you can use: +```PowerShell +Get-WELCEventChannel -ChannelFullName "AndiBellstedt-PowerShell-Tasks/Operational" +``` +This will output something like this, showing you the details and the config of the EventChannel: +``` +PS C:\> Get-WELCEventChannel -ChannelFullName "AndiBellstedt-PowerShell-Tasks/Operational" | Format-List + +ComputerName : MyComputer +Name : AndiBellstedt-PowerShell-Tasks/Operational +Enabled : False +LogMode : Circular +LogType : Administrative +LogFullName : C:\WINDOWS\System32\Winevt\Logs\AndiBellstedt-PowerShell-Tasks%4Operational.evtx +MaxEventLogSize : 1052672 +FileSize : +RecordCount : +IsFull : +LastWriteTime : +LastAccessTime : +ProviderName : AndiBellstedt-PowerShell-Tasks +ProviderId : 43b94bbe-2d97-4f04-96b4-c254483b53f4 +MessageFilePath : C:\EventLogs\AndiBellstedt.dll +ResourceFilePath : C:\EventLogs\AndiBellstedt.dll +ParameterFilePath : C:\EventLogs\AndiBellstedt.dll +Owner : Administrators +Access : {NT AUTORITY\BATCH: AccessAllowed (ListDirectory, WriteData), NT AUTORITY\INTERACTIVE: AccessAllowed (ListDirectory, WriteData), NT AUTORITY\SERVICE: AccessAllowed (ListDirectory, WriteData), NT AUTORITY\SYSTEM: AccessAllowed (ChangePermissions, CreateDirectories, Delete, GenericExecute, ListDirectory, ReadPermissions, TakeOwnership, WriteData, WriteKey)…} +``` +### Configuration on EventChannels +There are multiple ways to configure a EventChannel.
+The first, and explicit one is:
+```PowerShell +Set-WELCEventChannel -ChannelFullName "AndiBellstedt-PowerShell-Tasks/Operational" -Enabled $true -MaxEventLogSize 1GB -LogMode Circular -LogFilePath "C:\EventLogs\AB-PS-T-Ops.evtx" +``` + +Another way is to pipe in the result of a `Get-WELCEventChannel` command: +```PowerShell +$channel = Get-WELCEventChannel "AndiBellstedt*" + +$channel | Set-WELCEventChannel -Enabled $true -MaxEventLogSize 1GB -LogMode AutoBackup -LogFilePath "C:\EventLogs" +``` +Doing it this way, `$channel` can contain more than one EventChannel to configure.
+
+ +## Practical usage - Managing, creating and configuring multiple custom EventChannel +<< more to come >> +
+ + +[logo]: assets/WinEventLogCustomization_128x128.png +[EventChannel]: assets/pictures/EventChannel.png \ No newline at end of file diff --git a/WinEventLogCustomization/WinEventLogCustomization.psd1 b/WinEventLogCustomization/WinEventLogCustomization.psd1 index afc2a9d..aa255fe 100644 --- a/WinEventLogCustomization/WinEventLogCustomization.psd1 +++ b/WinEventLogCustomization/WinEventLogCustomization.psd1 @@ -1,55 +1,54 @@ @{ # Script module or binary module file associated with this manifest - RootModule = 'WinEventLogCustomization.psm1' + RootModule = 'WinEventLogCustomization.psm1' # Version number of this module. - ModuleVersion = '0.9.0' + ModuleVersion = '1.0.0' # ID used to uniquely identify this module - GUID = '9268705a-75d5-401c-b13d-4d1a8f380b17' + GUID = '9268705a-75d5-401c-b13d-4d1a8f380b17' # Author of this module - Author = 'Andreas Bellstedt' + Author = 'Andreas Bellstedt' # Company or vendor of this module - CompanyName = '' + CompanyName = '' # Copyright statement for this module - Copyright = 'Copyright (c) 2022 Andreas Bellstedt' + Copyright = 'Copyright (c) 2022 Andreas Bellstedt' # Description of the functionality provided by this module - Description = 'Module for creating and managing custom Windows EventLog channels' + Description = 'Module for creating and managing custom Windows EventLog channels' # Minimum version of the Windows PowerShell engine required by this module - PowerShellVersion = '5.1' + PowerShellVersion = '5.1' # Supported PSEditions CompatiblePSEditions = 'Desktop' # Modules that must be imported into the global environment prior to importing # this module - RequiredModules = @( + RequiredModules = @( @{ - ModuleName='PSFramework'; - ModuleVersion='1.7.227' + ModuleName = 'PSFramework'; + ModuleVersion = '1.7.227' } ) # Assemblies that must be loaded prior to importing this module - # RequiredAssemblies = @('bin\WinEventLogCustomization.dll') - RequiredAssemblies = @( + RequiredAssemblies = @( 'bin\EPPlus.Net40.dll' 'bin\WinEventLogCustomization.dll' ) # Type files (.ps1xml) to be loaded when importing this module - TypesToProcess = @('xml\WinEventLogCustomization.Types.ps1xml') + TypesToProcess = @('xml\WinEventLogCustomization.Types.ps1xml') # Format files (.ps1xml) to be loaded when importing this module - FormatsToProcess = @('xml\WinEventLogCustomization.Format.ps1xml') + FormatsToProcess = @('xml\WinEventLogCustomization.Format.ps1xml') # Functions to export from this module - FunctionsToExport = @( + FunctionsToExport = @( 'Import-WELCChannelDefinition', 'New-WELCEventChannelManifest', 'Register-WELCEventChannelManifest', @@ -62,40 +61,58 @@ ) # Cmdlets to export from this module - CmdletsToExport = '' + CmdletsToExport = '' # Variables to export from this module - VariablesToExport = '' + VariablesToExport = '' # Aliases to export from this module - AliasesToExport = '' + AliasesToExport = '' # List of all modules packaged with this module - ModuleList = @() + ModuleList = @() # List of all files packaged with this module - FileList = @() + FileList = @() # Private data to pass to the module specified in ModuleToProcess. This may also contain a PSData hashtable with additional module metadata used by PowerShell. - PrivateData = @{ - + PrivateData = @{ #Support for PowerShellGet galleries. PSData = @{ - # Tags applied to this module. These help with module discovery in online galleries. - # Tags = @() + Tags = @( + 'EventLog', + 'WindowsEvent', + 'WindowsEventLog', + 'EventLogChannel', + 'EventLogChannels', + 'EventChannel', + 'EventChannels', + 'CustomEventChannel', + 'CustomEventLog', + 'CustomEventLogChannel', + 'CustomEventLogFile', + 'CustomEventLogFiles', + 'EventLogManifest', + 'LogFile', + 'LogFiles', + 'Automation', + 'Logging', + 'PSEdition_Desktop', + 'Windows' + ) # A URL to the license for this module. - # LicenseUri = '' + LicenseUri = 'https://github.com/AndiBellstedt/WinEventLogCustomization/blob/main/license' # A URL to the main website for this project. - # ProjectUri = '' + ProjectUri = 'https://github.com/AndiBellstedt/WinEventLogCustomization' # A URL to an icon representing this module. - # IconUri = '' + IconUri = 'https://github.com/AndiBellstedt/WinEventLogCustomization/raw/main/assets/WinEventLogCustomization_128x128.png' # ReleaseNotes of this module - # ReleaseNotes = '' + ReleaseNotes = 'https://github.com/AndiBellstedt/WinEventLogCustomization/blob/main/WinEventLogCustomization/changelog.md' } # End of PSData hashtable diff --git a/WinEventLogCustomization/WinEventLogCustomization.psm1 b/WinEventLogCustomization/WinEventLogCustomization.psm1 index 5a23307..457fbe2 100644 --- a/WinEventLogCustomization/WinEventLogCustomization.psm1 +++ b/WinEventLogCustomization/WinEventLogCustomization.psm1 @@ -18,65 +18,63 @@ $importIndividualFiles = Get-PSFConfigValue -FullName WinEventLogCustomization.I if ($WinEventLogCustomization_importIndividualFiles) { $importIndividualFiles = $true } if (Test-Path (Resolve-PSFPath -Path "$($script:ModuleRoot)\..\.git" -SingleItem -NewChild)) { $importIndividualFiles = $true } if ("" -eq '') { $importIndividualFiles = $true } - -function Import-ModuleFile -{ - <# - .SYNOPSIS - Loads files into the module on module import. - - .DESCRIPTION - This helper function is used during module initialization. - It should always be dotsourced itself, in order to proper function. - - This provides a central location to react to files being imported, if later desired - - .PARAMETER Path - The path to the file to load - - .EXAMPLE - PS C:\> . Import-ModuleFile -File $function.FullName - - Imports the file stored in $function according to import policy - #> - [CmdletBinding()] - Param ( - [string] - $Path - ) - - $resolvedPath = $ExecutionContext.SessionState.Path.GetResolvedPSPathFromPSPath($Path).ProviderPath - if ($doDotSource) { . $resolvedPath } - else { $ExecutionContext.InvokeCommand.InvokeScript($false, ([scriptblock]::Create([io.file]::ReadAllText($resolvedPath))), $null, $null) } + +function Import-ModuleFile { + <# + .SYNOPSIS + Loads files into the module on module import. + + .DESCRIPTION + This helper function is used during module initialization. + It should always be dotsourced itself, in order to proper function. + + This provides a central location to react to files being imported, if later desired + + .PARAMETER Path + The path to the file to load + + .EXAMPLE + PS C:\> . Import-ModuleFile -File $function.FullName + + Imports the file stored in $function according to import policy + #> + [CmdletBinding()] + Param ( + [string] + $Path + ) + + $resolvedPath = $ExecutionContext.SessionState.Path.GetResolvedPSPathFromPSPath($Path).ProviderPath + if ($doDotSource) { . $resolvedPath } + else { $ExecutionContext.InvokeCommand.InvokeScript($false, ([scriptblock]::Create([io.file]::ReadAllText($resolvedPath))), $null, $null) } } #region Load individual files -if ($importIndividualFiles) -{ - # Execute Preimport actions - foreach ($path in (& "$ModuleRoot\internal\scripts\preimport.ps1")) { - . Import-ModuleFile -Path $path - } - - # Import all internal functions - foreach ($function in (Get-ChildItem "$ModuleRoot\internal\functions" -Filter "*.ps1" -Recurse -ErrorAction Ignore)) - { - . Import-ModuleFile -Path $function.FullName - } - - # Import all public functions - foreach ($function in (Get-ChildItem "$ModuleRoot\functions" -Filter "*.ps1" -Recurse -ErrorAction Ignore)) - { - . Import-ModuleFile -Path $function.FullName - } - - # Execute Postimport actions - foreach ($path in (& "$ModuleRoot\internal\scripts\postimport.ps1")) { - . Import-ModuleFile -Path $path - } - - # End it here, do not load compiled code below - return +if ($importIndividualFiles) { + # Execute Preimport actions + foreach ($path in (& "$ModuleRoot\internal\scripts\preimport.ps1")) { + . Import-ModuleFile -Path $path + } + + # Import all internal functions + foreach ($function in (Get-ChildItem "$ModuleRoot\internal\functions" -Filter "*.ps1" -Recurse -ErrorAction Ignore)) { + . Import-ModuleFile -Path $function.FullName + } + + # Import all public functions + $functions = (Get-ChildItem "$ModuleRoot\functions" -Filter "*.ps1" -Recurse -ErrorAction Ignore) + $function = $functions[6] + foreach ($function in $functions) { + . Import-ModuleFile -Path $function.FullName + } + + # Execute Postimport actions + foreach ($path in (& "$ModuleRoot\internal\scripts\postimport.ps1")) { + . Import-ModuleFile -Path $path + } + + # End it here, do not load compiled code below + return } #endregion Load individual files diff --git a/WinEventLogCustomization/bin/WinEventLogCustomization.xltx b/WinEventLogCustomization/bin/WinEventLogCustomization.xltx index e414691..3ba735e 100644 Binary files a/WinEventLogCustomization/bin/WinEventLogCustomization.xltx and b/WinEventLogCustomization/bin/WinEventLogCustomization.xltx differ diff --git a/WinEventLogCustomization/changelog.md b/WinEventLogCustomization/changelog.md index 75634fa..68b270f 100644 --- a/WinEventLogCustomization/changelog.md +++ b/WinEventLogCustomization/changelog.md @@ -1,5 +1,15 @@ # Changelog -## 1.0.0 (2022-06-26) - - New: Some Stuff - - Upd: Moar Stuff - - Fix: Much Stuff \ No newline at end of file +## 1.0.0 (2022-07-24) +First official release. + - New: Introducing functions within the module + - Get-WELCEventChannel + - Import-WELCChannelDefinition + - Move-WELCEventChannelManifest + - New-WELCEventChannelManifest + - Open-WELCExcelTemplate + - Register-WELCEventChannelManifest + - Set-WELCEventChannel + - Test-WELCEventChannelManifest + - Unregister-WELCEventChannelManifest + - Upd: --- + - Fix: --- \ No newline at end of file diff --git a/WinEventLogCustomization/en-us/about_WinEventLogCustomization.help.txt b/WinEventLogCustomization/en-us/about_WinEventLogCustomization.help.txt index fc11b87..19b8199 100644 --- a/WinEventLogCustomization/en-us/about_WinEventLogCustomization.help.txt +++ b/WinEventLogCustomization/en-us/about_WinEventLogCustomization.help.txt @@ -1,11 +1,120 @@ TOPIC - about_WinEventLogCustomization - + about_WinEventLogCustomization + SHORT DESCRIPTION - Explains how to use the WinEventLogCustomization powershell module - + A PowerShell module helping you build custom eventlog channels and registering them into Windows Event Viewer. + + The build logs appear under "Application and Services", + even like the "Windows PowerShell" or the "PowerShellCore/Operational" EventLog. + + All cmdlets are build with + - powershell regular verbs + - pipeline availabilities wherever it makes sense + - comprehensive logging on verbose and debug channel by the logging system of PSFramework + + Prerequisites + - Windows PowerShell 5.1 + - PowerShell 6 or 7 + - Administrative Priviledges are required for registering or unregistering EventChannels + LONG DESCRIPTION - + Creating a manifest for a EventChannel + -------------------------------------- + + For a quick start you can just execute: + New-WELCEventChannelManifest -ChannelFullName "AndiBellstedt/MyPersonalLog" + + another way is the following command style, if you are not familiar with the notation on ChannelFullNames: + New-WELCEventChannelManifest -RootFolderName "AndiBellstedt" -FolderSecondLevel "PowerShell" -FolderThirdLevel "Tasks" -ChannelName "Operational" + + + This will create a manifest- and a dll file (AndiBellstedt.man & AndiBellstedt.dll) within you current directory. + With the manifest file, the dll file can be registered to Windows EventLog system. + + !Attention!, the manifest file contains the paths to the dll and should not be moved in the Windows Explorer. + There is a command in the module to move the manifest with it's dll file consistently. + + + Register the EventChannel + ------------------------- + + Registering a manifest and its dll file is also easy: + Register-WELCEventChannelManifest -Path .\AndiBellstedt.man + + !Attention!, executing this command will require admninistrative priviledges. + Due to the fact, that changes on the Windows EventLog system are a administrative task. + + Following this, results in a new folder "AndiBellstedt" with two subfolders ("PowerShell" & "Tasks") + and a EventLog "Operational" under "Application and Services Logs" withing the Event Viewer. + + + Remove the EventChannel + ----------------------- + + If the EventChannel is no longer needed, it can be removed by unregistering the manifest: + UnRegister-WELCEventChannelManifest -Path .\AndiBellstedt.man + + + Show registered EventChannels + ----------------------------- + + After registering a manifest, the defined EventChannel can be queried + To query a EventChannel you can use: + Get-WELCEventChannel -ChannelFullName "AndiBellstedt-PowerShell-Tasks/Operational" + + This will output something like this, showing you the details and the config of the EventChannel: + PS C:\> Get-WELCEventChannel -ChannelFullName "AndiBellstedt-PowerShell-Tasks/Operational" | Format-List + + ComputerName : MyComputer + Name : AndiBellstedt-PowerShell-Tasks/Operational + Enabled : False + LogMode : Circular + LogType : Administrative + LogFullName : C:\WINDOWS\System32\Winevt\Logs\AndiBellstedt-PowerShell-Tasks%4Operational.evtx + MaxEventLogSize : 1052672 + FileSize : + RecordCount : + IsFull : + LastWriteTime : + LastAccessTime : + ProviderName : AndiBellstedt-PowerShell-Tasks + ProviderId : 43b94bbe-2d97-4f04-96b4-c254483b53f4 + MessageFilePath : C:\EventLogs\AndiBellstedt.dll + ResourceFilePath : C:\EventLogs\AndiBellstedt.dll + ParameterFilePath : C:\EventLogs\AndiBellstedt.dll + Owner : Administrators + Access : {NT AUTORITY\BATCH: AccessAllowed (ListDirectory, WriteData), NT AUTORITY\INTERACTIVE: + AccessAllowed (ListDirectory, WriteData), NT AUTORITY\SERVICE: AccessAllowed (ListDirectory, + WriteData), NT AUTORITY\SYSTEM: AccessAllowed (ChangePermissions, CreateDirectories, Delete, + GenericExecute, ListDirectory, ReadPermissions, TakeOwnership, WriteData, WriteKey)…} + + Configuration on EventChannels + ------------------------------ + + There are multiple ways to configure a EventChannel. + The first, and explicit one is: + Set-WELCEventChannel -ChannelFullName "AndiBellstedt-PowerShell-Tasks/Operational" -Enabled $true -MaxEventLogSize 1GB -LogMode Circular -LogFilePath "C:\EventLogs\AB-PS-T-Ops.evtx" + + Another way is to pipe in the result of a Get-WELCEventChannel command: + $channel = Get-WELCEventChannel "AndiBellstedt*" + + $channel | Set-WELCEventChannel -Enabled $true -MaxEventLogSize 1GB -LogMode AutoBackup -LogFilePath "C:\EventLogs" + + Doing it this way, $channel can contain more than one EventChannel to configure. + KEYWORDS - WinEventLogCustomization \ No newline at end of file + WinEventLogCustomization + EventLog + WindowsEvent + WindowsEventLog + EventLogChannel + EventLogChannels + EventChannel + EventChannels + CustomEventChannel + CustomEventLog + CustomEventLogChannel + CustomEventLogFile + CustomEventLogFiles + EventLogManifest diff --git a/WinEventLogCustomization/en-us/strings.psd1 b/WinEventLogCustomization/en-us/strings.psd1 index 7014c33..52a70e5 100644 --- a/WinEventLogCustomization/en-us/strings.psd1 +++ b/WinEventLogCustomization/en-us/strings.psd1 @@ -1,5 +1,5 @@ # This is where the strings go, that are written by # Write-PSFMessage, Stop-PSFFunction or the PSFramework validation scriptblocks @{ - 'key' = 'Value' + 'key' = 'Value' } \ No newline at end of file diff --git a/WinEventLogCustomization/functions/Import-WELCChannelDefinition.ps1 b/WinEventLogCustomization/functions/Import-WELCChannelDefinition.ps1 index 8ded307..041e94c 100644 --- a/WinEventLogCustomization/functions/Import-WELCChannelDefinition.ps1 +++ b/WinEventLogCustomization/functions/Import-WELCChannelDefinition.ps1 @@ -209,7 +209,7 @@ ChannelName = $item.ChannelName LogFullName = $item.LogFullName LogMode = $item.LogMode - Enabled = [bool]::Parse($item.Enabled) + Enabled = [bool]::Parse($item.ChannelEnabled) MaxEventLogSize = $item.MaxEventLogSize / 1 } $output diff --git a/WinEventLogCustomization/functions/Open-WELCExcelTemplate.ps1 b/WinEventLogCustomization/functions/Open-WELCExcelTemplate.ps1 index 0bbb8b1..937a087 100644 --- a/WinEventLogCustomization/functions/Open-WELCExcelTemplate.ps1 +++ b/WinEventLogCustomization/functions/Open-WELCExcelTemplate.ps1 @@ -35,6 +35,7 @@ end { $path = "$($ModuleRoot)\bin\WinEventLogCustomization.xltx" $pathExtension = $path.Split(".")[-1] + $null = New-PSDrive -PSProvider registry -Root HKEY_CLASSES_ROOT -Name HKCR Write-PSFMessage -Level Debug -Message "Looking for application to open '$($pathExtension)' files" # parse registry for file extension diff --git a/WinEventLogCustomization/functions/Set-WELCEventChannel.ps1 b/WinEventLogCustomization/functions/Set-WELCEventChannel.ps1 index 4c818d9..2862c54 100644 --- a/WinEventLogCustomization/functions/Set-WELCEventChannel.ps1 +++ b/WinEventLogCustomization/functions/Set-WELCEventChannel.ps1 @@ -120,6 +120,7 @@ PositionalBinding = $true, ConfirmImpact = 'Medium' )] + [Diagnostics.CodeAnalysis.SuppressMessageAttribute("PSReviewUnusedParameter", '', Justification = "Intentional, Pester not covering the usage correct")] Param( [Parameter( ParameterSetName = "TemplateChannelConfig", @@ -285,14 +286,14 @@ $null = $configList.Add( [PSCustomObject]@{ EventChannel = $eventChannelItem - Enabled = if (Test-PSFParameterBinding -ParameterName Enabled) { $Enabled } - MaxEventLogSize = if (Test-PSFParameterBinding -ParameterName MaxEventLogSize) { $MaxEventLogSize } - LogMode = if (Test-PSFParameterBinding -ParameterName LogMode) { $LogMode } - LogFileFullName = if ($logFileFullName -like "ToBeCalculated") { "$($logFileFolder)\$($eventChannelItem.LogFile)" } elseif (Test-PSFParameterBinding -ParameterName LogFilePath) { $logFileFullName } - LogFilePath = if (Test-PSFParameterBinding -ParameterName LogFilePath) { $logFileFolder } - CompressLogFolder = if (Test-PSFParameterBinding -ParameterName CompressLogFolder) { $CompressLogFolder } - AllowFileAccessForLocalService = if (Test-PSFParameterBinding -ParameterName AllowFileAccessForLocalService) { $AllowFileAccessForLocalService } - EventChannelSDDL = if (Test-PSFParameterBinding -ParameterName EventChannelSDDL) { $EventChannelSDDL } + Enabled = ( if (Test-PSFParameterBinding -ParameterName Enabled) { $Enabled } ) + MaxEventLogSize = ( if (Test-PSFParameterBinding -ParameterName MaxEventLogSize) { $MaxEventLogSize } ) + LogMode = ( if (Test-PSFParameterBinding -ParameterName LogMode) { $LogMode } ) + LogFileFullName = ( if ($logFileFullName -like "ToBeCalculated") { "$($logFileFolder)\$($eventChannelItem.LogFile)" } elseif (Test-PSFParameterBinding -ParameterName LogFilePath) { $logFileFullName } ) + LogFilePath = ( if (Test-PSFParameterBinding -ParameterName LogFilePath) { $logFileFolder } ) + CompressLogFolder = ( if (Test-PSFParameterBinding -ParameterName CompressLogFolder) { $CompressLogFolder } ) + AllowFileAccessForLocalService = ( if (Test-PSFParameterBinding -ParameterName AllowFileAccessForLocalService) { $AllowFileAccessForLocalService } ) + EventChannelSDDL = ( if (Test-PSFParameterBinding -ParameterName EventChannelSDDL) { $EventChannelSDDL } ) } ) } @@ -309,14 +310,14 @@ $null = $configList.Add( [PSCustomObject]@{ EventChannel = $eventChannelItem - Enabled = if (Test-PSFParameterBinding -ParameterName Enabled) { $Enabled } - MaxEventLogSize = if (Test-PSFParameterBinding -ParameterName MaxEventLogSize) { $MaxEventLogSize } - LogMode = if (Test-PSFParameterBinding -ParameterName LogMode) { $LogMode } - LogFileFullName = if ($logFileFullName -like "ToBeCalculated") { "$($logFileFolder)\$($eventChannelItem.LogFile)" } elseif (Test-PSFParameterBinding -ParameterName LogFilePath) { $logFileFullName } - LogFilePath = if (Test-PSFParameterBinding -ParameterName LogFilePath) { $logFileFolder } - CompressLogFolder = if (Test-PSFParameterBinding -ParameterName CompressLogFolder) { $CompressLogFolder } - AllowFileAccessForLocalService = if (Test-PSFParameterBinding -ParameterName AllowFileAccessForLocalService) { $AllowFileAccessForLocalService } - EventChannelSDDL = if (Test-PSFParameterBinding -ParameterName EventChannelSDDL) { $EventChannelSDDL } + Enabled = ( if (Test-PSFParameterBinding -ParameterName Enabled) { $Enabled } ) + MaxEventLogSize = ( if (Test-PSFParameterBinding -ParameterName MaxEventLogSize) { $MaxEventLogSize } ) + LogMode = ( if (Test-PSFParameterBinding -ParameterName LogMode) { $LogMode } ) + LogFileFullName = ( if ($logFileFullName -like "ToBeCalculated") { "$($logFileFolder)\$($eventChannelItem.LogFile)" } elseif (Test-PSFParameterBinding -ParameterName LogFilePath) { $logFileFullName } ) + LogFilePath = ( if (Test-PSFParameterBinding -ParameterName LogFilePath) { $logFileFolder } ) + CompressLogFolder = ( if (Test-PSFParameterBinding -ParameterName CompressLogFolder) { $CompressLogFolder } ) + AllowFileAccessForLocalService = ( if (Test-PSFParameterBinding -ParameterName AllowFileAccessForLocalService) { $AllowFileAccessForLocalService } ) + EventChannelSDDL = ( if (Test-PSFParameterBinding -ParameterName EventChannelSDDL) { $EventChannelSDDL } ) } ) } diff --git a/WinEventLogCustomization/internal/scripts/license.ps1 b/WinEventLogCustomization/internal/scripts/license.ps1 index 0ba64e4..0a024f7 100644 --- a/WinEventLogCustomization/internal/scripts/license.ps1 +++ b/WinEventLogCustomization/internal/scripts/license.ps1 @@ -1,5 +1,5 @@ -New-PSFLicense -Product 'WinEventLogCustomization' -Manufacturer 'Andreas.Bellstedt' -ProductVersion $script:ModuleVersion -ProductType Module -Name MIT -Version "1.0.0.0" -Date (Get-Date "2022-06-26") -Text @" -Copyright (c) 2022 Andreas.Bellstedt +New-PSFLicense -Product 'WinEventLogCustomization' -Manufacturer 'Andreas Bellstedt' -ProductVersion $script:ModuleVersion -ProductType Module -Name MIT -Version "1.0.0.0" -Date (Get-Date "2022-06-26") -Text @" +Copyright (c) 2022 Andreas Bellstedt Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal diff --git a/WinEventLogCustomization/internal/scripts/preimport.ps1 b/WinEventLogCustomization/internal/scripts/preimport.ps1 index 9bec8da..a2b9a2d 100644 --- a/WinEventLogCustomization/internal/scripts/preimport.ps1 +++ b/WinEventLogCustomization/internal/scripts/preimport.ps1 @@ -12,5 +12,3 @@ $moduleRoot = Split-Path (Split-Path $PSScriptRoot) # Load the strings used in messages "$moduleRoot\internal\scripts\strings.ps1" - -$null = New-PSDrive -PSProvider registry -Root HKEY_CLASSES_ROOT -Name HKCR \ No newline at end of file diff --git a/WinEventLogCustomization/internal/tepp/WinEventLogCustomization.tepp.ps1 b/WinEventLogCustomization/internal/tepp/WinEventLogCustomization.tepp.ps1 new file mode 100644 index 0000000..9c5ba28 --- /dev/null +++ b/WinEventLogCustomization/internal/tepp/WinEventLogCustomization.tepp.ps1 @@ -0,0 +1,39 @@ +Register-PSFTeppScriptblock -Name "WinEventLogCustomization.ChannelFullName" -ScriptBlock { + Get-WinEvent -ListLog * -ErrorAction Ignore | Select-Object -ExpandProperty LogName +} + +Register-PSFTeppScriptblock -Name "WinEventLogCustomization.Bool" -ScriptBlock { + @( + '$true', + '$false' + ) +} + +Register-PSFTeppScriptblock -Name "WinEventLogCustomization.MaxEventLogSize" -ScriptBlock { + @( + '16MB', + '64MB', + '128MB', + '512MB', + '1GB', + '2GB', + '5GB', + '10GB' + ) +} + +Register-PSFTeppScriptblock -Name "WinEventLogCustomization.FolderRoot" -ScriptBlock { + Get-WinEvent -ListLog *-* -ErrorAction Ignore | Select-Object -ExpandProperty LogName | ForEach-Object { $_.split("-")[0] } | Sort-Object -Unique +} + +Register-PSFTeppScriptblock -Name "WinEventLogCustomization.FolderSecondLevel" -ScriptBlock { + Get-WinEvent -ListLog *-*-* -ErrorAction Ignore | Select-Object -ExpandProperty LogName | ForEach-Object { $_.split("-")[1] } | Sort-Object -Unique +} + +Register-PSFTeppScriptblock -Name "WinEventLogCustomization.FolderThirdLevel" -ScriptBlock { + Get-WinEvent -ListLog *-*-* -ErrorAction Ignore | Select-Object -ExpandProperty LogName | ForEach-Object { $_.split("-")[2].split("/")[0] } | Sort-Object -Unique +} + +Register-PSFTeppScriptblock -Name "WinEventLogCustomization.ChannelName" -ScriptBlock { + Get-WinEvent -ListLog */* -ErrorAction Ignore | Select-Object -ExpandProperty LogName | ForEach-Object { $_.split("/")[1] } | Sort-Object -Unique +} diff --git a/WinEventLogCustomization/internal/tepp/assignment.ps1 b/WinEventLogCustomization/internal/tepp/assignment.ps1 index 9bc0c03..9f449d5 100644 --- a/WinEventLogCustomization/internal/tepp/assignment.ps1 +++ b/WinEventLogCustomization/internal/tepp/assignment.ps1 @@ -1,4 +1,15 @@ -<# -# Example: -Register-PSFTeppArgumentCompleter -Command Get-Alcohol -Parameter Type -Name WinEventLogCustomization.alcohol -#> \ No newline at end of file +# Get-WELCEventChannel +Register-PSFTeppArgumentCompleter -Command Get-WELCEventChannel -Parameter "ChannelFullName" -Name "WinEventLogCustomization.ChannelFullName" + +# Set-WELCEventChannel +Register-PSFTeppArgumentCompleter -Command Set-WELCEventChannel -Parameter "ChannelFullName" -Name "WinEventLogCustomization.ChannelFullName" +Register-PSFTeppArgumentCompleter -Command Set-WELCEventChannel -Parameter "Enabled" -Name "WinEventLogCustomization.Bool" +Register-PSFTeppArgumentCompleter -Command Set-WELCEventChannel -Parameter "CompressLogFolder" -Name "WinEventLogCustomization.Bool" +Register-PSFTeppArgumentCompleter -Command Set-WELCEventChannel -Parameter "AllowFileAccessForLocalService" -Name "WinEventLogCustomization.Bool" +Register-PSFTeppArgumentCompleter -Command Set-WELCEventChannel -Parameter "MaxEventLogSize" -Name "WinEventLogCustomization.MaxEventLogSize" + +# New-WELCEventChannelManifest +Register-PSFTeppArgumentCompleter -Command New-WELCEventChannelManifest -Parameter "FolderRoot" -Name "WinEventLogCustomization.FolderRoot" +Register-PSFTeppArgumentCompleter -Command New-WELCEventChannelManifest -Parameter "FolderSecondLevel" -Name "WinEventLogCustomization.FolderSecondLevel" +Register-PSFTeppArgumentCompleter -Command New-WELCEventChannelManifest -Parameter "FolderThirdLevel" -Name "WinEventLogCustomization.FolderThirdLevel" +Register-PSFTeppArgumentCompleter -Command New-WELCEventChannelManifest -Parameter "ChannelName" -Name "WinEventLogCustomization.ChannelName" diff --git a/WinEventLogCustomization/internal/tepp/example.tepp.ps1 b/WinEventLogCustomization/internal/tepp/example.tepp.ps1 deleted file mode 100644 index 69174bb..0000000 --- a/WinEventLogCustomization/internal/tepp/example.tepp.ps1 +++ /dev/null @@ -1,4 +0,0 @@ -<# -# Example: -Register-PSFTeppScriptblock -Name "WinEventLogCustomization.alcohol" -ScriptBlock { 'Beer','Mead','Whiskey','Wine','Vodka','Rum (3y)', 'Rum (5y)', 'Rum (7y)' } -#> \ No newline at end of file diff --git a/WinEventLogCustomization/readme.md b/WinEventLogCustomization/readme.md deleted file mode 100644 index b122789..0000000 --- a/WinEventLogCustomization/readme.md +++ /dev/null @@ -1,17 +0,0 @@ -# PSFModule guidance - -This is a finished module layout optimized for implementing the PSFramework. - -If you don't care to deal with the details, this is what you need to do to get started seeing results: - - - Add the functions you want to publish to `/functions/` - - Update the `FunctionsToExport` node in the module manifest (WinEventLogCustomization.psd1). All functions you want to publish should be in a list. - - Add internal helper functions the user should not see to `/internal/functions/` - - ## Path Warning - - > If you want your module to be compatible with Linux and MacOS, keep in mind that those OS are case sensitive for paths and files. - - `Import-ModuleFile` is preconfigured to resolve the path of the files specified, so it will reliably convert weird path notations the system can't handle. - Content imported through that command thus need not mind the path separator. - If you want to make sure your code too will survive OS-specific path notations, get used to using `Resolve-path` or the more powerful `Resolve-PSFPath`. \ No newline at end of file diff --git a/WinEventLogCustomization/tests/general/FileIntegrity.Exceptions.ps1 b/WinEventLogCustomization/tests/general/FileIntegrity.Exceptions.ps1 index 8a9d563..c705779 100644 --- a/WinEventLogCustomization/tests/general/FileIntegrity.Exceptions.ps1 +++ b/WinEventLogCustomization/tests/general/FileIntegrity.Exceptions.ps1 @@ -1,37 +1,37 @@ # List of forbidden commands $global:BannedCommands = @( - 'Write-Host' - 'Write-Verbose' - 'Write-Warning' - 'Write-Error' - 'Write-Output' - 'Write-Information' - 'Write-Debug' + 'Write-Host' + 'Write-Verbose' + 'Write-Warning' + 'Write-Error' + 'Write-Output' + 'Write-Information' + 'Write-Debug' - # Use CIM instead where possible - 'Get-WmiObject' - 'Invoke-WmiMethod' - 'Register-WmiEvent' - 'Remove-WmiObject' - 'Set-WmiInstance' + # Use CIM instead where possible + 'Get-WmiObject' + 'Invoke-WmiMethod' + 'Register-WmiEvent' + 'Remove-WmiObject' + 'Set-WmiInstance' - # Use Get-WinEvent instead - 'Get-EventLog' + # Use Get-WinEvent instead + 'Get-EventLog' ) <# - Contains list of exceptions for banned cmdlets. - Insert the file names of files that may contain them. + Contains list of exceptions for banned cmdlets. + Insert the file names of files that may contain them. - Example: - "Write-Host" = @('Write-PSFHostColor.ps1','Write-PSFMessage.ps1') + Example: + "Write-Host" = @('Write-PSFHostColor.ps1','Write-PSFMessage.ps1') #> $global:MayContainCommand = @{ - "Write-Host" = @() - "Write-Verbose" = @() - "Write-Warning" = @("Set-WELCEventChannel.ps1") - "Write-Error" = @("Get-WELCEventChannel.ps1", "Set-WELCEventChannel.ps1", "Register-WELCEventChannelManifest.ps1", "Unregister-WELCEventChannelManifest.ps1") - "Write-Output" = @() - "Write-Information" = @("Register-WELCEventChannelManifest.ps1", "Unregister-WELCEventChannelManifest.ps1") - "Write-Debug" = @() + "Write-Host" = @() + "Write-Verbose" = @() + "Write-Warning" = @("Set-WELCEventChannel.ps1") + "Write-Error" = @("Get-WELCEventChannel.ps1", "Set-WELCEventChannel.ps1", "Register-WELCEventChannelManifest.ps1", "Unregister-WELCEventChannelManifest.ps1") + "Write-Output" = @() + "Write-Information" = @("Register-WELCEventChannelManifest.ps1", "Unregister-WELCEventChannelManifest.ps1") + "Write-Debug" = @() } \ No newline at end of file diff --git a/WinEventLogCustomization/tests/general/FileIntegrity.Tests.ps1 b/WinEventLogCustomization/tests/general/FileIntegrity.Tests.ps1 index 89e6c9c..369a698 100644 --- a/WinEventLogCustomization/tests/general/FileIntegrity.Tests.ps1 +++ b/WinEventLogCustomization/tests/general/FileIntegrity.Tests.ps1 @@ -3,93 +3,85 @@ . "$global:testroot\general\FileIntegrity.Exceptions.ps1" Describe "Verifying integrity of module files" { - BeforeAll { - function Get-FileEncoding - { - <# - .SYNOPSIS - Tests a file for encoding. - - .DESCRIPTION - Tests a file for encoding. - - .PARAMETER Path - The file to test - #> - [CmdletBinding()] - Param ( - [Parameter(Mandatory = $True, ValueFromPipelineByPropertyName = $True)] - [Alias('FullName')] - [string] - $Path - ) - - if ($PSVersionTable.PSVersion.Major -lt 6) - { - [byte[]]$byte = get-content -Encoding byte -ReadCount 4 -TotalCount 4 -Path $Path - } - else - { - [byte[]]$byte = Get-Content -AsByteStream -ReadCount 4 -TotalCount 4 -Path $Path - } - - if ($byte[0] -eq 0xef -and $byte[1] -eq 0xbb -and $byte[2] -eq 0xbf) { 'UTF8 BOM' } - elseif ($byte[0] -eq 0xfe -and $byte[1] -eq 0xff) { 'Unicode' } - elseif ($byte[0] -eq 0 -and $byte[1] -eq 0 -and $byte[2] -eq 0xfe -and $byte[3] -eq 0xff) { 'UTF32' } - elseif ($byte[0] -eq 0x2b -and $byte[1] -eq 0x2f -and $byte[2] -eq 0x76) { 'UTF7' } - else { 'Unknown' } - } - } - - Context "Validating PS1 Script files" { - $allFiles = Get-ChildItem -Path $moduleRoot -Recurse | Where-Object Name -like "*.ps1" | Where-Object FullName -NotLike "$moduleRoot\tests\*" - - foreach ($file in $allFiles) - { - $name = $file.FullName.Replace("$moduleRoot\", '') - - It "[$name] Should have UTF8 encoding with Byte Order Mark" -TestCases @{ file = $file } { - Get-FileEncoding -Path $file.FullName | Should -Be 'UTF8 BOM' - } - - It "[$name] Should have no trailing space" -TestCases @{ file = $file } { - ($file | Select-String "\s$" | Where-Object { $_.Line.Trim().Length -gt 0}).LineNumber | Should -BeNullOrEmpty - } - - $tokens = $null - $parseErrors = $null - $ast = [System.Management.Automation.Language.Parser]::ParseFile($file.FullName, [ref]$tokens, [ref]$parseErrors) - - It "[$name] Should have no syntax errors" -TestCases @{ parseErrors = $parseErrors } { - $parseErrors | Should -BeNullOrEmpty - } - - foreach ($command in $global:BannedCommands) - { - if ($global:MayContainCommand["$command"] -notcontains $file.Name) - { - It "[$name] Should not use $command" -TestCases @{ tokens = $tokens; command = $command } { - $tokens | Where-Object Text -EQ $command | Should -BeNullOrEmpty - } - } - } - } - } - - Context "Validating help.txt help files" { - $allFiles = Get-ChildItem -Path $moduleRoot -Recurse | Where-Object Name -like "*.help.txt" | Where-Object FullName -NotLike "$moduleRoot\tests\*" - - foreach ($file in $allFiles) - { - $name = $file.FullName.Replace("$moduleRoot\", '') - - It "[$name] Should have UTF8 encoding" -TestCases @{ file = $file } { - Get-FileEncoding -Path $file.FullName | Should -Be 'UTF8 BOM' - } - - It "[$name] Should have no trailing space" -TestCases @{ file = $file } { - ($file | Select-String "\s$" | Where-Object { $_.Line.Trim().Length -gt 0 } | Measure-Object).Count | Should -Be 0 - } - } - } + BeforeAll { + function Get-FileEncoding { + <# + .SYNOPSIS + Tests a file for encoding. + + .DESCRIPTION + Tests a file for encoding. + + .PARAMETER Path + The file to test + #> + [CmdletBinding()] + Param ( + [Parameter(Mandatory = $True, ValueFromPipelineByPropertyName = $True)] + [Alias('FullName')] + [string] + $Path + ) + + if ($PSVersionTable.PSVersion.Major -lt 6) { + [byte[]]$byte = get-content -Encoding byte -ReadCount 4 -TotalCount 4 -Path $Path + } else { + [byte[]]$byte = Get-Content -AsByteStream -ReadCount 4 -TotalCount 4 -Path $Path + } + + if ($byte[0] -eq 0xef -and $byte[1] -eq 0xbb -and $byte[2] -eq 0xbf) { 'UTF8 BOM' } + elseif ($byte[0] -eq 0xfe -and $byte[1] -eq 0xff) { 'Unicode' } + elseif ($byte[0] -eq 0 -and $byte[1] -eq 0 -and $byte[2] -eq 0xfe -and $byte[3] -eq 0xff) { 'UTF32' } + elseif ($byte[0] -eq 0x2b -and $byte[1] -eq 0x2f -and $byte[2] -eq 0x76) { 'UTF7' } + else { 'Unknown' } + } + } + + Context "Validating PS1 Script files" { + $allFiles = Get-ChildItem -Path $moduleRoot -Recurse | Where-Object Name -like "*.ps1" | Where-Object FullName -NotLike "$moduleRoot\tests\*" + + foreach ($file in $allFiles) { + $name = $file.FullName.Replace("$moduleRoot\", '') + + It "[$name] Should have UTF8 encoding with Byte Order Mark" -TestCases @{ file = $file } { + Get-FileEncoding -Path $file.FullName | Should -Be 'UTF8 BOM' + } + + It "[$name] Should have no trailing space" -TestCases @{ file = $file } { + ($file | Select-String "\s$" | Where-Object { $_.Line.Trim().Length -gt 0 }).LineNumber | Should -BeNullOrEmpty + } + + $tokens = $null + $parseErrors = $null + $ast = [System.Management.Automation.Language.Parser]::ParseFile($file.FullName, [ref]$tokens, [ref]$parseErrors) + + It "[$name] Should have no syntax errors" -TestCases @{ parseErrors = $parseErrors } { + $parseErrors | Should -BeNullOrEmpty + } + + foreach ($command in $global:BannedCommands) { + if ($global:MayContainCommand["$command"] -notcontains $file.Name) { + It "[$name] Should not use $command" -TestCases @{ tokens = $tokens; command = $command } { + $tokens | Where-Object Text -EQ $command | Should -BeNullOrEmpty + } + } + } + } + } + + Context "Validating help.txt help files" { + $allFiles = Get-ChildItem -Path $moduleRoot -Recurse | Where-Object Name -like "*.help.txt" | Where-Object FullName -NotLike "$moduleRoot\tests\*" + + foreach ($file in $allFiles) { + $name = $file.FullName.Replace("$moduleRoot\", '') + + It "[$name] Should have UTF8 encoding" -TestCases @{ file = $file } { + Get-FileEncoding -Path $file.FullName | Should -Be 'UTF8 BOM' + } + + It "[$name] Should have no trailing space" -TestCases @{ file = $file } { + ($file | Select-String "\s$" | Where-Object { $_.Line.Trim().Length -gt 0 } | Measure-Object).Count | Should -Be 0 + } + } + } } \ No newline at end of file diff --git a/WinEventLogCustomization/tests/general/Help.Exceptions.ps1 b/WinEventLogCustomization/tests/general/Help.Exceptions.ps1 index f9c9bd7..4ca5f1a 100644 --- a/WinEventLogCustomization/tests/general/Help.Exceptions.ps1 +++ b/WinEventLogCustomization/tests/general/Help.Exceptions.ps1 @@ -1,6 +1,5 @@ # List of functions that should be ignored $global:FunctionHelpTestExceptions = @( - ) <# @@ -10,7 +9,6 @@ $global:FunctionHelpTestExceptions = @( "Sqlcollaborative.Dbatools.Connection.ManagementConnectionType[]" #> $global:HelpTestEnumeratedArrays = @( - ) <# @@ -22,5 +20,4 @@ $global:HelpTestEnumeratedArrays = @( "Get-DbaCmObject" = @("DoNotUse") #> $global:HelpTestSkipParameterType = @{ - } diff --git a/WinEventLogCustomization/tests/general/Help.Tests.ps1 b/WinEventLogCustomization/tests/general/Help.Tests.ps1 index 1207008..3ace1b1 100644 --- a/WinEventLogCustomization/tests/general/Help.Tests.ps1 +++ b/WinEventLogCustomization/tests/general/Help.Tests.ps1 @@ -1,48 +1,48 @@ <# .NOTES The original test this is based upon was written by June Blender. - After several rounds of modifications it stands now as it is, but the honor remains hers. + After several rounds of modifications it stands now as it is, but the honor remains hers. - Thank you June, for all you have done! + Thank you June, for all you have done! .DESCRIPTION - This test evaluates the help for all commands in a module. - - .PARAMETER SkipTest - Disables this test. - - .PARAMETER CommandPath - List of paths under which the script files are stored. - This test assumes that all functions have their own file that is named after themselves. - These paths are used to search for commands that should exist and be tested. - Will search recursively and accepts wildcards, make sure only functions are found - - .PARAMETER ModuleName - Name of the module to be tested. - The module must already be imported - - .PARAMETER ExceptionsFile - File in which exceptions and adjustments are configured. - In it there should be two arrays and a hashtable defined: - $global:FunctionHelpTestExceptions - $global:HelpTestEnumeratedArrays - $global:HelpTestSkipParameterType - These can be used to tweak the tests slightly in cases of need. - See the example file for explanations on each of these usage and effect. + This test evaluates the help for all commands in a module. + + .PARAMETER SkipTest + Disables this test. + + .PARAMETER CommandPath + List of paths under which the script files are stored. + This test assumes that all functions have their own file that is named after themselves. + These paths are used to search for commands that should exist and be tested. + Will search recursively and accepts wildcards, make sure only functions are found + + .PARAMETER ModuleName + Name of the module to be tested. + The module must already be imported + + .PARAMETER ExceptionsFile + File in which exceptions and adjustments are configured. + In it there should be two arrays and a hashtable defined: + $global:FunctionHelpTestExceptions + $global:HelpTestEnumeratedArrays + $global:HelpTestSkipParameterType + These can be used to tweak the tests slightly in cases of need. + See the example file for explanations on each of these usage and effect. #> [CmdletBinding()] Param ( - [switch] - $SkipTest, - - [string[]] - $CommandPath = @("$global:testroot\..\functions", "$global:testroot\..\internal\functions"), - - [string] - $ModuleName = "WinEventLogCustomization", - - [string] - $ExceptionsFile = "$global:testroot\general\Help.Exceptions.ps1" + [switch] + $SkipTest, + + [string[]] + $CommandPath = @("$global:testroot\..\functions", "$global:testroot\..\internal\functions"), + + [string] + $ModuleName = "WinEventLogCustomization", + + [string] + $ExceptionsFile = "$global:testroot\general\Help.Exceptions.ps1" ) if ($SkipTest) { return } . $ExceptionsFile @@ -58,89 +58,87 @@ $commands = Get-Command -Module (Get-Module $ModuleName) -CommandType $commandTy foreach ($command in $commands) { $commandName = $command.Name - + # Skip all functions that are on the exclusions list if ($global:FunctionHelpTestExceptions -contains $commandName) { continue } - + # The module-qualified command fails on Microsoft.PowerShell.Archive cmdlets $Help = Get-Help $commandName -ErrorAction SilentlyContinue - - Describe "Test help for $commandName" { - - # If help is not found, synopsis in auto-generated help is the syntax diagram - It "should not be auto-generated" -TestCases @{ Help = $Help } { - $Help.Synopsis | Should -Not -BeLike '*`[``]*' - } - - # Should be a description for every function - It "gets description for $commandName" -TestCases @{ Help = $Help } { - $Help.Description | Should -Not -BeNullOrEmpty - } - - # Should be at least one example - It "gets example code from $commandName" -TestCases @{ Help = $Help } { - ($Help.Examples.Example | Select-Object -First 1).Code | Should -Not -BeNullOrEmpty - } - - # Should be at least one example description - It "gets example help from $commandName" -TestCases @{ Help = $Help } { - ($Help.Examples.Example.Remarks | Select-Object -First 1).Text | Should -Not -BeNullOrEmpty - } - + + Describe "Test help for $commandName" { + + # If help is not found, synopsis in auto-generated help is the syntax diagram + It "should not be auto-generated" -TestCases @{ Help = $Help } { + $Help.Synopsis | Should -Not -BeLike '*`[``]*' + } + + # Should be a description for every function + It "gets description for $commandName" -TestCases @{ Help = $Help } { + $Help.Description | Should -Not -BeNullOrEmpty + } + + # Should be at least one example + It "gets example code from $commandName" -TestCases @{ Help = $Help } { + ($Help.Examples.Example | Select-Object -First 1).Code | Should -Not -BeNullOrEmpty + } + + # Should be at least one example description + It "gets example help from $commandName" -TestCases @{ Help = $Help } { + ($Help.Examples.Example.Remarks | Select-Object -First 1).Text | Should -Not -BeNullOrEmpty + } + Context "Test parameter help for $commandName" { - + $common = 'Debug', 'ErrorAction', 'ErrorVariable', 'InformationAction', 'InformationVariable', 'OutBuffer', 'OutVariable', 'PipelineVariable', 'Verbose', 'WarningAction', 'WarningVariable' - + $parameters = $command.ParameterSets.Parameters | Sort-Object -Property Name -Unique | Where-Object Name -notin $common $parameterNames = $parameters.Name $HelpParameterNames = $Help.Parameters.Parameter.Name | Sort-Object -Unique foreach ($parameter in $parameters) { $parameterName = $parameter.Name $parameterHelp = $Help.parameters.parameter | Where-Object Name -EQ $parameterName - - # Should be a description for every parameter - It "gets help for parameter: $parameterName : in $commandName" -TestCases @{ parameterHelp = $parameterHelp } { - $parameterHelp.Description.Text | Should -Not -BeNullOrEmpty - } - + + # Should be a description for every parameter + It "gets help for parameter: $parameterName : in $commandName" -TestCases @{ parameterHelp = $parameterHelp } { + $parameterHelp.Description.Text | Should -Not -BeNullOrEmpty + } + $codeMandatory = $parameter.IsMandatory.toString() - It "help for $parameterName parameter in $commandName has correct Mandatory value" -TestCases @{ parameterHelp = $parameterHelp; codeMandatory = $codeMandatory } { - $parameterHelp.Required | Should -Be $codeMandatory - } - + It "help for $parameterName parameter in $commandName has correct Mandatory value" -TestCases @{ parameterHelp = $parameterHelp; codeMandatory = $codeMandatory } { + $parameterHelp.Required | Should -Be $codeMandatory + } + if ($HelpTestSkipParameterType[$commandName] -contains $parameterName) { continue } - + $codeType = $parameter.ParameterType.Name - + if ($parameter.ParameterType.IsEnum) { # Enumerations often have issues with the typename not being reliably available $names = $parameter.ParameterType::GetNames($parameter.ParameterType) - # Parameter type in Help should match code - It "help for $commandName has correct parameter type for $parameterName" -TestCases @{ parameterHelp = $parameterHelp; names = $names } { - $parameterHelp.parameterValueGroup.parameterValue | Should -be $names - } - } - elseif ($parameter.ParameterType.FullName -in $HelpTestEnumeratedArrays) { + # Parameter type in Help should match code + It "help for $commandName has correct parameter type for $parameterName" -TestCases @{ parameterHelp = $parameterHelp; names = $names } { + $parameterHelp.parameterValueGroup.parameterValue | Should -be $names + } + } elseif ($parameter.ParameterType.FullName -in $HelpTestEnumeratedArrays) { # Enumerations often have issues with the typename not being reliably available $names = [Enum]::GetNames($parameter.ParameterType.DeclaredMembers[0].ReturnType) - It "help for $commandName has correct parameter type for $parameterName" -TestCases @{ parameterHelp = $parameterHelp; names = $names } { - $parameterHelp.parameterValueGroup.parameterValue | Should -be $names - } - } - else { + It "help for $commandName has correct parameter type for $parameterName" -TestCases @{ parameterHelp = $parameterHelp; names = $names } { + $parameterHelp.parameterValueGroup.parameterValue | Should -be $names + } + } else { # To avoid calling Trim method on a null object. $helpType = if ($parameterHelp.parameterValue) { $parameterHelp.parameterValue.Trim() } - # Parameter type in Help should match code - It "help for $commandName has correct parameter type for $parameterName" -TestCases @{ helpType = $helpType; codeType = $codeType } { - $helpType | Should -be $codeType - } + # Parameter type in Help should match code + It "help for $commandName has correct parameter type for $parameterName" -TestCases @{ helpType = $helpType; codeType = $codeType } { + $helpType | Should -be $codeType + } } } foreach ($helpParm in $HelpParameterNames) { - # Shouldn't find extra parameters in help. - It "finds help parameter in code: $helpParm" -TestCases @{ helpParm = $helpParm; parameterNames = $parameterNames } { - $helpParm -in $parameterNames | Should -Be $true - } + # Shouldn't find extra parameters in help. + It "finds help parameter in code: $helpParm" -TestCases @{ helpParm = $helpParm; parameterNames = $parameterNames } { + $helpParm -in $parameterNames | Should -Be $true + } } } } diff --git a/WinEventLogCustomization/tests/general/Manifest.Tests.ps1 b/WinEventLogCustomization/tests/general/Manifest.Tests.ps1 index 16d9bab..431e1ed 100644 --- a/WinEventLogCustomization/tests/general/Manifest.Tests.ps1 +++ b/WinEventLogCustomization/tests/general/Manifest.Tests.ps1 @@ -1,62 +1,57 @@ Describe "Validating the module manifest" { - $moduleRoot = (Resolve-Path "$global:testroot\..").Path - $manifest = ((Get-Content "$moduleRoot\WinEventLogCustomization.psd1") -join "`n") | Invoke-Expression - Context "Basic resources validation" { - $files = Get-ChildItem "$moduleRoot\functions" -Recurse -File | Where-Object Name -like "*.ps1" - It "Exports all functions in the public folder" -TestCases @{ files = $files; manifest = $manifest } { - - $functions = (Compare-Object -ReferenceObject $files.BaseName -DifferenceObject $manifest.FunctionsToExport | Where-Object SideIndicator -Like '<=').InputObject - $functions | Should -BeNullOrEmpty - } - It "Exports no function that isn't also present in the public folder" -TestCases @{ files = $files; manifest = $manifest } { - $functions = (Compare-Object -ReferenceObject $files.BaseName -DifferenceObject $manifest.FunctionsToExport | Where-Object SideIndicator -Like '=>').InputObject - $functions | Should -BeNullOrEmpty - } - - It "Exports none of its internal functions" -TestCases @{ moduleRoot = $moduleRoot; manifest = $manifest } { - $files = Get-ChildItem "$moduleRoot\internal\functions" -Recurse -File -Filter "*.ps1" - $files | Where-Object BaseName -In $manifest.FunctionsToExport | Should -BeNullOrEmpty - } - } - - Context "Individual file validation" { - It "The root module file exists" -TestCases @{ moduleRoot = $moduleRoot; manifest = $manifest } { - Test-Path "$moduleRoot\$($manifest.RootModule)" | Should -Be $true - } - - foreach ($format in $manifest.FormatsToProcess) - { - It "The file $format should exist" -TestCases @{ moduleRoot = $moduleRoot; format = $format } { - Test-Path "$moduleRoot\$format" | Should -Be $true - } - } - - foreach ($type in $manifest.TypesToProcess) - { - It "The file $type should exist" -TestCases @{ moduleRoot = $moduleRoot; type = $type } { - Test-Path "$moduleRoot\$type" | Should -Be $true - } - } - - foreach ($assembly in $manifest.RequiredAssemblies) - { + $moduleRoot = (Resolve-Path "$global:testroot\..").Path + $manifest = ((Get-Content "$moduleRoot\WinEventLogCustomization.psd1") -join "`n") | Invoke-Expression + Context "Basic resources validation" { + $files = Get-ChildItem "$moduleRoot\functions" -Recurse -File | Where-Object Name -like "*.ps1" + It "Exports all functions in the public folder" -TestCases @{ files = $files; manifest = $manifest } { + + $functions = (Compare-Object -ReferenceObject $files.BaseName -DifferenceObject $manifest.FunctionsToExport | Where-Object SideIndicator -Like '<=').InputObject + $functions | Should -BeNullOrEmpty + } + It "Exports no function that isn't also present in the public folder" -TestCases @{ files = $files; manifest = $manifest } { + $functions = (Compare-Object -ReferenceObject $files.BaseName -DifferenceObject $manifest.FunctionsToExport | Where-Object SideIndicator -Like '=>').InputObject + $functions | Should -BeNullOrEmpty + } + + It "Exports none of its internal functions" -TestCases @{ moduleRoot = $moduleRoot; manifest = $manifest } { + $files = Get-ChildItem "$moduleRoot\internal\functions" -Recurse -File -Filter "*.ps1" + $files | Where-Object BaseName -In $manifest.FunctionsToExport | Should -BeNullOrEmpty + } + } + + Context "Individual file validation" { + It "The root module file exists" -TestCases @{ moduleRoot = $moduleRoot; manifest = $manifest } { + Test-Path "$moduleRoot\$($manifest.RootModule)" | Should -Be $true + } + + foreach ($format in $manifest.FormatsToProcess) { + It "The file $format should exist" -TestCases @{ moduleRoot = $moduleRoot; format = $format } { + Test-Path "$moduleRoot\$format" | Should -Be $true + } + } + + foreach ($type in $manifest.TypesToProcess) { + It "The file $type should exist" -TestCases @{ moduleRoot = $moduleRoot; type = $type } { + Test-Path "$moduleRoot\$type" | Should -Be $true + } + } + + foreach ($assembly in $manifest.RequiredAssemblies) { if ($assembly -like "*.dll") { It "The file $assembly should exist" -TestCases @{ moduleRoot = $moduleRoot; assembly = $assembly } { Test-Path "$moduleRoot\$assembly" | Should -Be $true } - } - else { + } else { It "The file $assembly should load from the GAC" -TestCases @{ moduleRoot = $moduleRoot; assembly = $assembly } { { Add-Type -AssemblyName $assembly } | Should -Not -Throw } } } - - foreach ($tag in $manifest.PrivateData.PSData.Tags) - { - It "Tags should have no spaces in name" -TestCases @{ tag = $tag } { - $tag -match " " | Should -Be $false - } - } - } + + foreach ($tag in $manifest.PrivateData.PSData.Tags) { + It "Tags should have no spaces in name" -TestCases @{ tag = $tag } { + $tag -match " " | Should -Be $false + } + } + } } \ No newline at end of file diff --git a/WinEventLogCustomization/tests/general/PSScriptAnalyzer.Tests.ps1 b/WinEventLogCustomization/tests/general/PSScriptAnalyzer.Tests.ps1 index a99b60d..128a126 100644 --- a/WinEventLogCustomization/tests/general/PSScriptAnalyzer.Tests.ps1 +++ b/WinEventLogCustomization/tests/general/PSScriptAnalyzer.Tests.ps1 @@ -1,10 +1,10 @@ [CmdletBinding()] Param ( - [switch] - $SkipTest, - - [string[]] - $CommandPath = @("$global:testroot\..\functions", "$global:testroot\..\internal\functions") + [switch] + $SkipTest, + + [string[]] + $CommandPath = @("$global:testroot\..\functions", "$global:testroot\..\internal\functions") ) if ($SkipTest) { return } @@ -12,31 +12,26 @@ if ($SkipTest) { return } $global:__pester_data.ScriptAnalyzer = New-Object System.Collections.ArrayList Describe 'Invoking PSScriptAnalyzer against commandbase' { - $commandFiles = foreach ($path in $CommandPath) { + $commandFiles = foreach ($path in $CommandPath) { Get-ChildItem -Path $path -Recurse | Where-Object Name -like "*.ps1" } - $scriptAnalyzerRules = Get-ScriptAnalyzerRule - - foreach ($file in $commandFiles) - { - Context "Analyzing $($file.BaseName)" { - $analysis = Invoke-ScriptAnalyzer -Path $file.FullName -ExcludeRule PSAvoidTrailingWhitespace, PSShouldProcess - - forEach ($rule in $scriptAnalyzerRules) - { - It "Should pass $rule" -TestCases @{ analysis = $analysis; rule = $rule } { - If ($analysis.RuleName -contains $rule) - { - $analysis | Where-Object RuleName -EQ $rule -outvariable failures | ForEach-Object { $null = $global:__pester_data.ScriptAnalyzer.Add($_) } - - 1 | Should -Be 0 - } - else - { - 0 | Should -Be 0 - } - } - } - } - } + $scriptAnalyzerRules = Get-ScriptAnalyzerRule + + foreach ($file in $commandFiles) { + Context "Analyzing $($file.BaseName)" { + $analysis = Invoke-ScriptAnalyzer -Path $file.FullName -ExcludeRule PSAvoidTrailingWhitespace, PSShouldProcess + + forEach ($rule in $scriptAnalyzerRules) { + It "Should pass $rule" -TestCases @{ analysis = $analysis; rule = $rule } { + If ($analysis.RuleName -contains $rule) { + $analysis | Where-Object RuleName -EQ $rule -outvariable failures | ForEach-Object { $null = $global:__pester_data.ScriptAnalyzer.Add($_) } + + 1 | Should -Be 0 + } else { + 0 | Should -Be 0 + } + } + } + } + } } \ No newline at end of file diff --git a/WinEventLogCustomization/tests/general/strings.Exceptions.ps1 b/WinEventLogCustomization/tests/general/strings.Exceptions.ps1 index b4c91f2..5daf839 100644 --- a/WinEventLogCustomization/tests/general/strings.Exceptions.ps1 +++ b/WinEventLogCustomization/tests/general/strings.Exceptions.ps1 @@ -20,17 +20,17 @@ A list of entries that MAY be used without needing to have text defined. This is intended for modules (re-)using strings provided by another module #> $exceptions['NoTextNeeded'] = @( - 'Validate.FSPath' - 'Validate.FSPath.File' - 'Validate.FSPath.FileOrParent' - 'Validate.FSPath.Folder' - 'Validate.Path' - 'Validate.Path.Container' - 'Validate.Path.Leaf' - 'Validate.TimeSpan.Positive' - 'Validate.Uri.Absolute' - 'Validate.Uri.Absolute.File' - 'Validate.Uri.Absolute.Https' + 'Validate.FSPath' + 'Validate.FSPath.File' + 'Validate.FSPath.FileOrParent' + 'Validate.FSPath.Folder' + 'Validate.Path' + 'Validate.Path.Container' + 'Validate.Path.Leaf' + 'Validate.TimeSpan.Positive' + 'Validate.Uri.Absolute' + 'Validate.Uri.Absolute.File' + 'Validate.Uri.Absolute.Https' ) $exceptions \ No newline at end of file diff --git a/WinEventLogCustomization/tests/general/strings.Tests.ps1 b/WinEventLogCustomization/tests/general/strings.Tests.ps1 index 0bebede..1af6218 100644 --- a/WinEventLogCustomization/tests/general/strings.Tests.ps1 +++ b/WinEventLogCustomization/tests/general/strings.Tests.ps1 @@ -6,22 +6,20 @@ It also checks, whether the language files have orphaned entries that need cleaning up. #> - - Describe "Testing localization strings" { - $moduleRoot = (Get-Module WinEventLogCustomization).ModuleBase - $stringsResults = Export-PSMDString -ModuleRoot $moduleRoot - $exceptions = & "$global:testroot\general\strings.Exceptions.ps1" - - foreach ($stringEntry in $stringsResults) { + $moduleRoot = (Get-Module WinEventLogCustomization).ModuleBase + $stringsResults = Export-PSMDString -ModuleRoot $moduleRoot + $exceptions = & "$global:testroot\general\strings.Exceptions.ps1" + + foreach ($stringEntry in $stringsResults) { if ($stringEntry.String -eq "key") { continue } # Skipping the template default entry - It "Should be used & have text: $($stringEntry.String)" -TestCases @{ stringEntry = $stringEntry; exceptions = $exceptions } { + It "Should be used & have text: $($stringEntry.String)" -TestCases @{ stringEntry = $stringEntry; exceptions = $exceptions } { if ($exceptions.LegalSurplus -notcontains $stringEntry.String) { $stringEntry.Surplus | Should -BeFalse - } - if ($exceptions.NoTextNeeded -notcontains $stringEntry.String) { - $stringEntry.Text | Should -Not -BeNullOrEmpty - } + } + if ($exceptions.NoTextNeeded -notcontains $stringEntry.String) { + $stringEntry.Text | Should -Not -BeNullOrEmpty + } } } } \ No newline at end of file diff --git a/WinEventLogCustomization/tests/pester.ps1 b/WinEventLogCustomization/tests/pester.ps1 index 8b6d3d4..303a6ac 100644 --- a/WinEventLogCustomization/tests/pester.ps1 +++ b/WinEventLogCustomization/tests/pester.ps1 @@ -1,15 +1,15 @@ param ( - $TestGeneral = $true, - - $TestFunctions = $true, - - [ValidateSet('None', 'Normal', 'Detailed', 'Diagnostic')] - [Alias('Show')] - $Output = "None", - - $Include = "*", - - $Exclude = "" + $TestGeneral = $true, + + $TestFunctions = $true, + + [ValidateSet('None', 'Normal', 'Detailed', 'Diagnostic')] + [Alias('Show')] + $Output = "None", + + $Include = "*", + + $Exclude = "" ) Write-PSFMessage -Level Important -Message "Starting Tests" @@ -20,14 +20,14 @@ $global:testroot = $PSScriptRoot $global:__pester_data = @{ } Remove-Module WinEventLogCustomization -ErrorAction Ignore -Import-Module "$PSScriptRoot\..\WinEventLogCustomization.psd1" -Import-Module "$PSScriptRoot\..\WinEventLogCustomization.psm1" -Force +Import-Module "$testroot\..\WinEventLogCustomization.psd1" +Import-Module "$testroot\..\WinEventLogCustomization.psm1" -Force # Need to import explicitly so we can use the configuration class Import-Module Pester Write-PSFMessage -Level Important -Message "Creating test result folder" -$null = New-Item -Path "$PSScriptRoot\..\.." -Name TestResults -ItemType Directory -Force +$null = New-Item -Path "$testroot\..\.." -Name TestResults -ItemType Directory -Force $totalFailed = 0 $totalRun = 0 @@ -37,68 +37,62 @@ $config = [PesterConfiguration]::Default $config.TestResult.Enabled = $true #region Run General Tests -if ($TestGeneral) -{ - Write-PSFMessage -Level Important -Message "Modules imported, proceeding with general tests" - foreach ($file in (Get-ChildItem "$PSScriptRoot\general" | Where-Object Name -like "*.Tests.ps1")) - { - if ($file.Name -notlike $Include) { continue } - if ($file.Name -like $Exclude) { continue } - - Write-PSFMessage -Level Significant -Message " Executing $($file.Name)" - $config.TestResult.OutputPath = Join-Path "$PSScriptRoot\..\..\TestResults" "TEST-$($file.BaseName).xml" - $config.Run.Path = $file.FullName - $config.Run.PassThru = $true - $config.Output.Verbosity = $Output - $results = Invoke-Pester -Configuration $config - foreach ($result in $results) - { - $totalRun += $result.TotalCount - $totalFailed += $result.FailedCount - $result.Tests | Where-Object Result -ne 'Passed' | ForEach-Object { - $testresults += [pscustomobject]@{ - Block = $_.Block - Name = "It $($_.Name)" - Result = $_.Result - Message = $_.ErrorRecord.DisplayErrorMessage - } - } - } - } +if ($TestGeneral) { + Write-PSFMessage -Level Important -Message "Modules imported, proceeding with general tests" + foreach ($file in (Get-ChildItem "$testroot\general" | Where-Object Name -like "*.Tests.ps1")) { + if ($file.Name -notlike $Include) { continue } + if ($file.Name -like $Exclude) { continue } + + Write-PSFMessage -Level Significant -Message " Executing $($file.Name)" + $config.TestResult.OutputPath = Join-Path "$testroot\..\..\TestResults" "TEST-$($file.BaseName).xml" + $config.Run.Path = $file.FullName + $config.Run.PassThru = $true + $config.Output.Verbosity = $Output + $results = Invoke-Pester -Configuration $config + foreach ($result in $results) { + $totalRun += $result.TotalCount + $totalFailed += $result.FailedCount + $result.Tests | Where-Object Result -ne 'Passed' | ForEach-Object { + $testresults += [pscustomobject]@{ + Block = $_.Block + Name = "It $($_.Name)" + Result = $_.Result + Message = $_.ErrorRecord.DisplayErrorMessage + } + } + } + } } #endregion Run General Tests $global:__pester_data.ScriptAnalyzer | Out-Host #region Test Commands -if ($TestFunctions) -{ - Write-PSFMessage -Level Important -Message "Proceeding with individual tests" - foreach ($file in (Get-ChildItem "$PSScriptRoot\functions" -Recurse -File | Where-Object Name -like "*Tests.ps1")) - { - if ($file.Name -notlike $Include) { continue } - if ($file.Name -like $Exclude) { continue } - - Write-PSFMessage -Level Significant -Message " Executing $($file.Name)" - $config.TestResult.OutputPath = Join-Path "$PSScriptRoot\..\..\TestResults" "TEST-$($file.BaseName).xml" - $config.Run.Path = $file.FullName - $config.Run.PassThru = $true - $config.Output.Verbosity = $Output - $results = Invoke-Pester -Configuration $config - foreach ($result in $results) - { - $totalRun += $result.TotalCount - $totalFailed += $result.FailedCount - $result.Tests | Where-Object Result -ne 'Passed' | ForEach-Object { - $testresults += [pscustomobject]@{ - Block = $_.Block - Name = "It $($_.Name)" - Result = $_.Result - Message = $_.ErrorRecord.DisplayErrorMessage - } - } - } - } +if ($TestFunctions) { + Write-PSFMessage -Level Important -Message "Proceeding with individual tests" + foreach ($file in (Get-ChildItem "$testroot\functions" -Recurse -File | Where-Object Name -like "*Tests.ps1")) { + if ($file.Name -notlike $Include) { continue } + if ($file.Name -like $Exclude) { continue } + + Write-PSFMessage -Level Significant -Message " Executing $($file.Name)" + $config.TestResult.OutputPath = Join-Path "$testroot\..\..\TestResults" "TEST-$($file.BaseName).xml" + $config.Run.Path = $file.FullName + $config.Run.PassThru = $true + $config.Output.Verbosity = $Output + $results = Invoke-Pester -Configuration $config + foreach ($result in $results) { + $totalRun += $result.TotalCount + $totalFailed += $result.FailedCount + $result.Tests | Where-Object Result -ne 'Passed' | ForEach-Object { + $testresults += [pscustomobject]@{ + Block = $_.Block + Name = "It $($_.Name)" + Result = $_.Result + Message = $_.ErrorRecord.DisplayErrorMessage + } + } + } + } } #endregion Test Commands @@ -107,7 +101,6 @@ $testresults | Sort-Object Describe, Context, Name, Result, Message | Format-Lis if ($totalFailed -eq 0) { Write-PSFMessage -Level Critical -Message "All $totalRun tests executed without a single failure!" } else { Write-PSFMessage -Level Critical -Message "$totalFailed tests out of $totalRun tests failed!" } -if ($totalFailed -gt 0) -{ - throw "$totalFailed / $totalRun tests failed!" +if ($totalFailed -gt 0) { + throw "$totalFailed / $totalRun tests failed!" } \ No newline at end of file diff --git a/assets/WinEventLogCustomization.xltx b/assets/WinEventLogCustomization.xltx index e414691..3ba735e 100644 Binary files a/assets/WinEventLogCustomization.xltx and b/assets/WinEventLogCustomization.xltx differ diff --git a/assets/WinEventLogCustomization_128x128.png b/assets/WinEventLogCustomization_128x128.png new file mode 100644 index 0000000..63464f5 Binary files /dev/null and b/assets/WinEventLogCustomization_128x128.png differ diff --git a/assets/WinEventLogCustomization_256x256.png b/assets/WinEventLogCustomization_256x256.png new file mode 100644 index 0000000..dda792b Binary files /dev/null and b/assets/WinEventLogCustomization_256x256.png differ diff --git a/assets/WinEventLogCustomization_32x32.png b/assets/WinEventLogCustomization_32x32.png new file mode 100644 index 0000000..0422556 Binary files /dev/null and b/assets/WinEventLogCustomization_32x32.png differ diff --git a/assets/WinEventLogCustomization_500x500.png b/assets/WinEventLogCustomization_500x500.png new file mode 100644 index 0000000..4898a00 Binary files /dev/null and b/assets/WinEventLogCustomization_500x500.png differ diff --git a/assets/WinEventLogCustomization_64x64.png b/assets/WinEventLogCustomization_64x64.png new file mode 100644 index 0000000..77f24ce Binary files /dev/null and b/assets/WinEventLogCustomization_64x64.png differ diff --git a/assets/pictures/EventChannel.png b/assets/pictures/EventChannel.png new file mode 100644 index 0000000..77de25c Binary files /dev/null and b/assets/pictures/EventChannel.png differ diff --git a/build/AzureFunction.readme.md b/build/AzureFunction.readme.md deleted file mode 100644 index 434e135..0000000 --- a/build/AzureFunction.readme.md +++ /dev/null @@ -1,35 +0,0 @@ -# Setting up the release pipeline: - -## Preliminary - -Setting up a release pipeline, set the trigger to do continuous integration against the master branch only. -In Stage 1 set up a tasksequence: - -## 1) PowerShell Task: Prerequisites - -Have it execute `vsts-prerequisites.ps1` - -## 2) PowerShell Task: Validate - -Have it execute `vsts-prerequisites.ps1` - -## 3) PowerShell Task: Build - -Have it execute `vsts-build.ps1`. -The task requires two parameters: - - - `-LocalRepo` - - `-WorkingDirectory $(System.DefaultWorkingDirectory)/_�name�` - -## 4) Publish Test Results - -Configure task to pick up nunit type of tests (rather than the default junit). -Configure task to execute, even if previous steps failed or the task sequence was cancelled. - -## 5) PowerShell Task: Package Function - -Have it execute `vsts-packageFunction.ps1` - -## 6) Azure Function AppDeploy - -Configure to publish to the correct function app. \ No newline at end of file diff --git a/build/azure_pipeline-validate_and_Build.yml b/build/azure_pipeline-validate_and_Build.yml index b561171..4e9cd0d 100644 --- a/build/azure_pipeline-validate_and_Build.yml +++ b/build/azure_pipeline-validate_and_Build.yml @@ -1,5 +1,5 @@ pool: - name: Hosted VS2017 + vmImage: 'windows-latest' # Continuous integration only on branch master trigger: diff --git a/build/vsts-build.ps1 b/build/vsts-build.ps1 index 7b3acf4..4df10e9 100644 --- a/build/vsts-build.ps1 +++ b/build/vsts-build.ps1 @@ -5,30 +5,27 @@ It expects as input an ApiKey authorized to publish the module. Insert any build steps you may need to take before publishing it here. #> param ( - $ApiKey, - - $WorkingDirectory, - - $Repository = 'PSGallery', - - [switch] - $LocalRepo, - - [switch] - $SkipPublish, - - [switch] - $AutoVersion + $ApiKey, + + $WorkingDirectory, + + $Repository = 'PSGallery', + + [switch] + $LocalRepo, + + [switch] + $SkipPublish, + + [switch] + $AutoVersion ) #region Handle Working Directory Defaults -if (-not $WorkingDirectory) -{ - if ($env:RELEASE_PRIMARYARTIFACTSOURCEALIAS) - { - $WorkingDirectory = Join-Path -Path $env:SYSTEM_DEFAULTWORKINGDIRECTORY -ChildPath $env:RELEASE_PRIMARYARTIFACTSOURCEALIAS - } - else { $WorkingDirectory = $env:SYSTEM_DEFAULTWORKINGDIRECTORY } +if (-not $WorkingDirectory) { + if ($env:RELEASE_PRIMARYARTIFACTSOURCEALIAS) { + $WorkingDirectory = Join-Path -Path $env:SYSTEM_DEFAULTWORKINGDIRECTORY -ChildPath $env:RELEASE_PRIMARYARTIFACTSOURCEALIAS + } else { $WorkingDirectory = $env:SYSTEM_DEFAULTWORKINGDIRECTORY } } if (-not $WorkingDirectory) { $WorkingDirectory = Split-Path $PSScriptRoot } #endregion Handle Working Directory Defaults @@ -43,35 +40,33 @@ $text = @() $processed = @() # Gather Stuff to run before -foreach ($filePath in (& "$($PSScriptRoot)\..\WinEventLogCustomization\internal\scripts\preimport.ps1")) -{ - if ([string]::IsNullOrWhiteSpace($filePath)) { continue } - - $item = Get-Item $filePath - if ($item.PSIsContainer) { continue } - if ($item.FullName -in $processed) { continue } - $text += [System.IO.File]::ReadAllText($item.FullName) - $processed += $item.FullName +foreach ($filePath in (& "$($PSScriptRoot)\..\WinEventLogCustomization\internal\scripts\preimport.ps1")) { + if ([string]::IsNullOrWhiteSpace($filePath)) { continue } + + $item = Get-Item $filePath + if ($item.PSIsContainer) { continue } + if ($item.FullName -in $processed) { continue } + $text += [System.IO.File]::ReadAllText($item.FullName) + $processed += $item.FullName } # Gather commands Get-ChildItem -Path "$($publishDir.FullName)\WinEventLogCustomization\internal\functions\" -Recurse -File -Filter "*.ps1" | ForEach-Object { - $text += [System.IO.File]::ReadAllText($_.FullName) + $text += [System.IO.File]::ReadAllText($_.FullName) } Get-ChildItem -Path "$($publishDir.FullName)\WinEventLogCustomization\functions\" -Recurse -File -Filter "*.ps1" | ForEach-Object { - $text += [System.IO.File]::ReadAllText($_.FullName) + $text += [System.IO.File]::ReadAllText($_.FullName) } # Gather stuff to run afterwards -foreach ($filePath in (& "$($PSScriptRoot)\..\WinEventLogCustomization\internal\scripts\postimport.ps1")) -{ - if ([string]::IsNullOrWhiteSpace($filePath)) { continue } - - $item = Get-Item $filePath - if ($item.PSIsContainer) { continue } - if ($item.FullName -in $processed) { continue } - $text += [System.IO.File]::ReadAllText($item.FullName) - $processed += $item.FullName +foreach ($filePath in (& "$($PSScriptRoot)\..\WinEventLogCustomization\internal\scripts\postimport.ps1")) { + if ([string]::IsNullOrWhiteSpace($filePath)) { continue } + + $item = Get-Item $filePath + if ($item.PSIsContainer) { continue } + if ($item.FullName -in $processed) { continue } + $text += [System.IO.File]::ReadAllText($item.FullName) + $processed += $item.FullName } #endregion Gather text data to compile @@ -83,38 +78,32 @@ $fileData = $fileData.Replace('""', ($text -join "`n`n") #endregion Update the psm1 file #region Updating the Module Version -if ($AutoVersion) -{ - Write-PSFMessage -Level Important -Message "Updating module version numbers." - try { [version]$remoteVersion = (Find-Module 'WinEventLogCustomization' -Repository $Repository -ErrorAction Stop).Version } - catch - { - Stop-PSFFunction -Message "Failed to access $($Repository)" -EnableException $true -ErrorRecord $_ - } - if (-not $remoteVersion) - { - Stop-PSFFunction -Message "Couldn't find WinEventLogCustomization on repository $($Repository)" -EnableException $true - } - $newBuildNumber = $remoteVersion.Build + 1 - [version]$localVersion = (Import-PowerShellDataFile -Path "$($publishDir.FullName)\WinEventLogCustomization\WinEventLogCustomization.psd1").ModuleVersion - Update-ModuleManifest -Path "$($publishDir.FullName)\WinEventLogCustomization\WinEventLogCustomization.psd1" -ModuleVersion "$($localVersion.Major).$($localVersion.Minor).$($newBuildNumber)" +if ($AutoVersion) { + Write-PSFMessage -Level Important -Message "Updating module version numbers." + try { [version]$remoteVersion = (Find-Module 'WinEventLogCustomization' -Repository $Repository -ErrorAction Stop).Version } + catch { + Stop-PSFFunction -Message "Failed to access $($Repository)" -EnableException $true -ErrorRecord $_ + } + if (-not $remoteVersion) { + Stop-PSFFunction -Message "Couldn't find WinEventLogCustomization on repository $($Repository)" -EnableException $true + } + $newBuildNumber = $remoteVersion.Build + 1 + [version]$localVersion = (Import-PowerShellDataFile -Path "$($publishDir.FullName)\WinEventLogCustomization\WinEventLogCustomization.psd1").ModuleVersion + Update-ModuleManifest -Path "$($publishDir.FullName)\WinEventLogCustomization\WinEventLogCustomization.psd1" -ModuleVersion "$($localVersion.Major).$($localVersion.Minor).$($newBuildNumber)" } #endregion Updating the Module Version #region Publish if ($SkipPublish) { return } -if ($LocalRepo) -{ - # Dependencies must go first - Write-PSFMessage -Level Important -Message "Creating Nuget Package for module: PSFramework" - New-PSMDModuleNugetPackage -ModulePath (Get-Module -Name PSFramework).ModuleBase -PackagePath . - Write-PSFMessage -Level Important -Message "Creating Nuget Package for module: WinEventLogCustomization" - New-PSMDModuleNugetPackage -ModulePath "$($publishDir.FullName)\WinEventLogCustomization" -PackagePath . -} -else -{ - # Publish to Gallery - Write-PSFMessage -Level Important -Message "Publishing the WinEventLogCustomization module to $($Repository)" - Publish-Module -Path "$($publishDir.FullName)\WinEventLogCustomization" -NuGetApiKey $ApiKey -Force -Repository $Repository +if ($LocalRepo) { + # Dependencies must go first + Write-PSFMessage -Level Important -Message "Creating Nuget Package for module: PSFramework" + New-PSMDModuleNugetPackage -ModulePath (Get-Module -Name PSFramework).ModuleBase -PackagePath . + Write-PSFMessage -Level Important -Message "Creating Nuget Package for module: WinEventLogCustomization" + New-PSMDModuleNugetPackage -ModulePath "$($publishDir.FullName)\WinEventLogCustomization" -PackagePath . +} else { + # Publish to Gallery + Write-PSFMessage -Level Important -Message "Publishing the WinEventLogCustomization module to $($Repository)" + Publish-Module -Path "$($publishDir.FullName)\WinEventLogCustomization" -NuGetApiKey $ApiKey -Force -Repository $Repository } #endregion Publish \ No newline at end of file diff --git a/build/vsts-createFunctionClientModule.ps1 b/build/vsts-createFunctionClientModule.ps1 deleted file mode 100644 index 4263c67..0000000 --- a/build/vsts-createFunctionClientModule.ps1 +++ /dev/null @@ -1,201 +0,0 @@ - -<# - .SYNOPSIS - Build script that generates a client module for REST API endpoints of a Azure PowerShell Functions project. - - .DESCRIPTION - Build script that generates a client module for REST API endpoints of a Azure PowerShell Functions project. - - .PARAMETER ApiKey - The API key to use to publish the module to a Nuget Repository - - .PARAMETER WorkingDirectory - The root folder from which to build the module. - - .PARAMETER Repository - The name of the repository to publish to. - Defaults to PSGallery. - - .PARAMETER LocalRepo - Instead of publishing to a gallery, drop a nuget package in the root folder. - This package can then be picked up in a later step for publishing to Azure Artifacts. - - .PARAMETER ModuleName - The name to give to the client module. - By default, the client module will be named '.Client'. - - .PARAMETER IncludeFormat - Include the format xml of the source module for the client module. - - .PARAMETER IncludeType - Include the type extension xml of the source module for the client module. - - .PARAMETER IncludeAssembly - Include the binaries of the source module for the client module. -#> -param ( - $ApiKey, - - $WorkingDirectory, - - $Repository = 'PSGallery', - - [switch] - $LocalRepo, - - $ModuleName, - - [switch] - $IncludeFormat, - - [switch] - $IncludeType, - - [switch] - $IncludeAssembly -) - -#region Handle Working Directory Defaults -if (-not $WorkingDirectory) -{ - if ($env:RELEASE_PRIMARYARTIFACTSOURCEALIAS) - { - $WorkingDirectory = Join-Path -Path $env:SYSTEM_DEFAULTWORKINGDIRECTORY -ChildPath $env:RELEASE_PRIMARYARTIFACTSOURCEALIAS - } - else { $WorkingDirectory = $env:SYSTEM_DEFAULTWORKINGDIRECTORY } -} -#endregion Handle Working Directory Defaults - -Write-PSFMessage -Level Host -Message 'Starting Build: Client Module' -$parentModule = 'WinEventLogCustomization' -if (-not $ModuleName) { $ModuleName = 'WinEventLogCustomization.Client' } -Write-PSFMessage -Level Host -Message 'Creating Folder Structure' -$workingRoot = New-Item -Path $WorkingDirectory -Name $ModuleName -ItemType Directory -$publishRoot = Join-Path -Path $WorkingDirectory -ChildPath 'publish\WinEventLogCustomization' -Copy-Item -Path "$($WorkingDirectory)\azFunctionResources\clientModule\functions" -Destination "$($workingRoot.FullName)\" -Recurse -Copy-Item -Path "$($WorkingDirectory)\azFunctionResources\clientModule\internal" -Destination "$($workingRoot.FullName)\" -Recurse -Copy-Item -Path "$($publishRoot)\en-us" -Destination "$($workingRoot.FullName)\" -Recurse -$functionFolder = Get-Item -Path "$($workingRoot.FullName)\functions" - -#region Create Functions -$encoding = [PSFEncoding]'utf8' -$functionsText = Get-Content -Path "$($WorkingDirectory)\azFunctionResources\clientModule\function.ps1" -Raw - -Write-PSFMessage -Level Host -Message 'Creating Functions' -foreach ($functionSourceFile in (Get-ChildItem -Path "$($publishRoot)\functions" -Recurse -Filter '*.ps1')) -{ - Write-PSFMessage -Level Host -Message " Processing function: $($functionSourceFile.BaseName)" - $condensedName = $functionSourceFile.BaseName -replace '-', '' - - #region Load Overrides - $override = @{ } - if (Test-Path -Path "$($WorkingDirectory)\azFunctionResources\functionOverride\$($functionSourceFile.BaseName).psd1") - { - $override = Import-PowerShellDataFile -Path "$($WorkingDirectory)\azFunctionResources\functionOverride\$($functionSourceFile.BaseName).psd1" - } - if (Test-Path -Path "$($WorkingDirectory)\azFunctionResources\functionOverride\$($condensedName).psd1") - { - $override = Import-PowerShellDataFile -Path "$($WorkingDirectory)\azFunctionResources\functionOverride\$($condensedName).psd1" - } - if ($override.NoClientFunction) - { - Write-PSFMessage -Level Host -Message " Override 'NoClientFunction' detected, skipping!" - continue - } - - # If there is an definition override, use it and continue - if (Test-Path -Path "$($WorkingDirectory)\azFunctionResources\functionOverride\$($functionSourceFile.BaseName).ps1") - { - Write-PSFMessage -Level Host -Message " Override function definition detected, using override" - Copy-Item -Path "$($WorkingDirectory)\azFunctionResources\functionOverride\$($functionSourceFile.BaseName).ps1" -Destination $functionFolder.FullName - continue - } - - # Figure out the Rest Method to use - $methodName = 'Post' - if ($override.RestMethods) - { - $methodName = $override.RestMethods | Where-Object { $_ -ne 'Get' } | Select-Object -First 1 - } - - #endregion Load Overrides - - $currentFunctionsText = $functionsText -replace '%functionname%', $functionSourceFile.BaseName -replace '%condensedname%', $condensedName -replace '%method%', $methodName - - $parsedFunction = Read-PSMDScript -Path $functionSourceFile.FullName - $functionAst = $parsedFunction.Ast.EndBlock.Statements | Where-Object { - $_ -is [System.Management.Automation.Language.FunctionDefinitionAst] - } | Select-Object -First 1 - - $end = $functionAst.Body.ParamBlock.Extent.EndOffSet - $start = $functionAst.Body.Extent.StartOffSet + 1 - $currentFunctionsText = $currentFunctionsText.Replace('%parameter%', $functionAst.Body.Extent.Text.SubString(1, ($end - $start))) - - Write-PSFMessage -Level Host -Message " Creating file: $($functionFolder.FullName)\$($functionSourceFile.Name)" - [System.IO.File]::WriteAllText("$($functionFolder.FullName)\$($functionSourceFile.Name)", $currentFunctionsText, $encoding) -} -$functionsToExport = (Get-ChildItem -Path $functionFolder.FullName -Recurse -Filter *.ps1).BaseName | Sort-Object -#endregion Create Functions - -#region Create Core Module Files -# Get Manifest of published version, in order to catch build-phase changes such as module version. -$originalManifestData = Import-PowerShellDataFile -Path "$publishRoot\WinEventLogCustomization.psd1" -$prereqHash = @{ - ModuleName = 'PSFramework' - ModuleVersion = (Get-Module PSFramework).Version -} -$paramNewModuleManifest = @{ - Path = ('{0}\{1}.psd1' -f $workingRoot.FullName, $ModuleName) - FunctionsToExport = $functionsToExport - CompanyName = $originalManifestData.CompanyName - Author = $originalManifestData.Author - Description = $originalManifestData.Description - ModuleVersion = $originalManifestData.ModuleVersion - RootModule = ('{0}.psm1' -f $ModuleName) - Copyright = $originalManifestData.Copyright - TypesToProcess = @() - FormatsToProcess = @() - RequiredAssemblies = @() - RequiredModules = @($prereqHash) - CompatiblePSEditions = 'Core', 'Desktop' - PowerShellVersion = '5.1' -} - -if ($IncludeAssembly) { $paramNewModuleManifest.RequiredAssemblies = $originalManifestData.RequiredAssemblies } -if ($IncludeFormat) { $paramNewModuleManifest.FormatsToProcess = $originalManifestData.FormatsToProcess } -if ($IncludeType) { $paramNewModuleManifest.TypesToProcess = $originalManifestData.TypesToProcess } -Write-PSFMessage -Level Host -Message "Creating Module Manifest for module: $ModuleName" -New-ModuleManifest @paramNewModuleManifest - -Write-PSFMessage -Level Host -Message "Copying additional module files" -Copy-Item -Path "$($WorkingDirectory)\azFunctionResources\clientModule\moduleroot.psm1" -Destination "$($workingRoot.FullName)\$($ModuleName).psm1" -Copy-Item -Path "$($WorkingDirectory)\LICENSE" -Destination "$($workingRoot.FullName)\" -#endregion Create Core Module Files - -#region Transfer Additional Content -if ($IncludeAssembly) -{ - Copy-Item -Path "$publishRoot\bin" -Destination "$($workingRoot.FullName)\" -Recurse -} -if ($IncludeFormat -or $IncludeType) -{ - Copy-Item -Path "$publishRoot\xml" -Destination "$($workingRoot.FullName)\" -Recurse -} -#endregion Transfer Additional Content - -#region Publish -if ($LocalRepo) -{ - # Dependencies must go first - Write-PSFMessage -Level Important -Message "Creating Nuget Package for module: PSFramework" - New-PSMDModuleNugetPackage -ModulePath (Get-Module -Name PSFramework).ModuleBase -PackagePath . -WarningAction SilentlyContinue - Write-PSFMessage -Level Important -Message "Creating Nuget Package for module: WinEventLogCustomization" - New-PSMDModuleNugetPackage -ModulePath $workingRoot.FullName -PackagePath . -EnableException -} -else -{ - # Publish to Gallery - Write-PSFMessage -Level Important -Message "Publishing the WinEventLogCustomization module to $($Repository)" - Publish-Module -Path $workingRoot.FullName -NuGetApiKey $ApiKey -Force -Repository $Repository -} -#endregion Publish \ No newline at end of file diff --git a/build/vsts-packageFunction.ps1 b/build/vsts-packageFunction.ps1 deleted file mode 100644 index b5ca91c..0000000 --- a/build/vsts-packageFunction.ps1 +++ /dev/null @@ -1,143 +0,0 @@ - -<# - .SYNOPSIS - Packages an Azure Functions project, ready to release. - - .DESCRIPTION - Packages an Azure Functions project, ready to release. - Should be part of the release pipeline, after ensuring validation. - - Look into the 'AzureFunctionRest' template for generating functions for the module if you do. - - .PARAMETER WorkingDirectory - The root folder to work from. - - .PARAMETER Repository - The name of the repository to use for gathering dependencies from. -#> -param ( - $WorkingDirectory = "$($env:SYSTEM_DEFAULTWORKINGDIRECTORY)\_WinEventLogCustomization", - - $Repository = 'PSGallery', - - [switch] - $IncludeAZ -) - -$moduleName = 'WinEventLogCustomization' - -# Prepare Paths -Write-PSFMessage -Level Host -Message "Creating working folders" -$moduleRoot = Join-Path -Path $WorkingDirectory -ChildPath 'publish' -$workingRoot = New-Item -Path $WorkingDirectory -Name 'working' -ItemType Directory -$modulesFolder = New-Item -Path $workingRoot.FullName -Name Modules -ItemType Directory - -# Fill out the modules folder -Write-PSFMessage -Level Host -Message "Transfering built module data into working directory" -Copy-Item -Path "$moduleRoot\$moduleName" -Destination $modulesFolder.FullName -Recurse -Force -foreach ($dependency in (Import-PowerShellDataFile -Path "$moduleRoot\$moduleName\$moduleName.psd1").RequiredModules) -{ - $param = @{ - Repository = $Repository - Name = $dependency.ModuleName - Path = $modulesFolder.FullName - } - if ($dependency -is [string]) { $param['Name'] = $dependency } - if ($dependency.RequiredVersion) - { - $param['RequiredVersion'] = $dependency.RequiredVersion - } - Write-PSFMessage -Level Host -Message "Preparing Dependency: $($param['Name'])" - Save-Module @param -} - -# Generate function configuration -Write-PSFMessage -Level Host -Message 'Generating function configuration' -$runTemplate = Get-Content -Path "$($WorkingDirectory)\azFunctionResources\run.ps1" -Raw -foreach ($functionSourceFile in (Get-ChildItem -Path "$($moduleRoot)\$moduleName\functions" -Recurse -Filter '*.ps1')) -{ - Write-PSFMessage -Level Host -Message " Processing function: $functionSourceFile" - $condensedName = $functionSourceFile.BaseName -replace '-', '' - $functionFolder = New-Item -Path $workingRoot.FullName -Name $condensedName -ItemType Directory - - #region Load Overrides - $override = @{ } - if (Test-Path -Path "$($WorkingDirectory)\azFunctionResources\functionOverride\$($functionSourceFile.BaseName).psd1") - { - $override = Import-PowerShellDataFile -Path "$($WorkingDirectory)\azFunctionResources\functionOverride\$($functionSourceFile.BaseName).psd1" - } - if (Test-Path -Path "$($WorkingDirectory)\azFunctionResources\functionOverride\$($condensedName).psd1") - { - $override = Import-PowerShellDataFile -Path "$($WorkingDirectory)\azFunctionResources\functionOverride\$($condensedName).psd1" - } - #endregion Load Overrides - - #region Create Function Configuration - $restMethods = 'get', 'post' - if ($override.RestMethods) { $restMethods = $override.RestMethods } - - Set-Content -Path "$($functionFolder.FullName)\function.json" -Value @" -{ - "bindings": [ - { - "authLevel": "function", - "type": "httpTrigger", - "direction": "in", - "name": "Request", - "methods": [ - "$($restMethods -join "`", - `"")" - ] - }, - { - "type": "http", - "direction": "out", - "name": "Response" - } - ], - "disabled": false -} -"@ - #endregion Create Function Configuration - - #region Override Function Configuration - if (Test-Path -Path "$($WorkingDirectory)\azFunctionResources\functionOverride\$($functionSourceFile.BaseName).json") - { - Copy-Item -Path "$($WorkingDirectory)\azFunctionResources\functionOverride\$($functionSourceFile.BaseName).json" -Destination "$($functionFolder.FullName)\function.json" -Force - } - if (Test-Path -Path "$($WorkingDirectory)\azFunctionResources\functionOverride\$($condensedName).json") - { - Copy-Item -Path "$($WorkingDirectory)\azFunctionResources\functionOverride\$($condensedName).json" -Destination "$($functionFolder.FullName)\function.json" -Force - } - #endregion Override Function Configuration - - # Generate the run.ps1 file - $runText = $runTemplate -replace '%functionname%', $functionSourceFile.BaseName - $runText | Set-Content -Path "$($functionFolder.FullName)\run.ps1" -Encoding UTF8 -} - -# Transfer common files -Write-PSFMessage -Level Host -Message "Transfering core function data" -if ($IncludeAZ) -{ - Copy-Item -Path "$($WorkingDirectory)\azFunctionResources\host-az.json" -Destination "$($workingroot.FullName)\host.json" - Copy-Item -Path "$($WorkingDirectory)\azFunctionResources\requirements.psd1" -Destination "$($workingroot.FullName)\" -} -else -{ - Copy-Item -Path "$($WorkingDirectory)\azFunctionResources\host.json" -Destination "$($workingroot.FullName)\" -} -Copy-Item -Path "$($WorkingDirectory)\azFunctionResources\local.settings.json" -Destination "$($workingroot.FullName)\" - -# Build the profile file -$text = @() -$text += Get-Content -Path "$($WorkingDirectory)\azFunctionResources\profile.ps1" -Raw -foreach ($functionFile in (Get-ChildItem "$($WorkingDirectory)\azFunctionResources\profileFunctions" -Recurse)) -{ - $text += Get-Content -Path $functionFile.FullName -Raw -} -$text -join "`n`n" | Set-Content "$($workingroot.FullName)\profile.ps1" - -# Zip It -Write-PSFMessage -Level Host -Message "Creating function archive in '$($WorkingDirectory)\$moduleName.zip'" -Compress-Archive -Path "$($workingroot.FullName)\*" -DestinationPath "$($WorkingDirectory)\$moduleName.zip" -Force \ No newline at end of file diff --git a/build/vsts-prerequisites.ps1 b/build/vsts-prerequisites.ps1 index ce1fb39..c9a1d5d 100644 --- a/build/vsts-prerequisites.ps1 +++ b/build/vsts-prerequisites.ps1 @@ -11,8 +11,7 @@ foreach ($dependency in $data.RequiredModules) { if ($dependency -is [string]) { if ($modules -contains $dependency) { continue } $modules += $dependency - } - else { + } else { if ($modules -contains $dependency.ModuleName) { continue } $modules += $dependency.ModuleName } @@ -23,7 +22,3 @@ foreach ($module in $modules) { Install-Module $module -Force -SkipPublisherCheck -Repository $Repository Import-Module $module -Force -PassThru } - -Write-Host "Installing Pester v5.3.1" -ForegroundColor Cyan -Install-Module -Name "Pester" -RequiredVersion 5.3.1 -Force -SkipPublisherCheck -Repository $Repository -Import-Module "Pester" -Force -PassThru diff --git a/install.ps1 b/install.ps1 index 394b26b..b81b7a9 100644 --- a/install.ps1 +++ b/install.ps1 @@ -1,43 +1,43 @@ <# - .SYNOPSIS - Installs the WinEventLogCustomization Module from github - - .DESCRIPTION - This script installs the WinEventLogCustomization Module from github. - - It does so by ... - - downloading the specified branch as zip to $env:TEMP - - Unpacking that zip file to a folder in $env:TEMP - - Moving that content to a module folder in either program files (default) or the user profile - - .PARAMETER Branch - The branch to install. Installs master by default. - Unknown branches will terminate the script in error. - - .PARAMETER UserMode - The downloaded module will be moved to the user profile, rather than program files. - - .PARAMETER Scope - By default, the downloaded module will be moved to program files. - Setting this to 'CurrentUser' installs to the userprofile of the current user. - - .PARAMETER Force - The install script will overwrite an existing module. + .SYNOPSIS + Installs the WinEventLogCustomization Module from github + + .DESCRIPTION + This script installs the WinEventLogCustomization Module from github. + + It does so by ... + - downloading the specified branch as zip to $env:TEMP + - Unpacking that zip file to a folder in $env:TEMP + - Moving that content to a module folder in either program files (default) or the user profile + + .PARAMETER Branch + The branch to install. Installs master by default. + Unknown branches will terminate the script in error. + + .PARAMETER UserMode + The downloaded module will be moved to the user profile, rather than program files. + + .PARAMETER Scope + By default, the downloaded module will be moved to program files. + Setting this to 'CurrentUser' installs to the userprofile of the current user. + + .PARAMETER Force + The install script will overwrite an existing module. #> [CmdletBinding()] Param ( - [string] - $Branch = "master", - - [switch] - $UserMode, - - [ValidateSet('AllUsers', 'CurrentUser')] - [string] - $Scope = "AllUsers", - - [switch] - $Force + [string] + $Branch = "main", + + [switch] + $UserMode, + + [ValidateSet('AllUsers', 'CurrentUser')] + [string] + $Scope = "AllUsers", + + [switch] + $Force ) #region Configuration for cloning script @@ -45,7 +45,7 @@ Param ( $ModuleName = "WinEventLogCustomization" # Base path to the github repository -$BaseUrl = "https://github.com//WinEventLogCustomization" +$BaseUrl = "https://github.com/AndiBellstedt/WinEventLogCustomization" # If the module is in a subfolder of the cloned repository, specify relative path here. Empty string to skip. $SubFolder = "WinEventLogCustomization" @@ -61,128 +61,126 @@ if ($install_Branch) { $Branch = $install_Branch } #endregion Parameter Calculation #region Utility Functions -function Compress-Archive -{ - <# - .SYNOPSIS - Creates an archive, or zipped file, from specified files and folders. +function Compress-Archive { + <# + .SYNOPSIS + Creates an archive, or zipped file, from specified files and folders. - .DESCRIPTION - The Compress-Archive cmdlet creates a zipped (or compressed) archive file from one or more specified files or folders. An archive file allows multiple files to be packaged, and optionally compressed, into a single zipped file for easier distribution and storage. An archive file can be compressed by using the compression algorithm specified by the CompressionLevel parameter. + .DESCRIPTION + The Compress-Archive cmdlet creates a zipped (or compressed) archive file from one or more specified files or folders. An archive file allows multiple files to be packaged, and optionally compressed, into a single zipped file for easier distribution and storage. An archive file can be compressed by using the compression algorithm specified by the CompressionLevel parameter. - Because Compress-Archive relies upon the Microsoft .NET Framework API System.IO.Compression.ZipArchive to compress files, the maximum file size that you can compress by using Compress-Archive is currently 2 GB. This is a limitation of the underlying API. + Because Compress-Archive relies upon the Microsoft .NET Framework API System.IO.Compression.ZipArchive to compress files, the maximum file size that you can compress by using Compress-Archive is currently 2 GB. This is a limitation of the underlying API. - .PARAMETER Path - Specifies the path or paths to the files that you want to add to the archive zipped file. This parameter can accept wildcard characters. Wildcard characters allow you to add all files in a folder to your zipped archive file. To specify multiple paths, and include files in multiple locations in your output zipped file, use commas to separate the paths. + .PARAMETER Path + Specifies the path or paths to the files that you want to add to the archive zipped file. This parameter can accept wildcard characters. Wildcard characters allow you to add all files in a folder to your zipped archive file. To specify multiple paths, and include files in multiple locations in your output zipped file, use commas to separate the paths. - .PARAMETER LiteralPath - Specifies the path or paths to the files that you want to add to the archive zipped file. Unlike the Path parameter, the value of LiteralPath is used exactly as it is typed. No characters are interpreted as wildcards. If the path includes escape characters, enclose each escape character in single quotation marks, to instruct Windows PowerShell not to interpret any characters as escape sequences. To specify multiple paths, and include files in multiple locations in your output zipped file, use commas to separate the paths. + .PARAMETER LiteralPath + Specifies the path or paths to the files that you want to add to the archive zipped file. Unlike the Path parameter, the value of LiteralPath is used exactly as it is typed. No characters are interpreted as wildcards. If the path includes escape characters, enclose each escape character in single quotation marks, to instruct Windows PowerShell not to interpret any characters as escape sequences. To specify multiple paths, and include files in multiple locations in your output zipped file, use commas to separate the paths. - .PARAMETER DestinationPath - Specifies the path to the archive output file. This parameter is required. The specified DestinationPath value should include the desired name of the output zipped file; it specifies either the absolute or relative path to the zipped file. If the file name specified in DestinationPath does not have a .zip file name extension, the cmdlet adds a .zip file name extension. + .PARAMETER DestinationPath + Specifies the path to the archive output file. This parameter is required. The specified DestinationPath value should include the desired name of the output zipped file; it specifies either the absolute or relative path to the zipped file. If the file name specified in DestinationPath does not have a .zip file name extension, the cmdlet adds a .zip file name extension. - .PARAMETER CompressionLevel - Specifies how much compression to apply when you are creating the archive file. Faster compression requires less time to create the file, but can result in larger file sizes. The acceptable values for this parameter are: + .PARAMETER CompressionLevel + Specifies how much compression to apply when you are creating the archive file. Faster compression requires less time to create the file, but can result in larger file sizes. The acceptable values for this parameter are: - - Fastest. Use the fastest compression method available to decrease processing time; this can result in larger file sizes. - - NoCompression. Do not compress the source files. - - Optimal. Processing time is dependent on file size. + - Fastest. Use the fastest compression method available to decrease processing time; this can result in larger file sizes. + - NoCompression. Do not compress the source files. + - Optimal. Processing time is dependent on file size. - If this parameter is not specified, the command uses the default value, Optimal. + If this parameter is not specified, the command uses the default value, Optimal. - .PARAMETER Update - Updates the specified archive by replacing older versions of files in the archive with newer versions of files that have the same names. You can also add this parameter to add files to an existing archive. + .PARAMETER Update + Updates the specified archive by replacing older versions of files in the archive with newer versions of files that have the same names. You can also add this parameter to add files to an existing archive. - .PARAMETER Force - @{Text=} + .PARAMETER Force + @{Text=} - .PARAMETER Confirm - Prompts you for confirmation before running the cmdlet. + .PARAMETER Confirm + Prompts you for confirmation before running the cmdlet. - .PARAMETER WhatIf - Shows what would happen if the cmdlet runs. The cmdlet is not run. + .PARAMETER WhatIf + Shows what would happen if the cmdlet runs. The cmdlet is not run. - .EXAMPLE - Example 1: Create an archive file + .EXAMPLE + Example 1: Create an archive file - PS C:\>Compress-Archive -LiteralPath C:\Reference\Draftdoc.docx, C:\Reference\Images\diagram2.vsd -CompressionLevel Optimal -DestinationPath C:\Archives\Draft.Zip + PS C:\>Compress-Archive -LiteralPath C:\Reference\Draftdoc.docx, C:\Reference\Images\diagram2.vsd -CompressionLevel Optimal -DestinationPath C:\Archives\Draft.Zip - This command creates a new archive file, Draft.zip, by compressing two files, Draftdoc.docx and diagram2.vsd, specified by the LiteralPath parameter. The compression level specified for this operation is Optimal. + This command creates a new archive file, Draft.zip, by compressing two files, Draftdoc.docx and diagram2.vsd, specified by the LiteralPath parameter. The compression level specified for this operation is Optimal. - .EXAMPLE - Example 2: Create an archive with wildcard characters + .EXAMPLE + Example 2: Create an archive with wildcard characters - PS C:\>Compress-Archive -Path C:\Reference\* -CompressionLevel Fastest -DestinationPath C:\Archives\Draft + PS C:\>Compress-Archive -Path C:\Reference\* -CompressionLevel Fastest -DestinationPath C:\Archives\Draft - This command creates a new archive file, Draft.zip, in the C:\Archives folder. Note that though the file name extension .zip was not added to the value of the DestinationPath parameter, Windows PowerShell appends this to the specified archive file name automatically. The new archive file contains every file in the C:\Reference folder, because a wildcard character was used in place of specific file names in the Path parameter. The specified compression level is Fastest, which might result in a larger output file, but compresses a large number of files faster. + This command creates a new archive file, Draft.zip, in the C:\Archives folder. Note that though the file name extension .zip was not added to the value of the DestinationPath parameter, Windows PowerShell appends this to the specified archive file name automatically. The new archive file contains every file in the C:\Reference folder, because a wildcard character was used in place of specific file names in the Path parameter. The specified compression level is Fastest, which might result in a larger output file, but compresses a large number of files faster. - .EXAMPLE - Example 3: Update an existing archive file + .EXAMPLE + Example 3: Update an existing archive file - PS C:\>Compress-Archive -Path C:\Reference\* -Update -DestinationPath C:\Archives\Draft.Zip + PS C:\>Compress-Archive -Path C:\Reference\* -Update -DestinationPath C:\Archives\Draft.Zip - This command updates an existing archive file, Draft.Zip, in the C:\Archives folder. The command is run to update Draft.Zip with newer versions of existing files that came from the C:\Reference folder, and also to add new files that have been added to C:\Reference since Draft.Zip was initially created. + This command updates an existing archive file, Draft.Zip, in the C:\Archives folder. The command is run to update Draft.Zip with newer versions of existing files that came from the C:\Reference folder, and also to add new files that have been added to C:\Reference since Draft.Zip was initially created. - .EXAMPLE - Example 4: Create an archive from an entire folder + .EXAMPLE + Example 4: Create an archive from an entire folder - PS C:\>Compress-Archive -Path C:\Reference -DestinationPath C:\Archives\Draft + PS C:\>Compress-Archive -Path C:\Reference -DestinationPath C:\Archives\Draft - This command creates an archive from an entire folder, C:\Reference. Note that though the file name extension .zip was not added to the value of the DestinationPath parameter, Windows PowerShell appends this to the specified archive file name automatically. - #> - [CmdletBinding(DefaultParameterSetName = "Path", SupportsShouldProcess = $true, HelpUri = "http://go.microsoft.com/fwlink/?LinkID=393252")] - param - ( - [parameter (mandatory = $true, Position = 0, ParameterSetName = "Path", ValueFromPipeline = $true, ValueFromPipelineByPropertyName = $true)] - [parameter (mandatory = $true, Position = 0, ParameterSetName = "PathWithForce", ValueFromPipeline = $true, ValueFromPipelineByPropertyName = $true)] - [parameter (mandatory = $true, Position = 0, ParameterSetName = "PathWithUpdate", ValueFromPipeline = $true, ValueFromPipelineByPropertyName = $true)] - [ValidateNotNullOrEmpty()] - [string[]] - $Path, + This command creates an archive from an entire folder, C:\Reference. Note that though the file name extension .zip was not added to the value of the DestinationPath parameter, Windows PowerShell appends this to the specified archive file name automatically. + #> + [CmdletBinding(DefaultParameterSetName = "Path", SupportsShouldProcess = $true, HelpUri = "http://go.microsoft.com/fwlink/?LinkID=393252")] + param + ( + [parameter (mandatory = $true, Position = 0, ParameterSetName = "Path", ValueFromPipeline = $true, ValueFromPipelineByPropertyName = $true)] + [parameter (mandatory = $true, Position = 0, ParameterSetName = "PathWithForce", ValueFromPipeline = $true, ValueFromPipelineByPropertyName = $true)] + [parameter (mandatory = $true, Position = 0, ParameterSetName = "PathWithUpdate", ValueFromPipeline = $true, ValueFromPipelineByPropertyName = $true)] + [ValidateNotNullOrEmpty()] + [string[]] + $Path, - [parameter (mandatory = $true, ParameterSetName = "LiteralPath", ValueFromPipeline = $false, ValueFromPipelineByPropertyName = $true)] - [parameter (mandatory = $true, ParameterSetName = "LiteralPathWithForce", ValueFromPipeline = $false, ValueFromPipelineByPropertyName = $true)] - [parameter (mandatory = $true, ParameterSetName = "LiteralPathWithUpdate", ValueFromPipeline = $false, ValueFromPipelineByPropertyName = $true)] - [ValidateNotNullOrEmpty()] - [Alias("PSPath")] - [string[]] - $LiteralPath, + [parameter (mandatory = $true, ParameterSetName = "LiteralPath", ValueFromPipeline = $false, ValueFromPipelineByPropertyName = $true)] + [parameter (mandatory = $true, ParameterSetName = "LiteralPathWithForce", ValueFromPipeline = $false, ValueFromPipelineByPropertyName = $true)] + [parameter (mandatory = $true, ParameterSetName = "LiteralPathWithUpdate", ValueFromPipeline = $false, ValueFromPipelineByPropertyName = $true)] + [ValidateNotNullOrEmpty()] + [Alias("PSPath")] + [string[]] + $LiteralPath, - [parameter (mandatory = $true, - Position = 1, - ValueFromPipeline = $false, - ValueFromPipelineByPropertyName = $false)] - [ValidateNotNullOrEmpty()] - [string] - $DestinationPath, + [parameter (mandatory = $true, + Position = 1, + ValueFromPipeline = $false, + ValueFromPipelineByPropertyName = $false)] + [ValidateNotNullOrEmpty()] + [string] + $DestinationPath, - [parameter ( - mandatory = $false, - ValueFromPipeline = $false, - ValueFromPipelineByPropertyName = $false)] - [ValidateSet("Optimal", "NoCompression", "Fastest")] - [string] - $CompressionLevel = "Optimal", + [parameter ( + mandatory = $false, + ValueFromPipeline = $false, + ValueFromPipelineByPropertyName = $false)] + [ValidateSet("Optimal", "NoCompression", "Fastest")] + [string] + $CompressionLevel = "Optimal", - [parameter(mandatory = $true, ParameterSetName = "PathWithUpdate", ValueFromPipeline = $false, ValueFromPipelineByPropertyName = $false)] - [parameter(mandatory = $true, ParameterSetName = "LiteralPathWithUpdate", ValueFromPipeline = $false, ValueFromPipelineByPropertyName = $false)] - [switch] - $Update = $false, + [parameter(mandatory = $true, ParameterSetName = "PathWithUpdate", ValueFromPipeline = $false, ValueFromPipelineByPropertyName = $false)] + [parameter(mandatory = $true, ParameterSetName = "LiteralPathWithUpdate", ValueFromPipeline = $false, ValueFromPipelineByPropertyName = $false)] + [switch] + $Update = $false, - [parameter(mandatory = $true, ParameterSetName = "PathWithForce", ValueFromPipeline = $false, ValueFromPipelineByPropertyName = $false)] - [parameter(mandatory = $true, ParameterSetName = "LiteralPathWithForce", ValueFromPipeline = $false, ValueFromPipelineByPropertyName = $false)] - [switch] - $Force = $false - ) + [parameter(mandatory = $true, ParameterSetName = "PathWithForce", ValueFromPipeline = $false, ValueFromPipelineByPropertyName = $false)] + [parameter(mandatory = $true, ParameterSetName = "LiteralPathWithForce", ValueFromPipeline = $false, ValueFromPipelineByPropertyName = $false)] + [switch] + $Force = $false + ) - BEGIN - { - Add-Type -AssemblyName System.IO.Compression -ErrorAction Ignore - Add-Type -AssemblyName System.IO.Compression.FileSystem -ErrorAction Ignore + BEGIN { + Add-Type -AssemblyName System.IO.Compression -ErrorAction Ignore + Add-Type -AssemblyName System.IO.Compression.FileSystem -ErrorAction Ignore - $zipFileExtension = ".zip" + $zipFileExtension = ".zip" - $LocalizedData = ConvertFrom-StringData @' + $LocalizedData = ConvertFrom-StringData @' PathNotFoundError=The path '{0}' either does not exist or is not a valid file system path. ExpandArchiveInValidDestinationPath=The path '{0}' is not a valid file system directory path. InvalidZipFileExtensionError={0} is not a supported archive file format. {1} is the only supported archive file format. @@ -204,1131 +202,980 @@ PreparingToCompressVerboseMessage=Preparing to compress... PreparingToExpandVerboseMessage=Preparing to expand... '@ - #region Utility Functions - function GetResolvedPathHelper - { - param - ( - [string[]] - $path, - - [boolean] - $isLiteralPath, - - [System.Management.Automation.PSCmdlet] - $callerPSCmdlet - ) - - $resolvedPaths = @() - - # null and empty check are are already done on Path parameter at the cmdlet layer. - foreach ($currentPath in $path) - { - try - { - if ($isLiteralPath) - { - $currentResolvedPaths = Resolve-Path -LiteralPath $currentPath -ErrorAction Stop - } - else - { - $currentResolvedPaths = Resolve-Path -Path $currentPath -ErrorAction Stop - } - } - catch - { - $errorMessage = ($LocalizedData.PathNotFoundError -f $currentPath) - $exception = New-Object System.InvalidOperationException $errorMessage, $_.Exception - $errorRecord = CreateErrorRecordHelper "ArchiveCmdletPathNotFound" $null ([System.Management.Automation.ErrorCategory]::InvalidArgument) $exception $currentPath - $callerPSCmdlet.ThrowTerminatingError($errorRecord) - } - - foreach ($currentResolvedPath in $currentResolvedPaths) - { - $resolvedPaths += $currentResolvedPath.ProviderPath - } - } - - $resolvedPaths - } - - function Add-CompressionAssemblies - { - - if ($PSEdition -eq "Desktop") - { - Add-Type -AssemblyName System.IO.Compression - Add-Type -AssemblyName System.IO.Compression.FileSystem - } - } - - function IsValidFileSystemPath - { - param - ( - [string[]] - $path - ) - - $result = $true; - - # null and empty check are are already done on Path parameter at the cmdlet layer. - foreach ($currentPath in $path) - { - if (!([System.IO.File]::Exists($currentPath) -or [System.IO.Directory]::Exists($currentPath))) - { - $errorMessage = ($LocalizedData.PathNotFoundError -f $currentPath) - ThrowTerminatingErrorHelper "PathNotFound" $errorMessage ([System.Management.Automation.ErrorCategory]::InvalidArgument) $currentPath - } - } - - return $result; - } - - - function ValidateDuplicateFileSystemPath - { - param - ( - [string] - $inputParameter, - - [string[]] - $path - ) - - $uniqueInputPaths = @() - - # null and empty check are are already done on Path parameter at the cmdlet layer. - foreach ($currentPath in $path) - { - $currentInputPath = $currentPath.ToUpper() - if ($uniqueInputPaths.Contains($currentInputPath)) - { - $errorMessage = ($LocalizedData.DuplicatePathFoundError -f $inputParameter, $currentPath, $inputParameter) - ThrowTerminatingErrorHelper "DuplicatePathFound" $errorMessage ([System.Management.Automation.ErrorCategory]::InvalidArgument) $currentPath - } - else - { - $uniqueInputPaths += $currentInputPath - } - } - } - - function CompressionLevelMapper - { - param - ( - [string] - $compressionLevel - ) - - $compressionLevelFormat = [System.IO.Compression.CompressionLevel]::Optimal - - # CompressionLevel format is already validated at the cmdlet layer. - switch ($compressionLevel.ToString()) - { - "Fastest" - { - $compressionLevelFormat = [System.IO.Compression.CompressionLevel]::Fastest - } - "NoCompression" - { - $compressionLevelFormat = [System.IO.Compression.CompressionLevel]::NoCompression - } - } - - return $compressionLevelFormat - } - - function CompressArchiveHelper - { - param - ( - [string[]] - $sourcePath, - - [string] - $destinationPath, - - [string] - $compressionLevel, - - [bool] - $isUpdateMode - ) - - $numberOfItemsArchived = 0 - $sourceFilePaths = @() - $sourceDirPaths = @() - - foreach ($currentPath in $sourcePath) - { - $result = Test-Path -LiteralPath $currentPath -PathType Leaf - if ($result -eq $true) - { - $sourceFilePaths += $currentPath - } - else - { - $sourceDirPaths += $currentPath - } - } - - # The Soure Path contains one or more directory (this directory can have files under it) and no files to be compressed. - if ($sourceFilePaths.Count -eq 0 -and $sourceDirPaths.Count -gt 0) - { - $currentSegmentWeight = 100/[double]$sourceDirPaths.Count - $previousSegmentWeight = 0 - foreach ($currentSourceDirPath in $sourceDirPaths) - { - $count = CompressSingleDirHelper $currentSourceDirPath $destinationPath $compressionLevel $true $isUpdateMode $previousSegmentWeight $currentSegmentWeight - $numberOfItemsArchived += $count - $previousSegmentWeight += $currentSegmentWeight - } - } - - # The Soure Path contains only files to be compressed. - elseIf ($sourceFilePaths.Count -gt 0 -and $sourceDirPaths.Count -eq 0) - { - # $previousSegmentWeight is equal to 0 as there are no prior segments. - # $currentSegmentWeight is set to 100 as all files have equal weightage. - $previousSegmentWeight = 0 - $currentSegmentWeight = 100 - - $numberOfItemsArchived = CompressFilesHelper $sourceFilePaths $destinationPath $compressionLevel $isUpdateMode $previousSegmentWeight $currentSegmentWeight - } - # The Soure Path contains one or more files and one or more directories (this directory can have files under it) to be compressed. - elseif ($sourceFilePaths.Count -gt 0 -and $sourceDirPaths.Count -gt 0) - { - # each directory is considered as an individual segments & all the individual files are clubed in to a separate sgemnet. - $currentSegmentWeight = 100/[double]($sourceDirPaths.Count + 1) - $previousSegmentWeight = 0 - - foreach ($currentSourceDirPath in $sourceDirPaths) - { - $count = CompressSingleDirHelper $currentSourceDirPath $destinationPath $compressionLevel $true $isUpdateMode $previousSegmentWeight $currentSegmentWeight - $numberOfItemsArchived += $count - $previousSegmentWeight += $currentSegmentWeight - } - - $count = CompressFilesHelper $sourceFilePaths $destinationPath $compressionLevel $isUpdateMode $previousSegmentWeight $currentSegmentWeight - $numberOfItemsArchived += $count - } - - return $numberOfItemsArchived - } - - function CompressFilesHelper - { - param - ( - [string[]] - $sourceFilePaths, - - [string] - $destinationPath, - - [string] - $compressionLevel, - - [bool] - $isUpdateMode, - - [double] - $previousSegmentWeight, - - [double] - $currentSegmentWeight - ) - - $numberOfItemsArchived = ZipArchiveHelper $sourceFilePaths $destinationPath $compressionLevel $isUpdateMode $null $previousSegmentWeight $currentSegmentWeight - - return $numberOfItemsArchived - } - - function CompressSingleDirHelper - { - param - ( - [string] - $sourceDirPath, - - [string] - $destinationPath, - - [string] - $compressionLevel, - - [bool] - $useParentDirAsRoot, - - [bool] - $isUpdateMode, - - [double] - $previousSegmentWeight, - - [double] - $currentSegmentWeight - ) - - [System.Collections.Generic.List[System.String]]$subDirFiles = @() - - if ($useParentDirAsRoot) - { - $sourceDirInfo = New-Object -TypeName System.IO.DirectoryInfo -ArgumentList $sourceDirPath - $sourceDirFullName = $sourceDirInfo.Parent.FullName - - # If the directory is present at the drive level the DirectoryInfo.Parent include '\' example: C:\ - # On the other hand if the directory exists at a deper level then DirectoryInfo.Parent - # has just the path (without an ending '\'). example C:\source - if ($sourceDirFullName.Length -eq 3) - { - $modifiedSourceDirFullName = $sourceDirFullName - } - else - { - $modifiedSourceDirFullName = $sourceDirFullName + "\" - } - } - else - { - $sourceDirFullName = $sourceDirPath - $modifiedSourceDirFullName = $sourceDirFullName + "\" - } - - $dirContents = Get-ChildItem -LiteralPath $sourceDirPath -Recurse - foreach ($currentContent in $dirContents) - { - $isContainer = $currentContent -is [System.IO.DirectoryInfo] - if (!$isContainer) - { - $subDirFiles.Add($currentContent.FullName) - } - else - { - # The currentContent points to a directory. - # We need to check if the directory is an empty directory, if so such a - # directory has to be explictly added to the archive file. - # if there are no files in the directory the GetFiles() API returns an empty array. - $files = $currentContent.GetFiles() - if ($files.Count -eq 0) - { - $subDirFiles.Add($currentContent.FullName + "\") - } - } - } - - $numberOfItemsArchived = ZipArchiveHelper $subDirFiles.ToArray() $destinationPath $compressionLevel $isUpdateMode $modifiedSourceDirFullName $previousSegmentWeight $currentSegmentWeight - - return $numberOfItemsArchived - } - - function ZipArchiveHelper - { - param - ( - [System.Collections.Generic.List[System.String]] - $sourcePaths, - - [string] - $destinationPath, - - [string] - $compressionLevel, - - [bool] - $isUpdateMode, - - [string] - $modifiedSourceDirFullName, - - [double] - $previousSegmentWeight, - - [double] - $currentSegmentWeight - ) - - $numberOfItemsArchived = 0 - $fileMode = [System.IO.FileMode]::Create - $result = Test-Path -LiteralPath $DestinationPath -PathType Leaf - if ($result -eq $true) - { - $fileMode = [System.IO.FileMode]::Open - } - - Add-CompressionAssemblies - - try - { - # At this point we are sure that the archive file has write access. - $archiveFileStreamArgs = @($destinationPath, $fileMode) - $archiveFileStream = New-Object -TypeName System.IO.FileStream -ArgumentList $archiveFileStreamArgs - - $zipArchiveArgs = @($archiveFileStream, [System.IO.Compression.ZipArchiveMode]::Update, $false) - $zipArchive = New-Object -TypeName System.IO.Compression.ZipArchive -ArgumentList $zipArchiveArgs - - $currentEntryCount = 0 - $progressBarStatus = ($LocalizedData.CompressProgressBarText -f $destinationPath) - $bufferSize = 4kb - $buffer = New-Object Byte[] $bufferSize - - foreach ($currentFilePath in $sourcePaths) - { - if ($modifiedSourceDirFullName -ne $null -and $modifiedSourceDirFullName.Length -gt 0) - { - $index = $currentFilePath.IndexOf($modifiedSourceDirFullName, [System.StringComparison]::OrdinalIgnoreCase) - $currentFilePathSubString = $currentFilePath.Substring($index, $modifiedSourceDirFullName.Length) - $relativeFilePath = $currentFilePath.Replace($currentFilePathSubString, "").Trim() - } - else - { - $relativeFilePath = [System.IO.Path]::GetFileName($currentFilePath) - } - - # Update mode is selected. - # Check to see if archive file already contains one or more zip files in it. - if ($isUpdateMode -eq $true -and $zipArchive.Entries.Count -gt 0) - { - $entryToBeUpdated = $null - - # Check if the file already exists in the archive file. - # If so replace it with new file from the input source. - # If the file does not exist in the archive file then default to - # create mode and create the entry in the archive file. - - foreach ($currentArchiveEntry in $zipArchive.Entries) - { - if ($currentArchiveEntry.FullName -eq $relativeFilePath) - { - $entryToBeUpdated = $currentArchiveEntry - break - } - } - - if ($entryToBeUpdated -ne $null) - { - $addItemtoArchiveFileMessage = ($LocalizedData.AddItemtoArchiveFile -f $currentFilePath) - $entryToBeUpdated.Delete() - } - } - - $compression = CompressionLevelMapper $compressionLevel - - # If a directory needs to be added to an archive file, - # by convention the .Net API's expect the path of the diretcory - # to end with '\' to detect the path as an directory. - if (!$relativeFilePath.EndsWith("\", [StringComparison]::OrdinalIgnoreCase)) - { - try - { - try - { - $currentFileStream = [System.IO.File]::Open($currentFilePath, [System.IO.FileMode]::Open, [System.IO.FileAccess]::Read) - } - catch - { - # Failed to access the file. Write a non terminating error to the pipeline - # and move on with the remaining files. - $exception = $_.Exception - if ($null -ne $_.Exception -and - $null -ne $_.Exception.InnerException) - { - $exception = $_.Exception.InnerException - } - $errorRecord = CreateErrorRecordHelper "CompressArchiveUnauthorizedAccessError" $null ([System.Management.Automation.ErrorCategory]::PermissionDenied) $exception $currentFilePath - Write-Error -ErrorRecord $errorRecord - } - - if ($null -ne $currentFileStream) - { - $srcStream = New-Object System.IO.BinaryReader $currentFileStream - - $currentArchiveEntry = $zipArchive.CreateEntry($relativeFilePath, $compression) - - # Updating the File Creation time so that the same timestamp would be retained after expanding the compressed file. - # At this point we are sure that Get-ChildItem would succeed. - $currentArchiveEntry.LastWriteTime = (Get-Item -LiteralPath $currentFilePath).LastWriteTime - - $destStream = New-Object System.IO.BinaryWriter $currentArchiveEntry.Open() - - while ($numberOfBytesRead = $srcStream.Read($buffer, 0, $bufferSize)) - { - $destStream.Write($buffer, 0, $numberOfBytesRead) - $destStream.Flush() - } - - $numberOfItemsArchived += 1 - $addItemtoArchiveFileMessage = ($LocalizedData.AddItemtoArchiveFile -f $currentFilePath) - } - } - finally - { - If ($null -ne $currentFileStream) - { - $currentFileStream.Dispose() - } - If ($null -ne $srcStream) - { - $srcStream.Dispose() - } - If ($null -ne $destStream) - { - $destStream.Dispose() - } - } - } - else - { - $currentArchiveEntry = $zipArchive.CreateEntry("$relativeFilePath", $compression) - $numberOfItemsArchived += 1 - $addItemtoArchiveFileMessage = ($LocalizedData.AddItemtoArchiveFile -f $currentFilePath) - } - - if ($null -ne $addItemtoArchiveFileMessage) - { - Write-Verbose $addItemtoArchiveFileMessage - } - - $currentEntryCount += 1 - ProgressBarHelper "Compress-Archive" $progressBarStatus $previousSegmentWeight $currentSegmentWeight $sourcePaths.Count $currentEntryCount - } - } - finally - { - If ($null -ne $zipArchive) - { - $zipArchive.Dispose() - } - - If ($null -ne $archiveFileStream) - { - $archiveFileStream.Dispose() - } - - # Complete writing progress. - Write-Progress -Activity "Compress-Archive" -Completed - } - - return $numberOfItemsArchived - } - -<############################################################################################ -# ValidateArchivePathHelper: This is a helper function used to validate the archive file -# path & its file format. The only supported archive file format is .zip -############################################################################################> - function ValidateArchivePathHelper - { - param - ( - [string] - $archiveFile - ) - - if ([System.IO.File]::Exists($archiveFile)) - { - $extension = [system.IO.Path]::GetExtension($archiveFile) - - # Invalid file extension is specifed for the zip file. - if ($extension -ne $zipFileExtension) - { - $errorMessage = ($LocalizedData.InvalidZipFileExtensionError -f $extension, $zipFileExtension) - ThrowTerminatingErrorHelper "NotSupportedArchiveFileExtension" $errorMessage ([System.Management.Automation.ErrorCategory]::InvalidArgument) $extension - } - } - else - { - $errorMessage = ($LocalizedData.PathNotFoundError -f $archiveFile) - ThrowTerminatingErrorHelper "PathNotFound" $errorMessage ([System.Management.Automation.ErrorCategory]::InvalidArgument) $archiveFile - } - } - -<############################################################################################ -# ExpandArchiveHelper: This is a helper function used to expand the archive file contents -# to the specified directory. -############################################################################################> - function ExpandArchiveHelper - { - param - ( - [string] - $archiveFile, - - [string] - $expandedDir, - - [ref] - $expandedItems, - - [boolean] - $force, - - [boolean] - $isVerbose, - - [boolean] - $isConfirm - ) - - Add-CompressionAssemblies - - try - { - # The existance of archive file has already been validated by ValidateArchivePathHelper - # before calling this helper function. - $archiveFileStreamArgs = @($archiveFile, [System.IO.FileMode]::Open, [System.IO.FileAccess]::Read) - $archiveFileStream = New-Object -TypeName System.IO.FileStream -ArgumentList $archiveFileStreamArgs - - $zipArchiveArgs = @($archiveFileStream, [System.IO.Compression.ZipArchiveMode]::Read, $false) - $zipArchive = New-Object -TypeName System.IO.Compression.ZipArchive -ArgumentList $zipArchiveArgs - - if ($zipArchive.Entries.Count -eq 0) - { - $archiveFileIsEmpty = ($LocalizedData.ArchiveFileIsEmpty -f $archiveFile) - Write-Verbose $archiveFileIsEmpty - return - } - - $currentEntryCount = 0 - $progressBarStatus = ($LocalizedData.ExpandProgressBarText -f $archiveFile) - - # The archive entries can either be empty directories or files. - foreach ($currentArchiveEntry in $zipArchive.Entries) - { - $currentArchiveEntryPath = Join-Path -Path $expandedDir -ChildPath $currentArchiveEntry.FullName - $extension = [system.IO.Path]::GetExtension($currentArchiveEntryPath) - - # The current archive entry is an empty directory - # The FullName of the Archive Entry representing a directory would end with a trailing '\'. - if ($extension -eq [string]::Empty -and - $currentArchiveEntryPath.EndsWith("\", [StringComparison]::OrdinalIgnoreCase)) - { - $pathExists = Test-Path -LiteralPath $currentArchiveEntryPath - - # The current archive entry expects an empty directory. - # Check if the existing directory is empty. If its not empty - # then it means that user has added this directory by other means. - if ($pathExists -eq $false) - { - New-Item $currentArchiveEntryPath -ItemType Directory -Confirm:$isConfirm | Out-Null - - if (Test-Path -LiteralPath $currentArchiveEntryPath -PathType Container) - { - $addEmptyDirectorytoExpandedPathMessage = ($LocalizedData.AddItemtoArchiveFile -f $currentArchiveEntryPath) - Write-Verbose $addEmptyDirectorytoExpandedPathMessage - - $expandedItems.Value += $currentArchiveEntryPath - } - } - } - else - { - try - { - $currentArchiveEntryFileInfo = New-Object -TypeName System.IO.FileInfo -ArgumentList $currentArchiveEntryPath - $parentDirExists = Test-Path -LiteralPath $currentArchiveEntryFileInfo.DirectoryName -PathType Container - - # If the Parent directory of the current entry in the archive file does not exist, then create it. - if ($parentDirExists -eq $false) - { - New-Item $currentArchiveEntryFileInfo.DirectoryName -ItemType Directory -Confirm:$isConfirm | Out-Null - - if (!(Test-Path -LiteralPath $currentArchiveEntryFileInfo.DirectoryName -PathType Container)) - { - # The directory referred by $currentArchiveEntryFileInfo.DirectoryName was not successfully created. - # This could be because the user has specified -Confirm paramter when Expand-Archive was invoked - # and authorization was not provided when confirmation was prompted. In such a scenario, - # we skip the current file in the archive and continue with the remaining archive file contents. - Continue - } - - $expandedItems.Value += $currentArchiveEntryFileInfo.DirectoryName - } - - $hasNonTerminatingError = $false - - # Check if the file in to which the current archive entry contents - # would be expanded already exists. - if ($currentArchiveEntryFileInfo.Exists) - { - if ($force) - { - Remove-Item -LiteralPath $currentArchiveEntryFileInfo.FullName -Force -ErrorVariable ev -Verbose:$isVerbose -Confirm:$isConfirm - if ($ev -ne $null) - { - $hasNonTerminatingError = $true - } - - if (Test-Path -LiteralPath $currentArchiveEntryFileInfo.FullName -PathType Leaf) - { - # The file referred by $currentArchiveEntryFileInfo.FullName was not successfully removed. - # This could be because the user has specified -Confirm paramter when Expand-Archive was invoked - # and authorization was not provided when confirmation was prompted. In such a scenario, - # we skip the current file in the archive and continue with the remaining archive file contents. - Continue - } - } - else - { - # Write non-terminating error to the pipeline. - $errorMessage = ($LocalizedData.FileExistsError -f $currentArchiveEntryFileInfo.FullName, $archiveFile, $currentArchiveEntryFileInfo.FullName, $currentArchiveEntryFileInfo.FullName) - $errorRecord = CreateErrorRecordHelper "ExpandArchiveFileExists" $errorMessage ([System.Management.Automation.ErrorCategory]::InvalidOperation) $null $currentArchiveEntryFileInfo.FullName - Write-Error -ErrorRecord $errorRecord - $hasNonTerminatingError = $true - } - } - - if (!$hasNonTerminatingError) - { - [System.IO.Compression.ZipFileExtensions]::ExtractToFile($currentArchiveEntry, $currentArchiveEntryPath, $false) - - # Add the expanded file path to the $expandedItems array, - # to keep track of all the expanded files created while expanding the archive file. - # If user enters CTRL + C then at that point of time, all these expanded files - # would be deleted as part of the clean up process. - $expandedItems.Value += $currentArchiveEntryPath - - $addFiletoExpandedPathMessage = ($LocalizedData.CreateFileAtExpandedPath -f $currentArchiveEntryPath) - Write-Verbose $addFiletoExpandedPathMessage - } - } - finally - { - If ($null -ne $destStream) - { - $destStream.Dispose() - } - - If ($null -ne $srcStream) - { - $srcStream.Dispose() - } - } - } - - $currentEntryCount += 1 - # $currentSegmentWeight is Set to 100 giving equal weightage to each file that is getting expanded. - # $previousSegmentWeight is set to 0 as there are no prior segments. - $previousSegmentWeight = 0 - $currentSegmentWeight = 100 - ProgressBarHelper "Expand-Archive" $progressBarStatus $previousSegmentWeight $currentSegmentWeight $zipArchive.Entries.Count $currentEntryCount - } - } - finally - { - If ($null -ne $zipArchive) - { - $zipArchive.Dispose() - } - - If ($null -ne $archiveFileStream) - { - $archiveFileStream.Dispose() - } - - # Complete writing progress. - Write-Progress -Activity "Expand-Archive" -Completed - } - } - -<############################################################################################ -# ProgressBarHelper: This is a helper function used to display progress message. -# This function is used by both Compress-Archive & Expand-Archive to display archive file -# creation/expansion progress. -############################################################################################> - function ProgressBarHelper - { - param - ( - [string] - $cmdletName, - - [string] - $status, - - [double] - $previousSegmentWeight, - - [double] - $currentSegmentWeight, - - [int] - $totalNumberofEntries, - - [int] - $currentEntryCount - ) - - if ($currentEntryCount -gt 0 -and - $totalNumberofEntries -gt 0 -and - $previousSegmentWeight -ge 0 -and - $currentSegmentWeight -gt 0) - { - $entryDefaultWeight = $currentSegmentWeight/[double]$totalNumberofEntries - - $percentComplete = $previousSegmentWeight + ($entryDefaultWeight * $currentEntryCount) - Write-Progress -Activity $cmdletName -Status $status -PercentComplete $percentComplete - } - } - -<############################################################################################ -# CSVHelper: This is a helper function used to append comma after each path specifid by -# the SourcePath array. This helper function is used to display all the user supplied paths -# in the WhatIf message. -############################################################################################> - function CSVHelper - { - param - ( - [string[]] - $sourcePath - ) - - # SourcePath has already been validated by the calling funcation. - if ($sourcePath.Count -gt 1) - { - $sourcePathInCsvFormat = "`n" - for ($currentIndex = 0; $currentIndex -lt $sourcePath.Count; $currentIndex++) - { - if ($currentIndex -eq $sourcePath.Count - 1) - { - $sourcePathInCsvFormat += $sourcePath[$currentIndex] - } - else - { - $sourcePathInCsvFormat += $sourcePath[$currentIndex] + "`n" - } - } - } - else - { - $sourcePathInCsvFormat = $sourcePath - } - - return $sourcePathInCsvFormat - } - -<############################################################################################ -# ThrowTerminatingErrorHelper: This is a helper function used to throw terminating error. -############################################################################################> - function ThrowTerminatingErrorHelper - { - param - ( - [string] - $errorId, - - [string] - $errorMessage, - - [System.Management.Automation.ErrorCategory] - $errorCategory, - - [object] - $targetObject, - - [Exception] - $innerException - ) - - if ($innerException -eq $null) - { - $exception = New-object System.IO.IOException $errorMessage - } - else - { - $exception = New-Object System.IO.IOException $errorMessage, $innerException - } - - $exception = New-Object System.IO.IOException $errorMessage - $errorRecord = New-Object System.Management.Automation.ErrorRecord $exception, $errorId, $errorCategory, $targetObject - $PSCmdlet.ThrowTerminatingError($errorRecord) - } - -<############################################################################################ -# CreateErrorRecordHelper: This is a helper function used to create an ErrorRecord -############################################################################################> - function CreateErrorRecordHelper - { - param - ( - [string] - $errorId, - - [string] - $errorMessage, - - [System.Management.Automation.ErrorCategory] - $errorCategory, - - [Exception] - $exception, - - [object] - $targetObject - ) - - if ($null -eq $exception) - { - $exception = New-Object System.IO.IOException $errorMessage - } - - $errorRecord = New-Object System.Management.Automation.ErrorRecord $exception, $errorId, $errorCategory, $targetObject - return $errorRecord - } - #endregion Utility Functions - - $inputPaths = @() - $destinationParentDir = [system.IO.Path]::GetDirectoryName($DestinationPath) - if ($null -eq $destinationParentDir) - { - $errorMessage = ($LocalizedData.InvalidDestinationPath -f $DestinationPath) - ThrowTerminatingErrorHelper "InvalidArchiveFilePath" $errorMessage ([System.Management.Automation.ErrorCategory]::InvalidArgument) $DestinationPath - } - - if ($destinationParentDir -eq [string]::Empty) - { - $destinationParentDir = '.' - } - - $achiveFileName = [system.IO.Path]::GetFileName($DestinationPath) - $destinationParentDir = GetResolvedPathHelper $destinationParentDir $false $PSCmdlet - - if ($destinationParentDir.Count -gt 1) - { - $errorMessage = ($LocalizedData.InvalidArchiveFilePathError -f $DestinationPath, "DestinationPath", "DestinationPath") - ThrowTerminatingErrorHelper "InvalidArchiveFilePath" $errorMessage ([System.Management.Automation.ErrorCategory]::InvalidArgument) $DestinationPath - } - - IsValidFileSystemPath $destinationParentDir | Out-Null - $DestinationPath = Join-Path -Path $destinationParentDir -ChildPath $achiveFileName - - # GetExtension API does not validate for the actual existance of the path. - $extension = [system.IO.Path]::GetExtension($DestinationPath) - - # If user does not specify .Zip extension, we append it. - If ($extension -eq [string]::Empty) - { - $DestinationPathWithOutExtension = $DestinationPath - $DestinationPath = $DestinationPathWithOutExtension + $zipFileExtension - $appendArchiveFileExtensionMessage = ($LocalizedData.AppendArchiveFileExtensionMessage -f $DestinationPathWithOutExtension, $DestinationPath) - Write-Verbose $appendArchiveFileExtensionMessage - } - else - { - # Invalid file extension is specified for the zip file to be created. - if ($extension -ne $zipFileExtension) - { - $errorMessage = ($LocalizedData.InvalidZipFileExtensionError -f $extension, $zipFileExtension) - ThrowTerminatingErrorHelper "NotSupportedArchiveFileExtension" $errorMessage ([System.Management.Automation.ErrorCategory]::InvalidArgument) $extension - } - } - - $archiveFileExist = Test-Path -LiteralPath $DestinationPath -PathType Leaf - - if ($archiveFileExist -and ($Update -eq $false -and $Force -eq $false)) - { - $errorMessage = ($LocalizedData.ZipFileExistError -f $DestinationPath) - ThrowTerminatingErrorHelper "ArchiveFileExists" $errorMessage ([System.Management.Automation.ErrorCategory]::InvalidArgument) $DestinationPath - } - - # If archive file already exists and if -Update is specified, then we check to see - # if we have write access permission to update the existing archive file. - if ($archiveFileExist -and $Update -eq $true) - { - $item = Get-Item -Path $DestinationPath - if ($item.Attributes.ToString().Contains("ReadOnly")) - { - $errorMessage = ($LocalizedData.ArchiveFileIsReadOnly -f $DestinationPath) - ThrowTerminatingErrorHelper "ArchiveFileIsReadOnly" $errorMessage ([System.Management.Automation.ErrorCategory]::InvalidOperation) $DestinationPath - } - } - - $isWhatIf = $psboundparameters.ContainsKey("WhatIf") - if (!$isWhatIf) - { - $preparingToCompressVerboseMessage = ($LocalizedData.PreparingToCompressVerboseMessage) - Write-Verbose $preparingToCompressVerboseMessage - - $progressBarStatus = ($LocalizedData.CompressProgressBarText -f $DestinationPath) - ProgressBarHelper "Compress-Archive" $progressBarStatus 0 100 100 1 - } - } - PROCESS - { - if ($PsCmdlet.ParameterSetName -eq "Path" -or - $PsCmdlet.ParameterSetName -eq "PathWithForce" -or - $PsCmdlet.ParameterSetName -eq "PathWithUpdate") - { - $inputPaths += $Path - } - - if ($PsCmdlet.ParameterSetName -eq "LiteralPath" -or - $PsCmdlet.ParameterSetName -eq "LiteralPathWithForce" -or - $PsCmdlet.ParameterSetName -eq "LiteralPathWithUpdate") - { - $inputPaths += $LiteralPath - } - } - END - { - # If archive file already exists and if -Force is specified, we delete the - # existing artchive file and create a brand new one. - if (($PsCmdlet.ParameterSetName -eq "PathWithForce" -or - $PsCmdlet.ParameterSetName -eq "LiteralPathWithForce") -and $archiveFileExist) - { - Remove-Item -Path $DestinationPath -Force -ErrorAction Stop - } - - # Validate Source Path depeding on parameter set being used. - # The specified source path conatins one or more files or directories that needs - # to be compressed. - $isLiteralPathUsed = $false - if ($PsCmdlet.ParameterSetName -eq "LiteralPath" -or - $PsCmdlet.ParameterSetName -eq "LiteralPathWithForce" -or - $PsCmdlet.ParameterSetName -eq "LiteralPathWithUpdate") - { - $isLiteralPathUsed = $true - } - - ValidateDuplicateFileSystemPath $PsCmdlet.ParameterSetName $inputPaths - $resolvedPaths = GetResolvedPathHelper $inputPaths $isLiteralPathUsed $PSCmdlet - IsValidFileSystemPath $resolvedPaths | Out-Null - - $sourcePath = $resolvedPaths; - - # CSVHelper: This is a helper function used to append comma after each path specifid by - # the $sourcePath array. The comma saperated paths are displayed in the -WhatIf message. - $sourcePathInCsvFormat = CSVHelper $sourcePath - if ($pscmdlet.ShouldProcess($sourcePathInCsvFormat)) - { - try - { - # StopProcessing is not avaliable in Script cmdlets. However the pipleline execution - # is terminated when ever 'CTRL + C' is entered by user to terminate the cmdlet execution. - # The finally block is executed whenever pipleline is terminated. - # $isArchiveFileProcessingComplete variable is used to track if 'CTRL + C' is entered by the - # user. - $isArchiveFileProcessingComplete = $false - - $numberOfItemsArchived = CompressArchiveHelper $sourcePath $DestinationPath $CompressionLevel $Update - - $isArchiveFileProcessingComplete = $true - } - finally - { - # The $isArchiveFileProcessingComplete would be set to $false if user has typed 'CTRL + C' to - # terminate the cmdlet execution or if an unhandled exception is thrown. - # $numberOfItemsArchived contains the count of number of files or directories add to the archive file. - # If the newly created archive file is empty then we delete it as its not usable. - if (($isArchiveFileProcessingComplete -eq $false) -or - ($numberOfItemsArchived -eq 0)) - { - $DeleteArchiveFileMessage = ($LocalizedData.DeleteArchiveFile -f $DestinationPath) - Write-Verbose $DeleteArchiveFileMessage - - # delete the partial archive file created. - if (Test-Path $DestinationPath) - { - Remove-Item -LiteralPath $DestinationPath -Force -Recurse -ErrorAction SilentlyContinue - } - } - } - } - } + #region Utility Functions + function GetResolvedPathHelper { + param + ( + [string[]] + $path, + + [boolean] + $isLiteralPath, + + [System.Management.Automation.PSCmdlet] + $callerPSCmdlet + ) + + $resolvedPaths = @() + + # null and empty check are are already done on Path parameter at the cmdlet layer. + foreach ($currentPath in $path) { + try { + if ($isLiteralPath) { + $currentResolvedPaths = Resolve-Path -LiteralPath $currentPath -ErrorAction Stop + } else { + $currentResolvedPaths = Resolve-Path -Path $currentPath -ErrorAction Stop + } + } catch { + $errorMessage = ($LocalizedData.PathNotFoundError -f $currentPath) + $exception = New-Object System.InvalidOperationException $errorMessage, $_.Exception + $errorRecord = CreateErrorRecordHelper "ArchiveCmdletPathNotFound" $null ([System.Management.Automation.ErrorCategory]::InvalidArgument) $exception $currentPath + $callerPSCmdlet.ThrowTerminatingError($errorRecord) + } + + foreach ($currentResolvedPath in $currentResolvedPaths) { + $resolvedPaths += $currentResolvedPath.ProviderPath + } + } + + $resolvedPaths + } + + function Add-CompressionAssemblies { + + if ($PSEdition -eq "Desktop") { + Add-Type -AssemblyName System.IO.Compression + Add-Type -AssemblyName System.IO.Compression.FileSystem + } + } + + function IsValidFileSystemPath { + param + ( + [string[]] + $path + ) + + $result = $true; + + # null and empty check are are already done on Path parameter at the cmdlet layer. + foreach ($currentPath in $path) { + if (!([System.IO.File]::Exists($currentPath) -or [System.IO.Directory]::Exists($currentPath))) { + $errorMessage = ($LocalizedData.PathNotFoundError -f $currentPath) + ThrowTerminatingErrorHelper "PathNotFound" $errorMessage ([System.Management.Automation.ErrorCategory]::InvalidArgument) $currentPath + } + } + + return $result; + } + + + function ValidateDuplicateFileSystemPath { + param + ( + [string] + $inputParameter, + + [string[]] + $path + ) + + $uniqueInputPaths = @() + + # null and empty check are are already done on Path parameter at the cmdlet layer. + foreach ($currentPath in $path) { + $currentInputPath = $currentPath.ToUpper() + if ($uniqueInputPaths.Contains($currentInputPath)) { + $errorMessage = ($LocalizedData.DuplicatePathFoundError -f $inputParameter, $currentPath, $inputParameter) + ThrowTerminatingErrorHelper "DuplicatePathFound" $errorMessage ([System.Management.Automation.ErrorCategory]::InvalidArgument) $currentPath + } else { + $uniqueInputPaths += $currentInputPath + } + } + } + + function CompressionLevelMapper { + param + ( + [string] + $compressionLevel + ) + + $compressionLevelFormat = [System.IO.Compression.CompressionLevel]::Optimal + + # CompressionLevel format is already validated at the cmdlet layer. + switch ($compressionLevel.ToString()) { + "Fastest" { + $compressionLevelFormat = [System.IO.Compression.CompressionLevel]::Fastest + } + "NoCompression" { + $compressionLevelFormat = [System.IO.Compression.CompressionLevel]::NoCompression + } + } + + return $compressionLevelFormat + } + + function CompressArchiveHelper { + param + ( + [string[]] + $sourcePath, + + [string] + $destinationPath, + + [string] + $compressionLevel, + + [bool] + $isUpdateMode + ) + + $numberOfItemsArchived = 0 + $sourceFilePaths = @() + $sourceDirPaths = @() + + foreach ($currentPath in $sourcePath) { + $result = Test-Path -LiteralPath $currentPath -PathType Leaf + if ($result -eq $true) { + $sourceFilePaths += $currentPath + } else { + $sourceDirPaths += $currentPath + } + } + + # The Soure Path contains one or more directory (this directory can have files under it) and no files to be compressed. + if ($sourceFilePaths.Count -eq 0 -and $sourceDirPaths.Count -gt 0) { + $currentSegmentWeight = 100 / [double]$sourceDirPaths.Count + $previousSegmentWeight = 0 + foreach ($currentSourceDirPath in $sourceDirPaths) { + $count = CompressSingleDirHelper $currentSourceDirPath $destinationPath $compressionLevel $true $isUpdateMode $previousSegmentWeight $currentSegmentWeight + $numberOfItemsArchived += $count + $previousSegmentWeight += $currentSegmentWeight + } + } + + # The Soure Path contains only files to be compressed. + elseIf ($sourceFilePaths.Count -gt 0 -and $sourceDirPaths.Count -eq 0) { + # $previousSegmentWeight is equal to 0 as there are no prior segments. + # $currentSegmentWeight is set to 100 as all files have equal weightage. + $previousSegmentWeight = 0 + $currentSegmentWeight = 100 + + $numberOfItemsArchived = CompressFilesHelper $sourceFilePaths $destinationPath $compressionLevel $isUpdateMode $previousSegmentWeight $currentSegmentWeight + } + # The Soure Path contains one or more files and one or more directories (this directory can have files under it) to be compressed. + elseif ($sourceFilePaths.Count -gt 0 -and $sourceDirPaths.Count -gt 0) { + # each directory is considered as an individual segments & all the individual files are clubed in to a separate sgemnet. + $currentSegmentWeight = 100 / [double]($sourceDirPaths.Count + 1) + $previousSegmentWeight = 0 + + foreach ($currentSourceDirPath in $sourceDirPaths) { + $count = CompressSingleDirHelper $currentSourceDirPath $destinationPath $compressionLevel $true $isUpdateMode $previousSegmentWeight $currentSegmentWeight + $numberOfItemsArchived += $count + $previousSegmentWeight += $currentSegmentWeight + } + + $count = CompressFilesHelper $sourceFilePaths $destinationPath $compressionLevel $isUpdateMode $previousSegmentWeight $currentSegmentWeight + $numberOfItemsArchived += $count + } + + return $numberOfItemsArchived + } + + function CompressFilesHelper { + param + ( + [string[]] + $sourceFilePaths, + + [string] + $destinationPath, + + [string] + $compressionLevel, + + [bool] + $isUpdateMode, + + [double] + $previousSegmentWeight, + + [double] + $currentSegmentWeight + ) + + $numberOfItemsArchived = ZipArchiveHelper $sourceFilePaths $destinationPath $compressionLevel $isUpdateMode $null $previousSegmentWeight $currentSegmentWeight + + return $numberOfItemsArchived + } + + function CompressSingleDirHelper { + param + ( + [string] + $sourceDirPath, + + [string] + $destinationPath, + + [string] + $compressionLevel, + + [bool] + $useParentDirAsRoot, + + [bool] + $isUpdateMode, + + [double] + $previousSegmentWeight, + + [double] + $currentSegmentWeight + ) + + [System.Collections.Generic.List[System.String]]$subDirFiles = @() + + if ($useParentDirAsRoot) { + $sourceDirInfo = New-Object -TypeName System.IO.DirectoryInfo -ArgumentList $sourceDirPath + $sourceDirFullName = $sourceDirInfo.Parent.FullName + + # If the directory is present at the drive level the DirectoryInfo.Parent include '\' example: C:\ + # On the other hand if the directory exists at a deper level then DirectoryInfo.Parent + # has just the path (without an ending '\'). example C:\source + if ($sourceDirFullName.Length -eq 3) { + $modifiedSourceDirFullName = $sourceDirFullName + } else { + $modifiedSourceDirFullName = $sourceDirFullName + "\" + } + } else { + $sourceDirFullName = $sourceDirPath + $modifiedSourceDirFullName = $sourceDirFullName + "\" + } + + $dirContents = Get-ChildItem -LiteralPath $sourceDirPath -Recurse + foreach ($currentContent in $dirContents) { + $isContainer = $currentContent -is [System.IO.DirectoryInfo] + if (!$isContainer) { + $subDirFiles.Add($currentContent.FullName) + } else { + # The currentContent points to a directory. + # We need to check if the directory is an empty directory, if so such a + # directory has to be explictly added to the archive file. + # if there are no files in the directory the GetFiles() API returns an empty array. + $files = $currentContent.GetFiles() + if ($files.Count -eq 0) { + $subDirFiles.Add($currentContent.FullName + "\") + } + } + } + + $numberOfItemsArchived = ZipArchiveHelper $subDirFiles.ToArray() $destinationPath $compressionLevel $isUpdateMode $modifiedSourceDirFullName $previousSegmentWeight $currentSegmentWeight + + return $numberOfItemsArchived + } + + function ZipArchiveHelper { + param ( + [System.Collections.Generic.List[System.String]] + $sourcePaths, + + [string] + $destinationPath, + + [string] + $compressionLevel, + + [bool] + $isUpdateMode, + + [string] + $modifiedSourceDirFullName, + + [double] + $previousSegmentWeight, + + [double] + $currentSegmentWeight + ) + + $numberOfItemsArchived = 0 + $fileMode = [System.IO.FileMode]::Create + $result = Test-Path -LiteralPath $DestinationPath -PathType Leaf + if ($result -eq $true) { + $fileMode = [System.IO.FileMode]::Open + } + + Add-CompressionAssemblies + + try { + # At this point we are sure that the archive file has write access. + $archiveFileStreamArgs = @($destinationPath, $fileMode) + $archiveFileStream = New-Object -TypeName System.IO.FileStream -ArgumentList $archiveFileStreamArgs + + $zipArchiveArgs = @($archiveFileStream, [System.IO.Compression.ZipArchiveMode]::Update, $false) + $zipArchive = New-Object -TypeName System.IO.Compression.ZipArchive -ArgumentList $zipArchiveArgs + + $currentEntryCount = 0 + $progressBarStatus = ($LocalizedData.CompressProgressBarText -f $destinationPath) + $bufferSize = 4kb + $buffer = New-Object Byte[] $bufferSize + + foreach ($currentFilePath in $sourcePaths) { + if ($modifiedSourceDirFullName -ne $null -and $modifiedSourceDirFullName.Length -gt 0) { + $index = $currentFilePath.IndexOf($modifiedSourceDirFullName, [System.StringComparison]::OrdinalIgnoreCase) + $currentFilePathSubString = $currentFilePath.Substring($index, $modifiedSourceDirFullName.Length) + $relativeFilePath = $currentFilePath.Replace($currentFilePathSubString, "").Trim() + } else { + $relativeFilePath = [System.IO.Path]::GetFileName($currentFilePath) + } + + # Update mode is selected. + # Check to see if archive file already contains one or more zip files in it. + if ($isUpdateMode -eq $true -and $zipArchive.Entries.Count -gt 0) { + $entryToBeUpdated = $null + + # Check if the file already exists in the archive file. + # If so replace it with new file from the input source. + # If the file does not exist in the archive file then default to + # create mode and create the entry in the archive file. + + foreach ($currentArchiveEntry in $zipArchive.Entries) { + if ($currentArchiveEntry.FullName -eq $relativeFilePath) { + $entryToBeUpdated = $currentArchiveEntry + break + } + } + + if ($entryToBeUpdated -ne $null) { + $addItemtoArchiveFileMessage = ($LocalizedData.AddItemtoArchiveFile -f $currentFilePath) + $entryToBeUpdated.Delete() + } + } + + $compression = CompressionLevelMapper $compressionLevel + + # If a directory needs to be added to an archive file, + # by convention the .Net API's expect the path of the diretcory + # to end with '\' to detect the path as an directory. + if (!$relativeFilePath.EndsWith("\", [StringComparison]::OrdinalIgnoreCase)) { + try { + try { + $currentFileStream = [System.IO.File]::Open($currentFilePath, [System.IO.FileMode]::Open, [System.IO.FileAccess]::Read) + } catch { + # Failed to access the file. Write a non terminating error to the pipeline + # and move on with the remaining files. + $exception = $_.Exception + if ($null -ne $_.Exception -and + $null -ne $_.Exception.InnerException) { + $exception = $_.Exception.InnerException + } + $errorRecord = CreateErrorRecordHelper "CompressArchiveUnauthorizedAccessError" $null ([System.Management.Automation.ErrorCategory]::PermissionDenied) $exception $currentFilePath + Write-Error -ErrorRecord $errorRecord + } + + if ($null -ne $currentFileStream) { + $srcStream = New-Object System.IO.BinaryReader $currentFileStream + + $currentArchiveEntry = $zipArchive.CreateEntry($relativeFilePath, $compression) + + # Updating the File Creation time so that the same timestamp would be retained after expanding the compressed file. + # At this point we are sure that Get-ChildItem would succeed. + $currentArchiveEntry.LastWriteTime = (Get-Item -LiteralPath $currentFilePath).LastWriteTime + + $destStream = New-Object System.IO.BinaryWriter $currentArchiveEntry.Open() + + while ($numberOfBytesRead = $srcStream.Read($buffer, 0, $bufferSize)) { + $destStream.Write($buffer, 0, $numberOfBytesRead) + $destStream.Flush() + } + + $numberOfItemsArchived += 1 + $addItemtoArchiveFileMessage = ($LocalizedData.AddItemtoArchiveFile -f $currentFilePath) + } + } finally { + If ($null -ne $currentFileStream) { + $currentFileStream.Dispose() + } + If ($null -ne $srcStream) { + $srcStream.Dispose() + } + If ($null -ne $destStream) { + $destStream.Dispose() + } + } + } else { + $currentArchiveEntry = $zipArchive.CreateEntry("$relativeFilePath", $compression) + $numberOfItemsArchived += 1 + $addItemtoArchiveFileMessage = ($LocalizedData.AddItemtoArchiveFile -f $currentFilePath) + } + + if ($null -ne $addItemtoArchiveFileMessage) { + Write-Verbose $addItemtoArchiveFileMessage + } + + $currentEntryCount += 1 + ProgressBarHelper "Compress-Archive" $progressBarStatus $previousSegmentWeight $currentSegmentWeight $sourcePaths.Count $currentEntryCount + } + } finally { + If ($null -ne $zipArchive) { + $zipArchive.Dispose() + } + + If ($null -ne $archiveFileStream) { + $archiveFileStream.Dispose() + } + + # Complete writing progress. + Write-Progress -Activity "Compress-Archive" -Completed + } + + return $numberOfItemsArchived + } + + <############################################################################################ + # ValidateArchivePathHelper: This is a helper function used to validate the archive file + # path & its file format. The only supported archive file format is .zip + ############################################################################################> + function ValidateArchivePathHelper { + param + ( + [string] + $archiveFile + ) + + if ([System.IO.File]::Exists($archiveFile)) { + $extension = [system.IO.Path]::GetExtension($archiveFile) + + # Invalid file extension is specifed for the zip file. + if ($extension -ne $zipFileExtension) { + $errorMessage = ($LocalizedData.InvalidZipFileExtensionError -f $extension, $zipFileExtension) + ThrowTerminatingErrorHelper "NotSupportedArchiveFileExtension" $errorMessage ([System.Management.Automation.ErrorCategory]::InvalidArgument) $extension + } + } else { + $errorMessage = ($LocalizedData.PathNotFoundError -f $archiveFile) + ThrowTerminatingErrorHelper "PathNotFound" $errorMessage ([System.Management.Automation.ErrorCategory]::InvalidArgument) $archiveFile + } + } + + <############################################################################################ + # ExpandArchiveHelper: This is a helper function used to expand the archive file contents + # to the specified directory. + ############################################################################################> + function ExpandArchiveHelper { + param + ( + [string] + $archiveFile, + + [string] + $expandedDir, + + [ref] + $expandedItems, + + [boolean] + $force, + + [boolean] + $isVerbose, + + [boolean] + $isConfirm + ) + + Add-CompressionAssemblies + + try { + # The existance of archive file has already been validated by ValidateArchivePathHelper + # before calling this helper function. + $archiveFileStreamArgs = @($archiveFile, [System.IO.FileMode]::Open, [System.IO.FileAccess]::Read) + $archiveFileStream = New-Object -TypeName System.IO.FileStream -ArgumentList $archiveFileStreamArgs + + $zipArchiveArgs = @($archiveFileStream, [System.IO.Compression.ZipArchiveMode]::Read, $false) + $zipArchive = New-Object -TypeName System.IO.Compression.ZipArchive -ArgumentList $zipArchiveArgs + + if ($zipArchive.Entries.Count -eq 0) { + $archiveFileIsEmpty = ($LocalizedData.ArchiveFileIsEmpty -f $archiveFile) + Write-Verbose $archiveFileIsEmpty + return + } + + $currentEntryCount = 0 + $progressBarStatus = ($LocalizedData.ExpandProgressBarText -f $archiveFile) + + # The archive entries can either be empty directories or files. + foreach ($currentArchiveEntry in $zipArchive.Entries) { + $currentArchiveEntryPath = Join-Path -Path $expandedDir -ChildPath $currentArchiveEntry.FullName + $extension = [system.IO.Path]::GetExtension($currentArchiveEntryPath) + + # The current archive entry is an empty directory + # The FullName of the Archive Entry representing a directory would end with a trailing '\'. + if ($extension -eq [string]::Empty -and + $currentArchiveEntryPath.EndsWith("\", [StringComparison]::OrdinalIgnoreCase)) { + $pathExists = Test-Path -LiteralPath $currentArchiveEntryPath + + # The current archive entry expects an empty directory. + # Check if the existing directory is empty. If its not empty + # then it means that user has added this directory by other means. + if ($pathExists -eq $false) { + New-Item $currentArchiveEntryPath -ItemType Directory -Confirm:$isConfirm | Out-Null + + if (Test-Path -LiteralPath $currentArchiveEntryPath -PathType Container) { + $addEmptyDirectorytoExpandedPathMessage = ($LocalizedData.AddItemtoArchiveFile -f $currentArchiveEntryPath) + Write-Verbose $addEmptyDirectorytoExpandedPathMessage + + $expandedItems.Value += $currentArchiveEntryPath + } + } + } else { + try { + $currentArchiveEntryFileInfo = New-Object -TypeName System.IO.FileInfo -ArgumentList $currentArchiveEntryPath + $parentDirExists = Test-Path -LiteralPath $currentArchiveEntryFileInfo.DirectoryName -PathType Container + + # If the Parent directory of the current entry in the archive file does not exist, then create it. + if ($parentDirExists -eq $false) { + New-Item $currentArchiveEntryFileInfo.DirectoryName -ItemType Directory -Confirm:$isConfirm | Out-Null + + if (!(Test-Path -LiteralPath $currentArchiveEntryFileInfo.DirectoryName -PathType Container)) { + # The directory referred by $currentArchiveEntryFileInfo.DirectoryName was not successfully created. + # This could be because the user has specified -Confirm paramter when Expand-Archive was invoked + # and authorization was not provided when confirmation was prompted. In such a scenario, + # we skip the current file in the archive and continue with the remaining archive file contents. + Continue + } + + $expandedItems.Value += $currentArchiveEntryFileInfo.DirectoryName + } + + $hasNonTerminatingError = $false + + # Check if the file in to which the current archive entry contents + # would be expanded already exists. + if ($currentArchiveEntryFileInfo.Exists) { + if ($force) { + Remove-Item -LiteralPath $currentArchiveEntryFileInfo.FullName -Force -ErrorVariable ev -Verbose:$isVerbose -Confirm:$isConfirm + if ($ev -ne $null) { + $hasNonTerminatingError = $true + } + + if (Test-Path -LiteralPath $currentArchiveEntryFileInfo.FullName -PathType Leaf) { + # The file referred by $currentArchiveEntryFileInfo.FullName was not successfully removed. + # This could be because the user has specified -Confirm paramter when Expand-Archive was invoked + # and authorization was not provided when confirmation was prompted. In such a scenario, + # we skip the current file in the archive and continue with the remaining archive file contents. + Continue + } + } else { + # Write non-terminating error to the pipeline. + $errorMessage = ($LocalizedData.FileExistsError -f $currentArchiveEntryFileInfo.FullName, $archiveFile, $currentArchiveEntryFileInfo.FullName, $currentArchiveEntryFileInfo.FullName) + $errorRecord = CreateErrorRecordHelper "ExpandArchiveFileExists" $errorMessage ([System.Management.Automation.ErrorCategory]::InvalidOperation) $null $currentArchiveEntryFileInfo.FullName + Write-Error -ErrorRecord $errorRecord + $hasNonTerminatingError = $true + } + } + + if (!$hasNonTerminatingError) { + [System.IO.Compression.ZipFileExtensions]::ExtractToFile($currentArchiveEntry, $currentArchiveEntryPath, $false) + + # Add the expanded file path to the $expandedItems array, + # to keep track of all the expanded files created while expanding the archive file. + # If user enters CTRL + C then at that point of time, all these expanded files + # would be deleted as part of the clean up process. + $expandedItems.Value += $currentArchiveEntryPath + + $addFiletoExpandedPathMessage = ($LocalizedData.CreateFileAtExpandedPath -f $currentArchiveEntryPath) + Write-Verbose $addFiletoExpandedPathMessage + } + } finally { + If ($null -ne $destStream) { + $destStream.Dispose() + } + + If ($null -ne $srcStream) { + $srcStream.Dispose() + } + } + } + + $currentEntryCount += 1 + # $currentSegmentWeight is Set to 100 giving equal weightage to each file that is getting expanded. + # $previousSegmentWeight is set to 0 as there are no prior segments. + $previousSegmentWeight = 0 + $currentSegmentWeight = 100 + ProgressBarHelper "Expand-Archive" $progressBarStatus $previousSegmentWeight $currentSegmentWeight $zipArchive.Entries.Count $currentEntryCount + } + } finally { + If ($null -ne $zipArchive) { + $zipArchive.Dispose() + } + + If ($null -ne $archiveFileStream) { + $archiveFileStream.Dispose() + } + + # Complete writing progress. + Write-Progress -Activity "Expand-Archive" -Completed + } + } + + <############################################################################################ + # ProgressBarHelper: This is a helper function used to display progress message. + # This function is used by both Compress-Archive & Expand-Archive to display archive file + # creation/expansion progress. + ############################################################################################> + function ProgressBarHelper { + param + ( + [string] + $cmdletName, + + [string] + $status, + + [double] + $previousSegmentWeight, + + [double] + $currentSegmentWeight, + + [int] + $totalNumberofEntries, + + [int] + $currentEntryCount + ) + + if ($currentEntryCount -gt 0 -and + $totalNumberofEntries -gt 0 -and + $previousSegmentWeight -ge 0 -and + $currentSegmentWeight -gt 0) { + $entryDefaultWeight = $currentSegmentWeight / [double]$totalNumberofEntries + + $percentComplete = $previousSegmentWeight + ($entryDefaultWeight * $currentEntryCount) + Write-Progress -Activity $cmdletName -Status $status -PercentComplete $percentComplete + } + } + + <############################################################################################ + # CSVHelper: This is a helper function used to append comma after each path specifid by + # the SourcePath array. This helper function is used to display all the user supplied paths + # in the WhatIf message. + ############################################################################################> + function CSVHelper { + param + ( + [string[]] + $sourcePath + ) + + # SourcePath has already been validated by the calling funcation. + if ($sourcePath.Count -gt 1) { + $sourcePathInCsvFormat = "`n" + for ($currentIndex = 0; $currentIndex -lt $sourcePath.Count; $currentIndex++) { + if ($currentIndex -eq $sourcePath.Count - 1) { + $sourcePathInCsvFormat += $sourcePath[$currentIndex] + } else { + $sourcePathInCsvFormat += $sourcePath[$currentIndex] + "`n" + } + } + } else { + $sourcePathInCsvFormat = $sourcePath + } + + return $sourcePathInCsvFormat + } + + <############################################################################################ + # ThrowTerminatingErrorHelper: This is a helper function used to throw terminating error. + ############################################################################################> + function ThrowTerminatingErrorHelper { + param + ( + [string] + $errorId, + + [string] + $errorMessage, + + [System.Management.Automation.ErrorCategory] + $errorCategory, + + [object] + $targetObject, + + [Exception] + $innerException + ) + + if ($innerException -eq $null) { + $exception = New-object System.IO.IOException $errorMessage + } else { + $exception = New-Object System.IO.IOException $errorMessage, $innerException + } + + $exception = New-Object System.IO.IOException $errorMessage + $errorRecord = New-Object System.Management.Automation.ErrorRecord $exception, $errorId, $errorCategory, $targetObject + $PSCmdlet.ThrowTerminatingError($errorRecord) + } + + <############################################################################################ + # CreateErrorRecordHelper: This is a helper function used to create an ErrorRecord + ############################################################################################> + function CreateErrorRecordHelper { + param + ( + [string] + $errorId, + + [string] + $errorMessage, + + [System.Management.Automation.ErrorCategory] + $errorCategory, + + [Exception] + $exception, + + [object] + $targetObject + ) + + if ($null -eq $exception) { + $exception = New-Object System.IO.IOException $errorMessage + } + + $errorRecord = New-Object System.Management.Automation.ErrorRecord $exception, $errorId, $errorCategory, $targetObject + return $errorRecord + } + #endregion Utility Functions + + $inputPaths = @() + $destinationParentDir = [system.IO.Path]::GetDirectoryName($DestinationPath) + if ($null -eq $destinationParentDir) { + $errorMessage = ($LocalizedData.InvalidDestinationPath -f $DestinationPath) + ThrowTerminatingErrorHelper "InvalidArchiveFilePath" $errorMessage ([System.Management.Automation.ErrorCategory]::InvalidArgument) $DestinationPath + } + + if ($destinationParentDir -eq [string]::Empty) { + $destinationParentDir = '.' + } + + $achiveFileName = [system.IO.Path]::GetFileName($DestinationPath) + $destinationParentDir = GetResolvedPathHelper $destinationParentDir $false $PSCmdlet + + if ($destinationParentDir.Count -gt 1) { + $errorMessage = ($LocalizedData.InvalidArchiveFilePathError -f $DestinationPath, "DestinationPath", "DestinationPath") + ThrowTerminatingErrorHelper "InvalidArchiveFilePath" $errorMessage ([System.Management.Automation.ErrorCategory]::InvalidArgument) $DestinationPath + } + + IsValidFileSystemPath $destinationParentDir | Out-Null + $DestinationPath = Join-Path -Path $destinationParentDir -ChildPath $achiveFileName + + # GetExtension API does not validate for the actual existance of the path. + $extension = [system.IO.Path]::GetExtension($DestinationPath) + + # If user does not specify .Zip extension, we append it. + If ($extension -eq [string]::Empty) { + $DestinationPathWithOutExtension = $DestinationPath + $DestinationPath = $DestinationPathWithOutExtension + $zipFileExtension + $appendArchiveFileExtensionMessage = ($LocalizedData.AppendArchiveFileExtensionMessage -f $DestinationPathWithOutExtension, $DestinationPath) + Write-Verbose $appendArchiveFileExtensionMessage + } else { + # Invalid file extension is specified for the zip file to be created. + if ($extension -ne $zipFileExtension) { + $errorMessage = ($LocalizedData.InvalidZipFileExtensionError -f $extension, $zipFileExtension) + ThrowTerminatingErrorHelper "NotSupportedArchiveFileExtension" $errorMessage ([System.Management.Automation.ErrorCategory]::InvalidArgument) $extension + } + } + + $archiveFileExist = Test-Path -LiteralPath $DestinationPath -PathType Leaf + + if ($archiveFileExist -and ($Update -eq $false -and $Force -eq $false)) { + $errorMessage = ($LocalizedData.ZipFileExistError -f $DestinationPath) + ThrowTerminatingErrorHelper "ArchiveFileExists" $errorMessage ([System.Management.Automation.ErrorCategory]::InvalidArgument) $DestinationPath + } + + # If archive file already exists and if -Update is specified, then we check to see + # if we have write access permission to update the existing archive file. + if ($archiveFileExist -and $Update -eq $true) { + $item = Get-Item -Path $DestinationPath + if ($item.Attributes.ToString().Contains("ReadOnly")) { + $errorMessage = ($LocalizedData.ArchiveFileIsReadOnly -f $DestinationPath) + ThrowTerminatingErrorHelper "ArchiveFileIsReadOnly" $errorMessage ([System.Management.Automation.ErrorCategory]::InvalidOperation) $DestinationPath + } + } + + $isWhatIf = $psboundparameters.ContainsKey("WhatIf") + if (!$isWhatIf) { + $preparingToCompressVerboseMessage = ($LocalizedData.PreparingToCompressVerboseMessage) + Write-Verbose $preparingToCompressVerboseMessage + + $progressBarStatus = ($LocalizedData.CompressProgressBarText -f $DestinationPath) + ProgressBarHelper "Compress-Archive" $progressBarStatus 0 100 100 1 + } + } + PROCESS { + if ($PsCmdlet.ParameterSetName -eq "Path" -or + $PsCmdlet.ParameterSetName -eq "PathWithForce" -or + $PsCmdlet.ParameterSetName -eq "PathWithUpdate") { + $inputPaths += $Path + } + + if ($PsCmdlet.ParameterSetName -eq "LiteralPath" -or + $PsCmdlet.ParameterSetName -eq "LiteralPathWithForce" -or + $PsCmdlet.ParameterSetName -eq "LiteralPathWithUpdate") { + $inputPaths += $LiteralPath + } + } + END { + # If archive file already exists and if -Force is specified, we delete the + # existing artchive file and create a brand new one. + if (($PsCmdlet.ParameterSetName -eq "PathWithForce" -or + $PsCmdlet.ParameterSetName -eq "LiteralPathWithForce") -and $archiveFileExist) { + Remove-Item -Path $DestinationPath -Force -ErrorAction Stop + } + + # Validate Source Path depeding on parameter set being used. + # The specified source path conatins one or more files or directories that needs + # to be compressed. + $isLiteralPathUsed = $false + if ($PsCmdlet.ParameterSetName -eq "LiteralPath" -or + $PsCmdlet.ParameterSetName -eq "LiteralPathWithForce" -or + $PsCmdlet.ParameterSetName -eq "LiteralPathWithUpdate") { + $isLiteralPathUsed = $true + } + + ValidateDuplicateFileSystemPath $PsCmdlet.ParameterSetName $inputPaths + $resolvedPaths = GetResolvedPathHelper $inputPaths $isLiteralPathUsed $PSCmdlet + IsValidFileSystemPath $resolvedPaths | Out-Null + + $sourcePath = $resolvedPaths; + + # CSVHelper: This is a helper function used to append comma after each path specifid by + # the $sourcePath array. The comma saperated paths are displayed in the -WhatIf message. + $sourcePathInCsvFormat = CSVHelper $sourcePath + if ($pscmdlet.ShouldProcess($sourcePathInCsvFormat)) { + try { + # StopProcessing is not avaliable in Script cmdlets. However the pipleline execution + # is terminated when ever 'CTRL + C' is entered by user to terminate the cmdlet execution. + # The finally block is executed whenever pipleline is terminated. + # $isArchiveFileProcessingComplete variable is used to track if 'CTRL + C' is entered by the + # user. + $isArchiveFileProcessingComplete = $false + + $numberOfItemsArchived = CompressArchiveHelper $sourcePath $DestinationPath $CompressionLevel $Update + + $isArchiveFileProcessingComplete = $true + } finally { + # The $isArchiveFileProcessingComplete would be set to $false if user has typed 'CTRL + C' to + # terminate the cmdlet execution or if an unhandled exception is thrown. + # $numberOfItemsArchived contains the count of number of files or directories add to the archive file. + # If the newly created archive file is empty then we delete it as its not usable. + if (($isArchiveFileProcessingComplete -eq $false) -or + ($numberOfItemsArchived -eq 0)) { + $DeleteArchiveFileMessage = ($LocalizedData.DeleteArchiveFile -f $DestinationPath) + Write-Verbose $DeleteArchiveFileMessage + + # delete the partial archive file created. + if (Test-Path $DestinationPath) { + Remove-Item -LiteralPath $DestinationPath -Force -Recurse -ErrorAction SilentlyContinue + } + } + } + } + } } -function Expand-Archive -{ - <# - .SYNOPSIS - Extracts files from a specified archive (zipped) file. - - .DESCRIPTION - The Expand-Archive cmdlet extracts files from a specified zipped archive file to a specified destination folder. An archive file allows multiple files to be packaged, and optionally compressed, into a single zipped file for easier distribution and storage. - - .PARAMETER Path - Specifies the path to the archive file. - - .PARAMETER LiteralPath - Specifies the path to an archive file. Unlike the Path parameter, the value of LiteralPath is used exactly as it is typed. Wildcard characters are not supported. If the path includes escape characters, enclose each escape character in single quotation marks, to instruct Windows PowerShell not to interpret any characters as escape sequences. - - .PARAMETER DestinationPath - Specifies the path to the folder in which you want the command to save extracted files. Enter the path to a folder, but do not specify a file name or file name extension. This parameter is required. - - .PARAMETER Force - Forces the command to run without asking for user confirmation. - - .PARAMETER Confirm - Prompts you for confirmation before running the cmdlet. - - .PARAMETER WhatIf - Shows what would happen if the cmdlet runs. The cmdlet is not run. - - .EXAMPLE - Example 1: Extract the contents of an archive - - PS C:\>Expand-Archive -LiteralPath C:\Archives\Draft.Zip -DestinationPath C:\Reference - - This command extracts the contents of an existing archive file, Draft.zip, into the folder specified by the DestinationPath parameter, C:\Reference. - - .EXAMPLE - Example 2: Extract the contents of an archive in the current folder - - PS C:\>Expand-Archive -Path Draft.Zip -DestinationPath C:\Reference - - This command extracts the contents of an existing archive file in the current folder, Draft.zip, into the folder specified by the DestinationPath parameter, C:\Reference. - #> - [CmdletBinding( - DefaultParameterSetName = "Path", - SupportsShouldProcess = $true, - HelpUri = "http://go.microsoft.com/fwlink/?LinkID=393253")] - param - ( - [parameter ( - mandatory = $true, - Position = 0, - ParameterSetName = "Path", - ValueFromPipeline = $true, - ValueFromPipelineByPropertyName = $true)] - [ValidateNotNullOrEmpty()] - [string] - $Path, - - [parameter ( - mandatory = $true, - ParameterSetName = "LiteralPath", - ValueFromPipelineByPropertyName = $true)] - [ValidateNotNullOrEmpty()] - [Alias("PSPath")] - [string] - $LiteralPath, - - [parameter (mandatory = $false, - Position = 1, - ValueFromPipeline = $false, - ValueFromPipelineByPropertyName = $false)] - [ValidateNotNullOrEmpty()] - [string] - $DestinationPath, - - [parameter (mandatory = $false, - ValueFromPipeline = $false, - ValueFromPipelineByPropertyName = $false)] - [switch] - $Force - ) - - BEGIN - { - Add-Type -AssemblyName System.IO.Compression -ErrorAction Ignore - Add-Type -AssemblyName System.IO.Compression.FileSystem -ErrorAction Ignore - - $zipFileExtension = ".zip" - - $LocalizedData = ConvertFrom-StringData @' +function Expand-Archive { + <# + .SYNOPSIS + Extracts files from a specified archive (zipped) file. + + .DESCRIPTION + The Expand-Archive cmdlet extracts files from a specified zipped archive file to a specified destination folder. An archive file allows multiple files to be packaged, and optionally compressed, into a single zipped file for easier distribution and storage. + + .PARAMETER Path + Specifies the path to the archive file. + + .PARAMETER LiteralPath + Specifies the path to an archive file. Unlike the Path parameter, the value of LiteralPath is used exactly as it is typed. Wildcard characters are not supported. If the path includes escape characters, enclose each escape character in single quotation marks, to instruct Windows PowerShell not to interpret any characters as escape sequences. + + .PARAMETER DestinationPath + Specifies the path to the folder in which you want the command to save extracted files. Enter the path to a folder, but do not specify a file name or file name extension. This parameter is required. + + .PARAMETER Force + Forces the command to run without asking for user confirmation. + + .PARAMETER Confirm + Prompts you for confirmation before running the cmdlet. + + .PARAMETER WhatIf + Shows what would happen if the cmdlet runs. The cmdlet is not run. + + .EXAMPLE + Example 1: Extract the contents of an archive + + PS C:\>Expand-Archive -LiteralPath C:\Archives\Draft.Zip -DestinationPath C:\Reference + + This command extracts the contents of an existing archive file, Draft.zip, into the folder specified by the DestinationPath parameter, C:\Reference. + + .EXAMPLE + Example 2: Extract the contents of an archive in the current folder + + PS C:\>Expand-Archive -Path Draft.Zip -DestinationPath C:\Reference + + This command extracts the contents of an existing archive file in the current folder, Draft.zip, into the folder specified by the DestinationPath parameter, C:\Reference. + #> + [CmdletBinding( + DefaultParameterSetName = "Path", + SupportsShouldProcess = $true, + HelpUri = "http://go.microsoft.com/fwlink/?LinkID=393253")] + param + ( + [parameter ( + mandatory = $true, + Position = 0, + ParameterSetName = "Path", + ValueFromPipeline = $true, + ValueFromPipelineByPropertyName = $true)] + [ValidateNotNullOrEmpty()] + [string] + $Path, + + [parameter ( + mandatory = $true, + ParameterSetName = "LiteralPath", + ValueFromPipelineByPropertyName = $true)] + [ValidateNotNullOrEmpty()] + [Alias("PSPath")] + [string] + $LiteralPath, + + [parameter (mandatory = $false, + Position = 1, + ValueFromPipeline = $false, + ValueFromPipelineByPropertyName = $false)] + [ValidateNotNullOrEmpty()] + [string] + $DestinationPath, + + [parameter (mandatory = $false, + ValueFromPipeline = $false, + ValueFromPipelineByPropertyName = $false)] + [switch] + $Force + ) + + BEGIN { + Add-Type -AssemblyName System.IO.Compression -ErrorAction Ignore + Add-Type -AssemblyName System.IO.Compression.FileSystem -ErrorAction Ignore + + $zipFileExtension = ".zip" + + $LocalizedData = ConvertFrom-StringData @' PathNotFoundError=The path '{0}' either does not exist or is not a valid file system path. ExpandArchiveInValidDestinationPath=The path '{0}' is not a valid file system directory path. InvalidZipFileExtensionError={0} is not a supported archive file format. {1} is the only supported archive file format. @@ -1350,1017 +1197,867 @@ PreparingToCompressVerboseMessage=Preparing to compress... PreparingToExpandVerboseMessage=Preparing to expand... '@ - #region Utility Functions - function GetResolvedPathHelper - { - param - ( - [string[]] - $path, - - [boolean] - $isLiteralPath, - - [System.Management.Automation.PSCmdlet] - $callerPSCmdlet - ) - - $resolvedPaths = @() - - # null and empty check are are already done on Path parameter at the cmdlet layer. - foreach ($currentPath in $path) - { - try - { - if ($isLiteralPath) - { - $currentResolvedPaths = Resolve-Path -LiteralPath $currentPath -ErrorAction Stop - } - else - { - $currentResolvedPaths = Resolve-Path -Path $currentPath -ErrorAction Stop - } - } - catch - { - $errorMessage = ($LocalizedData.PathNotFoundError -f $currentPath) - $exception = New-Object System.InvalidOperationException $errorMessage, $_.Exception - $errorRecord = CreateErrorRecordHelper "ArchiveCmdletPathNotFound" $null ([System.Management.Automation.ErrorCategory]::InvalidArgument) $exception $currentPath - $callerPSCmdlet.ThrowTerminatingError($errorRecord) - } - - foreach ($currentResolvedPath in $currentResolvedPaths) - { - $resolvedPaths += $currentResolvedPath.ProviderPath - } - } - - $resolvedPaths - } - - function Add-CompressionAssemblies - { - - if ($PSEdition -eq "Desktop") - { - Add-Type -AssemblyName System.IO.Compression - Add-Type -AssemblyName System.IO.Compression.FileSystem - } - } - - function IsValidFileSystemPath - { - param - ( - [string[]] - $path - ) - - $result = $true; - - # null and empty check are are already done on Path parameter at the cmdlet layer. - foreach ($currentPath in $path) - { - if (!([System.IO.File]::Exists($currentPath) -or [System.IO.Directory]::Exists($currentPath))) - { - $errorMessage = ($LocalizedData.PathNotFoundError -f $currentPath) - ThrowTerminatingErrorHelper "PathNotFound" $errorMessage ([System.Management.Automation.ErrorCategory]::InvalidArgument) $currentPath - } - } - - return $result; - } - - - function ValidateDuplicateFileSystemPath - { - param - ( - [string] - $inputParameter, - - [string[]] - $path - ) - - $uniqueInputPaths = @() - - # null and empty check are are already done on Path parameter at the cmdlet layer. - foreach ($currentPath in $path) - { - $currentInputPath = $currentPath.ToUpper() - if ($uniqueInputPaths.Contains($currentInputPath)) - { - $errorMessage = ($LocalizedData.DuplicatePathFoundError -f $inputParameter, $currentPath, $inputParameter) - ThrowTerminatingErrorHelper "DuplicatePathFound" $errorMessage ([System.Management.Automation.ErrorCategory]::InvalidArgument) $currentPath - } - else - { - $uniqueInputPaths += $currentInputPath - } - } - } - - function CompressionLevelMapper - { - param - ( - [string] - $compressionLevel - ) - - $compressionLevelFormat = [System.IO.Compression.CompressionLevel]::Optimal - - # CompressionLevel format is already validated at the cmdlet layer. - switch ($compressionLevel.ToString()) - { - "Fastest" - { - $compressionLevelFormat = [System.IO.Compression.CompressionLevel]::Fastest - } - "NoCompression" - { - $compressionLevelFormat = [System.IO.Compression.CompressionLevel]::NoCompression - } - } - - return $compressionLevelFormat - } - - function CompressArchiveHelper - { - param - ( - [string[]] - $sourcePath, - - [string] - $destinationPath, - - [string] - $compressionLevel, - - [bool] - $isUpdateMode - ) - - $numberOfItemsArchived = 0 - $sourceFilePaths = @() - $sourceDirPaths = @() - - foreach ($currentPath in $sourcePath) - { - $result = Test-Path -LiteralPath $currentPath -PathType Leaf - if ($result -eq $true) - { - $sourceFilePaths += $currentPath - } - else - { - $sourceDirPaths += $currentPath - } - } - - # The Soure Path contains one or more directory (this directory can have files under it) and no files to be compressed. - if ($sourceFilePaths.Count -eq 0 -and $sourceDirPaths.Count -gt 0) - { - $currentSegmentWeight = 100/[double]$sourceDirPaths.Count - $previousSegmentWeight = 0 - foreach ($currentSourceDirPath in $sourceDirPaths) - { - $count = CompressSingleDirHelper $currentSourceDirPath $destinationPath $compressionLevel $true $isUpdateMode $previousSegmentWeight $currentSegmentWeight - $numberOfItemsArchived += $count - $previousSegmentWeight += $currentSegmentWeight - } - } - - # The Soure Path contains only files to be compressed. - elseIf ($sourceFilePaths.Count -gt 0 -and $sourceDirPaths.Count -eq 0) - { - # $previousSegmentWeight is equal to 0 as there are no prior segments. - # $currentSegmentWeight is set to 100 as all files have equal weightage. - $previousSegmentWeight = 0 - $currentSegmentWeight = 100 - - $numberOfItemsArchived = CompressFilesHelper $sourceFilePaths $destinationPath $compressionLevel $isUpdateMode $previousSegmentWeight $currentSegmentWeight - } - # The Soure Path contains one or more files and one or more directories (this directory can have files under it) to be compressed. - elseif ($sourceFilePaths.Count -gt 0 -and $sourceDirPaths.Count -gt 0) - { - # each directory is considered as an individual segments & all the individual files are clubed in to a separate sgemnet. - $currentSegmentWeight = 100/[double]($sourceDirPaths.Count + 1) - $previousSegmentWeight = 0 - - foreach ($currentSourceDirPath in $sourceDirPaths) - { - $count = CompressSingleDirHelper $currentSourceDirPath $destinationPath $compressionLevel $true $isUpdateMode $previousSegmentWeight $currentSegmentWeight - $numberOfItemsArchived += $count - $previousSegmentWeight += $currentSegmentWeight - } - - $count = CompressFilesHelper $sourceFilePaths $destinationPath $compressionLevel $isUpdateMode $previousSegmentWeight $currentSegmentWeight - $numberOfItemsArchived += $count - } - - return $numberOfItemsArchived - } - - function CompressFilesHelper - { - param - ( - [string[]] - $sourceFilePaths, - - [string] - $destinationPath, - - [string] - $compressionLevel, - - [bool] - $isUpdateMode, - - [double] - $previousSegmentWeight, - - [double] - $currentSegmentWeight - ) - - $numberOfItemsArchived = ZipArchiveHelper $sourceFilePaths $destinationPath $compressionLevel $isUpdateMode $null $previousSegmentWeight $currentSegmentWeight - - return $numberOfItemsArchived - } - - function CompressSingleDirHelper - { - param - ( - [string] - $sourceDirPath, - - [string] - $destinationPath, - - [string] - $compressionLevel, - - [bool] - $useParentDirAsRoot, - - [bool] - $isUpdateMode, - - [double] - $previousSegmentWeight, - - [double] - $currentSegmentWeight - ) - - [System.Collections.Generic.List[System.String]]$subDirFiles = @() - - if ($useParentDirAsRoot) - { - $sourceDirInfo = New-Object -TypeName System.IO.DirectoryInfo -ArgumentList $sourceDirPath - $sourceDirFullName = $sourceDirInfo.Parent.FullName - - # If the directory is present at the drive level the DirectoryInfo.Parent include '\' example: C:\ - # On the other hand if the directory exists at a deper level then DirectoryInfo.Parent - # has just the path (without an ending '\'). example C:\source - if ($sourceDirFullName.Length -eq 3) - { - $modifiedSourceDirFullName = $sourceDirFullName - } - else - { - $modifiedSourceDirFullName = $sourceDirFullName + "\" - } - } - else - { - $sourceDirFullName = $sourceDirPath - $modifiedSourceDirFullName = $sourceDirFullName + "\" - } - - $dirContents = Get-ChildItem -LiteralPath $sourceDirPath -Recurse - foreach ($currentContent in $dirContents) - { - $isContainer = $currentContent -is [System.IO.DirectoryInfo] - if (!$isContainer) - { - $subDirFiles.Add($currentContent.FullName) - } - else - { - # The currentContent points to a directory. - # We need to check if the directory is an empty directory, if so such a - # directory has to be explictly added to the archive file. - # if there are no files in the directory the GetFiles() API returns an empty array. - $files = $currentContent.GetFiles() - if ($files.Count -eq 0) - { - $subDirFiles.Add($currentContent.FullName + "\") - } - } - } - - $numberOfItemsArchived = ZipArchiveHelper $subDirFiles.ToArray() $destinationPath $compressionLevel $isUpdateMode $modifiedSourceDirFullName $previousSegmentWeight $currentSegmentWeight - - return $numberOfItemsArchived - } - - function ZipArchiveHelper - { - param - ( - [System.Collections.Generic.List[System.String]] - $sourcePaths, - - [string] - $destinationPath, - - [string] - $compressionLevel, - - [bool] - $isUpdateMode, - - [string] - $modifiedSourceDirFullName, - - [double] - $previousSegmentWeight, - - [double] - $currentSegmentWeight - ) - - $numberOfItemsArchived = 0 - $fileMode = [System.IO.FileMode]::Create - $result = Test-Path -LiteralPath $DestinationPath -PathType Leaf - if ($result -eq $true) - { - $fileMode = [System.IO.FileMode]::Open - } - - Add-CompressionAssemblies - - try - { - # At this point we are sure that the archive file has write access. - $archiveFileStreamArgs = @($destinationPath, $fileMode) - $archiveFileStream = New-Object -TypeName System.IO.FileStream -ArgumentList $archiveFileStreamArgs - - $zipArchiveArgs = @($archiveFileStream, [System.IO.Compression.ZipArchiveMode]::Update, $false) - $zipArchive = New-Object -TypeName System.IO.Compression.ZipArchive -ArgumentList $zipArchiveArgs - - $currentEntryCount = 0 - $progressBarStatus = ($LocalizedData.CompressProgressBarText -f $destinationPath) - $bufferSize = 4kb - $buffer = New-Object Byte[] $bufferSize - - foreach ($currentFilePath in $sourcePaths) - { - if ($modifiedSourceDirFullName -ne $null -and $modifiedSourceDirFullName.Length -gt 0) - { - $index = $currentFilePath.IndexOf($modifiedSourceDirFullName, [System.StringComparison]::OrdinalIgnoreCase) - $currentFilePathSubString = $currentFilePath.Substring($index, $modifiedSourceDirFullName.Length) - $relativeFilePath = $currentFilePath.Replace($currentFilePathSubString, "").Trim() - } - else - { - $relativeFilePath = [System.IO.Path]::GetFileName($currentFilePath) - } - - # Update mode is selected. - # Check to see if archive file already contains one or more zip files in it. - if ($isUpdateMode -eq $true -and $zipArchive.Entries.Count -gt 0) - { - $entryToBeUpdated = $null - - # Check if the file already exists in the archive file. - # If so replace it with new file from the input source. - # If the file does not exist in the archive file then default to - # create mode and create the entry in the archive file. - - foreach ($currentArchiveEntry in $zipArchive.Entries) - { - if ($currentArchiveEntry.FullName -eq $relativeFilePath) - { - $entryToBeUpdated = $currentArchiveEntry - break - } - } - - if ($entryToBeUpdated -ne $null) - { - $addItemtoArchiveFileMessage = ($LocalizedData.AddItemtoArchiveFile -f $currentFilePath) - $entryToBeUpdated.Delete() - } - } - - $compression = CompressionLevelMapper $compressionLevel - - # If a directory needs to be added to an archive file, - # by convention the .Net API's expect the path of the diretcory - # to end with '\' to detect the path as an directory. - if (!$relativeFilePath.EndsWith("\", [StringComparison]::OrdinalIgnoreCase)) - { - try - { - try - { - $currentFileStream = [System.IO.File]::Open($currentFilePath, [System.IO.FileMode]::Open, [System.IO.FileAccess]::Read) - } - catch - { - # Failed to access the file. Write a non terminating error to the pipeline - # and move on with the remaining files. - $exception = $_.Exception - if ($null -ne $_.Exception -and - $null -ne $_.Exception.InnerException) - { - $exception = $_.Exception.InnerException - } - $errorRecord = CreateErrorRecordHelper "CompressArchiveUnauthorizedAccessError" $null ([System.Management.Automation.ErrorCategory]::PermissionDenied) $exception $currentFilePath - Write-Error -ErrorRecord $errorRecord - } - - if ($null -ne $currentFileStream) - { - $srcStream = New-Object System.IO.BinaryReader $currentFileStream - - $currentArchiveEntry = $zipArchive.CreateEntry($relativeFilePath, $compression) - - # Updating the File Creation time so that the same timestamp would be retained after expanding the compressed file. - # At this point we are sure that Get-ChildItem would succeed. - $currentArchiveEntry.LastWriteTime = (Get-Item -LiteralPath $currentFilePath).LastWriteTime - - $destStream = New-Object System.IO.BinaryWriter $currentArchiveEntry.Open() - - while ($numberOfBytesRead = $srcStream.Read($buffer, 0, $bufferSize)) - { - $destStream.Write($buffer, 0, $numberOfBytesRead) - $destStream.Flush() - } - - $numberOfItemsArchived += 1 - $addItemtoArchiveFileMessage = ($LocalizedData.AddItemtoArchiveFile -f $currentFilePath) - } - } - finally - { - If ($null -ne $currentFileStream) - { - $currentFileStream.Dispose() - } - If ($null -ne $srcStream) - { - $srcStream.Dispose() - } - If ($null -ne $destStream) - { - $destStream.Dispose() - } - } - } - else - { - $currentArchiveEntry = $zipArchive.CreateEntry("$relativeFilePath", $compression) - $numberOfItemsArchived += 1 - $addItemtoArchiveFileMessage = ($LocalizedData.AddItemtoArchiveFile -f $currentFilePath) - } - - if ($null -ne $addItemtoArchiveFileMessage) - { - Write-Verbose $addItemtoArchiveFileMessage - } - - $currentEntryCount += 1 - ProgressBarHelper "Compress-Archive" $progressBarStatus $previousSegmentWeight $currentSegmentWeight $sourcePaths.Count $currentEntryCount - } - } - finally - { - If ($null -ne $zipArchive) - { - $zipArchive.Dispose() - } - - If ($null -ne $archiveFileStream) - { - $archiveFileStream.Dispose() - } - - # Complete writing progress. - Write-Progress -Activity "Compress-Archive" -Completed - } - - return $numberOfItemsArchived - } - -<############################################################################################ -# ValidateArchivePathHelper: This is a helper function used to validate the archive file -# path & its file format. The only supported archive file format is .zip -############################################################################################> - function ValidateArchivePathHelper - { - param - ( - [string] - $archiveFile - ) - - if ([System.IO.File]::Exists($archiveFile)) - { - $extension = [system.IO.Path]::GetExtension($archiveFile) - - # Invalid file extension is specifed for the zip file. - if ($extension -ne $zipFileExtension) - { - $errorMessage = ($LocalizedData.InvalidZipFileExtensionError -f $extension, $zipFileExtension) - ThrowTerminatingErrorHelper "NotSupportedArchiveFileExtension" $errorMessage ([System.Management.Automation.ErrorCategory]::InvalidArgument) $extension - } - } - else - { - $errorMessage = ($LocalizedData.PathNotFoundError -f $archiveFile) - ThrowTerminatingErrorHelper "PathNotFound" $errorMessage ([System.Management.Automation.ErrorCategory]::InvalidArgument) $archiveFile - } - } - -<############################################################################################ -# ExpandArchiveHelper: This is a helper function used to expand the archive file contents -# to the specified directory. -############################################################################################> - function ExpandArchiveHelper - { - param - ( - [string] - $archiveFile, - - [string] - $expandedDir, - - [ref] - $expandedItems, - - [boolean] - $force, - - [boolean] - $isVerbose, - - [boolean] - $isConfirm - ) - - Add-CompressionAssemblies - - try - { - # The existance of archive file has already been validated by ValidateArchivePathHelper - # before calling this helper function. - $archiveFileStreamArgs = @($archiveFile, [System.IO.FileMode]::Open, [System.IO.FileAccess]::Read) - $archiveFileStream = New-Object -TypeName System.IO.FileStream -ArgumentList $archiveFileStreamArgs - - $zipArchiveArgs = @($archiveFileStream, [System.IO.Compression.ZipArchiveMode]::Read, $false) - $zipArchive = New-Object -TypeName System.IO.Compression.ZipArchive -ArgumentList $zipArchiveArgs - - if ($zipArchive.Entries.Count -eq 0) - { - $archiveFileIsEmpty = ($LocalizedData.ArchiveFileIsEmpty -f $archiveFile) - Write-Verbose $archiveFileIsEmpty - return - } - - $currentEntryCount = 0 - $progressBarStatus = ($LocalizedData.ExpandProgressBarText -f $archiveFile) - - # The archive entries can either be empty directories or files. - foreach ($currentArchiveEntry in $zipArchive.Entries) - { - $currentArchiveEntryPath = Join-Path -Path $expandedDir -ChildPath $currentArchiveEntry.FullName - $extension = [system.IO.Path]::GetExtension($currentArchiveEntryPath) - - # The current archive entry is an empty directory - # The FullName of the Archive Entry representing a directory would end with a trailing '\'. - if ($extension -eq [string]::Empty -and - $currentArchiveEntryPath.EndsWith("\", [StringComparison]::OrdinalIgnoreCase)) - { - $pathExists = Test-Path -LiteralPath $currentArchiveEntryPath - - # The current archive entry expects an empty directory. - # Check if the existing directory is empty. If its not empty - # then it means that user has added this directory by other means. - if ($pathExists -eq $false) - { - New-Item $currentArchiveEntryPath -ItemType Directory -Confirm:$isConfirm | Out-Null - - if (Test-Path -LiteralPath $currentArchiveEntryPath -PathType Container) - { - $addEmptyDirectorytoExpandedPathMessage = ($LocalizedData.AddItemtoArchiveFile -f $currentArchiveEntryPath) - Write-Verbose $addEmptyDirectorytoExpandedPathMessage - - $expandedItems.Value += $currentArchiveEntryPath - } - } - } - else - { - try - { - $currentArchiveEntryFileInfo = New-Object -TypeName System.IO.FileInfo -ArgumentList $currentArchiveEntryPath - $parentDirExists = Test-Path -LiteralPath $currentArchiveEntryFileInfo.DirectoryName -PathType Container - - # If the Parent directory of the current entry in the archive file does not exist, then create it. - if ($parentDirExists -eq $false) - { - New-Item $currentArchiveEntryFileInfo.DirectoryName -ItemType Directory -Confirm:$isConfirm | Out-Null - - if (!(Test-Path -LiteralPath $currentArchiveEntryFileInfo.DirectoryName -PathType Container)) - { - # The directory referred by $currentArchiveEntryFileInfo.DirectoryName was not successfully created. - # This could be because the user has specified -Confirm paramter when Expand-Archive was invoked - # and authorization was not provided when confirmation was prompted. In such a scenario, - # we skip the current file in the archive and continue with the remaining archive file contents. - Continue - } - - $expandedItems.Value += $currentArchiveEntryFileInfo.DirectoryName - } - - $hasNonTerminatingError = $false - - # Check if the file in to which the current archive entry contents - # would be expanded already exists. - if ($currentArchiveEntryFileInfo.Exists) - { - if ($force) - { - Remove-Item -LiteralPath $currentArchiveEntryFileInfo.FullName -Force -ErrorVariable ev -Verbose:$isVerbose -Confirm:$isConfirm - if ($ev -ne $null) - { - $hasNonTerminatingError = $true - } - - if (Test-Path -LiteralPath $currentArchiveEntryFileInfo.FullName -PathType Leaf) - { - # The file referred by $currentArchiveEntryFileInfo.FullName was not successfully removed. - # This could be because the user has specified -Confirm paramter when Expand-Archive was invoked - # and authorization was not provided when confirmation was prompted. In such a scenario, - # we skip the current file in the archive and continue with the remaining archive file contents. - Continue - } - } - else - { - # Write non-terminating error to the pipeline. - $errorMessage = ($LocalizedData.FileExistsError -f $currentArchiveEntryFileInfo.FullName, $archiveFile, $currentArchiveEntryFileInfo.FullName, $currentArchiveEntryFileInfo.FullName) - $errorRecord = CreateErrorRecordHelper "ExpandArchiveFileExists" $errorMessage ([System.Management.Automation.ErrorCategory]::InvalidOperation) $null $currentArchiveEntryFileInfo.FullName - Write-Error -ErrorRecord $errorRecord - $hasNonTerminatingError = $true - } - } - - if (!$hasNonTerminatingError) - { - [System.IO.Compression.ZipFileExtensions]::ExtractToFile($currentArchiveEntry, $currentArchiveEntryPath, $false) - - # Add the expanded file path to the $expandedItems array, - # to keep track of all the expanded files created while expanding the archive file. - # If user enters CTRL + C then at that point of time, all these expanded files - # would be deleted as part of the clean up process. - $expandedItems.Value += $currentArchiveEntryPath - - $addFiletoExpandedPathMessage = ($LocalizedData.CreateFileAtExpandedPath -f $currentArchiveEntryPath) - Write-Verbose $addFiletoExpandedPathMessage - } - } - finally - { - If ($null -ne $destStream) - { - $destStream.Dispose() - } - - If ($null -ne $srcStream) - { - $srcStream.Dispose() - } - } - } - - $currentEntryCount += 1 - # $currentSegmentWeight is Set to 100 giving equal weightage to each file that is getting expanded. - # $previousSegmentWeight is set to 0 as there are no prior segments. - $previousSegmentWeight = 0 - $currentSegmentWeight = 100 - ProgressBarHelper "Expand-Archive" $progressBarStatus $previousSegmentWeight $currentSegmentWeight $zipArchive.Entries.Count $currentEntryCount - } - } - finally - { - If ($null -ne $zipArchive) - { - $zipArchive.Dispose() - } - - If ($null -ne $archiveFileStream) - { - $archiveFileStream.Dispose() - } - - # Complete writing progress. - Write-Progress -Activity "Expand-Archive" -Completed - } - } - -<############################################################################################ -# ProgressBarHelper: This is a helper function used to display progress message. -# This function is used by both Compress-Archive & Expand-Archive to display archive file -# creation/expansion progress. -############################################################################################> - function ProgressBarHelper - { - param - ( - [string] - $cmdletName, - - [string] - $status, - - [double] - $previousSegmentWeight, - - [double] - $currentSegmentWeight, - - [int] - $totalNumberofEntries, - - [int] - $currentEntryCount - ) - - if ($currentEntryCount -gt 0 -and - $totalNumberofEntries -gt 0 -and - $previousSegmentWeight -ge 0 -and - $currentSegmentWeight -gt 0) - { - $entryDefaultWeight = $currentSegmentWeight/[double]$totalNumberofEntries - - $percentComplete = $previousSegmentWeight + ($entryDefaultWeight * $currentEntryCount) - Write-Progress -Activity $cmdletName -Status $status -PercentComplete $percentComplete - } - } - -<############################################################################################ -# CSVHelper: This is a helper function used to append comma after each path specifid by -# the SourcePath array. This helper function is used to display all the user supplied paths -# in the WhatIf message. -############################################################################################> - function CSVHelper - { - param - ( - [string[]] - $sourcePath - ) - - # SourcePath has already been validated by the calling funcation. - if ($sourcePath.Count -gt 1) - { - $sourcePathInCsvFormat = "`n" - for ($currentIndex = 0; $currentIndex -lt $sourcePath.Count; $currentIndex++) - { - if ($currentIndex -eq $sourcePath.Count - 1) - { - $sourcePathInCsvFormat += $sourcePath[$currentIndex] - } - else - { - $sourcePathInCsvFormat += $sourcePath[$currentIndex] + "`n" - } - } - } - else - { - $sourcePathInCsvFormat = $sourcePath - } - - return $sourcePathInCsvFormat - } - -<############################################################################################ -# ThrowTerminatingErrorHelper: This is a helper function used to throw terminating error. -############################################################################################> - function ThrowTerminatingErrorHelper - { - param - ( - [string] - $errorId, - - [string] - $errorMessage, - - [System.Management.Automation.ErrorCategory] - $errorCategory, - - [object] - $targetObject, - - [Exception] - $innerException - ) - - if ($innerException -eq $null) - { - $exception = New-object System.IO.IOException $errorMessage - } - else - { - $exception = New-Object System.IO.IOException $errorMessage, $innerException - } - - $exception = New-Object System.IO.IOException $errorMessage - $errorRecord = New-Object System.Management.Automation.ErrorRecord $exception, $errorId, $errorCategory, $targetObject - $PSCmdlet.ThrowTerminatingError($errorRecord) - } - -<############################################################################################ -# CreateErrorRecordHelper: This is a helper function used to create an ErrorRecord -############################################################################################> - function CreateErrorRecordHelper - { - param - ( - [string] - $errorId, - - [string] - $errorMessage, - - [System.Management.Automation.ErrorCategory] - $errorCategory, - - [Exception] - $exception, - - [object] - $targetObject - ) - - if ($null -eq $exception) - { - $exception = New-Object System.IO.IOException $errorMessage - } - - $errorRecord = New-Object System.Management.Automation.ErrorRecord $exception, $errorId, $errorCategory, $targetObject - return $errorRecord - } - #endregion Utility Functions - - $isVerbose = $psboundparameters.ContainsKey("Verbose") - $isConfirm = $psboundparameters.ContainsKey("Confirm") - - $isDestinationPathProvided = $true - if ($DestinationPath -eq [string]::Empty) - { - $resolvedDestinationPath = $pwd - $isDestinationPathProvided = $false - } - else - { - $destinationPathExists = Test-Path -Path $DestinationPath -PathType Container - if ($destinationPathExists) - { - $resolvedDestinationPath = GetResolvedPathHelper $DestinationPath $false $PSCmdlet - if ($resolvedDestinationPath.Count -gt 1) - { - $errorMessage = ($LocalizedData.InvalidExpandedDirPathError -f $DestinationPath) - ThrowTerminatingErrorHelper "InvalidDestinationPath" $errorMessage ([System.Management.Automation.ErrorCategory]::InvalidArgument) $DestinationPath - } - - # At this point we are sure that the provided path resolves to a valid single path. - # Calling Resolve-Path again to get the underlying provider name. - $suppliedDestinationPath = Resolve-Path -Path $DestinationPath - if ($suppliedDestinationPath.Provider.Name -ne "FileSystem") - { - $errorMessage = ($LocalizedData.ExpandArchiveInValidDestinationPath -f $DestinationPath) - ThrowTerminatingErrorHelper "InvalidDirectoryPath" $errorMessage ([System.Management.Automation.ErrorCategory]::InvalidArgument) $DestinationPath - } - } - else - { - $createdItem = New-Item -Path $DestinationPath -ItemType Directory -Confirm:$isConfirm -Verbose:$isVerbose -ErrorAction Stop - if ($createdItem -ne $null -and $createdItem.PSProvider.Name -ne "FileSystem") - { - Remove-Item "$DestinationPath" -Force -Recurse -ErrorAction SilentlyContinue - $errorMessage = ($LocalizedData.ExpandArchiveInValidDestinationPath -f $DestinationPath) - ThrowTerminatingErrorHelper "InvalidDirectoryPath" $errorMessage ([System.Management.Automation.ErrorCategory]::InvalidArgument) $DestinationPath - } - - $resolvedDestinationPath = GetResolvedPathHelper $DestinationPath $true $PSCmdlet - } - } - - $isWhatIf = $psboundparameters.ContainsKey("WhatIf") - if (!$isWhatIf) - { - $preparingToExpandVerboseMessage = ($LocalizedData.PreparingToExpandVerboseMessage) - Write-Verbose $preparingToExpandVerboseMessage - - $progressBarStatus = ($LocalizedData.ExpandProgressBarText -f $DestinationPath) - ProgressBarHelper "Expand-Archive" $progressBarStatus 0 100 100 1 - } - } - PROCESS - { - switch ($PsCmdlet.ParameterSetName) - { - "Path" - { - $resolvedSourcePaths = GetResolvedPathHelper $Path $false $PSCmdlet - - if ($resolvedSourcePaths.Count -gt 1) - { - $errorMessage = ($LocalizedData.InvalidArchiveFilePathError -f $Path, $PsCmdlet.ParameterSetName, $PsCmdlet.ParameterSetName) - ThrowTerminatingErrorHelper "InvalidArchiveFilePath" $errorMessage ([System.Management.Automation.ErrorCategory]::InvalidArgument) $Path - } - } - "LiteralPath" - { - $resolvedSourcePaths = GetResolvedPathHelper $LiteralPath $true $PSCmdlet - - if ($resolvedSourcePaths.Count -gt 1) - { - $errorMessage = ($LocalizedData.InvalidArchiveFilePathError -f $LiteralPath, $PsCmdlet.ParameterSetName, $PsCmdlet.ParameterSetName) - ThrowTerminatingErrorHelper "InvalidArchiveFilePath" $errorMessage ([System.Management.Automation.ErrorCategory]::InvalidArgument) $LiteralPath - } - } - } - - ValidateArchivePathHelper $resolvedSourcePaths - - if ($pscmdlet.ShouldProcess($resolvedSourcePaths)) - { - $expandedItems = @() - - try - { - # StopProcessing is not avaliable in Script cmdlets. However the pipleline execution - # is terminated when ever 'CTRL + C' is entered by user to terminate the cmdlet execution. - # The finally block is executed whenever pipleline is terminated. - # $isArchiveFileProcessingComplete variable is used to track if 'CTRL + C' is entered by the - # user. - $isArchiveFileProcessingComplete = $false - - # The User has not provided a destination path, hence we use '$pwd\ArchiveFileName' as the directory where the - # archive file contents would be expanded. If the path '$pwd\ArchiveFileName' already exists then we use the - # Windows default mechanism of appending a counter value at the end of the directory name where the contents - # would be expanded. - if (!$isDestinationPathProvided) - { - $archiveFile = New-Object System.IO.FileInfo $resolvedSourcePaths - $resolvedDestinationPath = Join-Path -Path $resolvedDestinationPath -ChildPath $archiveFile.BaseName - $destinationPathExists = Test-Path -LiteralPath $resolvedDestinationPath -PathType Container - - if (!$destinationPathExists) - { - New-Item -Path $resolvedDestinationPath -ItemType Directory -Confirm:$isConfirm -Verbose:$isVerbose -ErrorAction Stop | Out-Null - } - } - - ExpandArchiveHelper $resolvedSourcePaths $resolvedDestinationPath ([ref]$expandedItems) $Force $isVerbose $isConfirm - - $isArchiveFileProcessingComplete = $true - } - finally - { - # The $isArchiveFileProcessingComplete would be set to $false if user has typed 'CTRL + C' to - # terminate the cmdlet execution or if an unhandled exception is thrown. - if ($isArchiveFileProcessingComplete -eq $false) - { - if ($expandedItems.Count -gt 0) - { - # delete the expanded file/directory as the archive - # file was not completly expanded. - $expandedItems | ForEach-Object { Remove-Item $_ -Force -Recurse } - } - } - } - } - } + #region Utility Functions + function GetResolvedPathHelper { + param + ( + [string[]] + $path, + + [boolean] + $isLiteralPath, + + [System.Management.Automation.PSCmdlet] + $callerPSCmdlet + ) + + $resolvedPaths = @() + + # null and empty check are are already done on Path parameter at the cmdlet layer. + foreach ($currentPath in $path) { + try { + if ($isLiteralPath) { + $currentResolvedPaths = Resolve-Path -LiteralPath $currentPath -ErrorAction Stop + } else { + $currentResolvedPaths = Resolve-Path -Path $currentPath -ErrorAction Stop + } + } catch { + $errorMessage = ($LocalizedData.PathNotFoundError -f $currentPath) + $exception = New-Object System.InvalidOperationException $errorMessage, $_.Exception + $errorRecord = CreateErrorRecordHelper "ArchiveCmdletPathNotFound" $null ([System.Management.Automation.ErrorCategory]::InvalidArgument) $exception $currentPath + $callerPSCmdlet.ThrowTerminatingError($errorRecord) + } + + foreach ($currentResolvedPath in $currentResolvedPaths) { + $resolvedPaths += $currentResolvedPath.ProviderPath + } + } + + $resolvedPaths + } + + function Add-CompressionAssemblies { + + if ($PSEdition -eq "Desktop") { + Add-Type -AssemblyName System.IO.Compression + Add-Type -AssemblyName System.IO.Compression.FileSystem + } + } + + function IsValidFileSystemPath { + param + ( + [string[]] + $path + ) + + $result = $true; + + # null and empty check are are already done on Path parameter at the cmdlet layer. + foreach ($currentPath in $path) { + if (!([System.IO.File]::Exists($currentPath) -or [System.IO.Directory]::Exists($currentPath))) { + $errorMessage = ($LocalizedData.PathNotFoundError -f $currentPath) + ThrowTerminatingErrorHelper "PathNotFound" $errorMessage ([System.Management.Automation.ErrorCategory]::InvalidArgument) $currentPath + } + } + + return $result; + } + + + function ValidateDuplicateFileSystemPath { + param + ( + [string] + $inputParameter, + + [string[]] + $path + ) + + $uniqueInputPaths = @() + + # null and empty check are are already done on Path parameter at the cmdlet layer. + foreach ($currentPath in $path) { + $currentInputPath = $currentPath.ToUpper() + if ($uniqueInputPaths.Contains($currentInputPath)) { + $errorMessage = ($LocalizedData.DuplicatePathFoundError -f $inputParameter, $currentPath, $inputParameter) + ThrowTerminatingErrorHelper "DuplicatePathFound" $errorMessage ([System.Management.Automation.ErrorCategory]::InvalidArgument) $currentPath + } else { + $uniqueInputPaths += $currentInputPath + } + } + } + + function CompressionLevelMapper { + param + ( + [string] + $compressionLevel + ) + + $compressionLevelFormat = [System.IO.Compression.CompressionLevel]::Optimal + + # CompressionLevel format is already validated at the cmdlet layer. + switch ($compressionLevel.ToString()) { + "Fastest" { + $compressionLevelFormat = [System.IO.Compression.CompressionLevel]::Fastest + } + "NoCompression" { + $compressionLevelFormat = [System.IO.Compression.CompressionLevel]::NoCompression + } + } + + return $compressionLevelFormat + } + + function CompressArchiveHelper { + param + ( + [string[]] + $sourcePath, + + [string] + $destinationPath, + + [string] + $compressionLevel, + + [bool] + $isUpdateMode + ) + + $numberOfItemsArchived = 0 + $sourceFilePaths = @() + $sourceDirPaths = @() + + foreach ($currentPath in $sourcePath) { + $result = Test-Path -LiteralPath $currentPath -PathType Leaf + if ($result -eq $true) { + $sourceFilePaths += $currentPath + } else { + $sourceDirPaths += $currentPath + } + } + + # The Soure Path contains one or more directory (this directory can have files under it) and no files to be compressed. + if ($sourceFilePaths.Count -eq 0 -and $sourceDirPaths.Count -gt 0) { + $currentSegmentWeight = 100 / [double]$sourceDirPaths.Count + $previousSegmentWeight = 0 + foreach ($currentSourceDirPath in $sourceDirPaths) { + $count = CompressSingleDirHelper $currentSourceDirPath $destinationPath $compressionLevel $true $isUpdateMode $previousSegmentWeight $currentSegmentWeight + $numberOfItemsArchived += $count + $previousSegmentWeight += $currentSegmentWeight + } + } + + # The Soure Path contains only files to be compressed. + elseIf ($sourceFilePaths.Count -gt 0 -and $sourceDirPaths.Count -eq 0) { + # $previousSegmentWeight is equal to 0 as there are no prior segments. + # $currentSegmentWeight is set to 100 as all files have equal weightage. + $previousSegmentWeight = 0 + $currentSegmentWeight = 100 + + $numberOfItemsArchived = CompressFilesHelper $sourceFilePaths $destinationPath $compressionLevel $isUpdateMode $previousSegmentWeight $currentSegmentWeight + } + # The Soure Path contains one or more files and one or more directories (this directory can have files under it) to be compressed. + elseif ($sourceFilePaths.Count -gt 0 -and $sourceDirPaths.Count -gt 0) { + # each directory is considered as an individual segments & all the individual files are clubed in to a separate sgemnet. + $currentSegmentWeight = 100 / [double]($sourceDirPaths.Count + 1) + $previousSegmentWeight = 0 + + foreach ($currentSourceDirPath in $sourceDirPaths) { + $count = CompressSingleDirHelper $currentSourceDirPath $destinationPath $compressionLevel $true $isUpdateMode $previousSegmentWeight $currentSegmentWeight + $numberOfItemsArchived += $count + $previousSegmentWeight += $currentSegmentWeight + } + + $count = CompressFilesHelper $sourceFilePaths $destinationPath $compressionLevel $isUpdateMode $previousSegmentWeight $currentSegmentWeight + $numberOfItemsArchived += $count + } + + return $numberOfItemsArchived + } + + function CompressFilesHelper { + param + ( + [string[]] + $sourceFilePaths, + + [string] + $destinationPath, + + [string] + $compressionLevel, + + [bool] + $isUpdateMode, + + [double] + $previousSegmentWeight, + + [double] + $currentSegmentWeight + ) + + $numberOfItemsArchived = ZipArchiveHelper $sourceFilePaths $destinationPath $compressionLevel $isUpdateMode $null $previousSegmentWeight $currentSegmentWeight + + return $numberOfItemsArchived + } + + function CompressSingleDirHelper { + param + ( + [string] + $sourceDirPath, + + [string] + $destinationPath, + + [string] + $compressionLevel, + + [bool] + $useParentDirAsRoot, + + [bool] + $isUpdateMode, + + [double] + $previousSegmentWeight, + + [double] + $currentSegmentWeight + ) + + [System.Collections.Generic.List[System.String]]$subDirFiles = @() + + if ($useParentDirAsRoot) { + $sourceDirInfo = New-Object -TypeName System.IO.DirectoryInfo -ArgumentList $sourceDirPath + $sourceDirFullName = $sourceDirInfo.Parent.FullName + + # If the directory is present at the drive level the DirectoryInfo.Parent include '\' example: C:\ + # On the other hand if the directory exists at a deper level then DirectoryInfo.Parent + # has just the path (without an ending '\'). example C:\source + if ($sourceDirFullName.Length -eq 3) { + $modifiedSourceDirFullName = $sourceDirFullName + } else { + $modifiedSourceDirFullName = $sourceDirFullName + "\" + } + } else { + $sourceDirFullName = $sourceDirPath + $modifiedSourceDirFullName = $sourceDirFullName + "\" + } + + $dirContents = Get-ChildItem -LiteralPath $sourceDirPath -Recurse + foreach ($currentContent in $dirContents) { + $isContainer = $currentContent -is [System.IO.DirectoryInfo] + if (!$isContainer) { + $subDirFiles.Add($currentContent.FullName) + } else { + # The currentContent points to a directory. + # We need to check if the directory is an empty directory, if so such a + # directory has to be explictly added to the archive file. + # if there are no files in the directory the GetFiles() API returns an empty array. + $files = $currentContent.GetFiles() + if ($files.Count -eq 0) { + $subDirFiles.Add($currentContent.FullName + "\") + } + } + } + + $numberOfItemsArchived = ZipArchiveHelper $subDirFiles.ToArray() $destinationPath $compressionLevel $isUpdateMode $modifiedSourceDirFullName $previousSegmentWeight $currentSegmentWeight + + return $numberOfItemsArchived + } + + function ZipArchiveHelper { + param + ( + [System.Collections.Generic.List[System.String]] + $sourcePaths, + + [string] + $destinationPath, + + [string] + $compressionLevel, + + [bool] + $isUpdateMode, + + [string] + $modifiedSourceDirFullName, + + [double] + $previousSegmentWeight, + + [double] + $currentSegmentWeight + ) + + $numberOfItemsArchived = 0 + $fileMode = [System.IO.FileMode]::Create + $result = Test-Path -LiteralPath $DestinationPath -PathType Leaf + if ($result -eq $true) { + $fileMode = [System.IO.FileMode]::Open + } + + Add-CompressionAssemblies + + try { + # At this point we are sure that the archive file has write access. + $archiveFileStreamArgs = @($destinationPath, $fileMode) + $archiveFileStream = New-Object -TypeName System.IO.FileStream -ArgumentList $archiveFileStreamArgs + + $zipArchiveArgs = @($archiveFileStream, [System.IO.Compression.ZipArchiveMode]::Update, $false) + $zipArchive = New-Object -TypeName System.IO.Compression.ZipArchive -ArgumentList $zipArchiveArgs + + $currentEntryCount = 0 + $progressBarStatus = ($LocalizedData.CompressProgressBarText -f $destinationPath) + $bufferSize = 4kb + $buffer = New-Object Byte[] $bufferSize + + foreach ($currentFilePath in $sourcePaths) { + if ($modifiedSourceDirFullName -ne $null -and $modifiedSourceDirFullName.Length -gt 0) { + $index = $currentFilePath.IndexOf($modifiedSourceDirFullName, [System.StringComparison]::OrdinalIgnoreCase) + $currentFilePathSubString = $currentFilePath.Substring($index, $modifiedSourceDirFullName.Length) + $relativeFilePath = $currentFilePath.Replace($currentFilePathSubString, "").Trim() + } else { + $relativeFilePath = [System.IO.Path]::GetFileName($currentFilePath) + } + + # Update mode is selected. + # Check to see if archive file already contains one or more zip files in it. + if ($isUpdateMode -eq $true -and $zipArchive.Entries.Count -gt 0) { + $entryToBeUpdated = $null + + # Check if the file already exists in the archive file. + # If so replace it with new file from the input source. + # If the file does not exist in the archive file then default to + # create mode and create the entry in the archive file. + + foreach ($currentArchiveEntry in $zipArchive.Entries) { + if ($currentArchiveEntry.FullName -eq $relativeFilePath) { + $entryToBeUpdated = $currentArchiveEntry + break + } + } + + if ($entryToBeUpdated -ne $null) { + $addItemtoArchiveFileMessage = ($LocalizedData.AddItemtoArchiveFile -f $currentFilePath) + $entryToBeUpdated.Delete() + } + } + + $compression = CompressionLevelMapper $compressionLevel + + # If a directory needs to be added to an archive file, + # by convention the .Net API's expect the path of the diretcory + # to end with '\' to detect the path as an directory. + if (!$relativeFilePath.EndsWith("\", [StringComparison]::OrdinalIgnoreCase)) { + try { + try { + $currentFileStream = [System.IO.File]::Open($currentFilePath, [System.IO.FileMode]::Open, [System.IO.FileAccess]::Read) + } catch { + # Failed to access the file. Write a non terminating error to the pipeline + # and move on with the remaining files. + $exception = $_.Exception + if ($null -ne $_.Exception -and + $null -ne $_.Exception.InnerException) { + $exception = $_.Exception.InnerException + } + $errorRecord = CreateErrorRecordHelper "CompressArchiveUnauthorizedAccessError" $null ([System.Management.Automation.ErrorCategory]::PermissionDenied) $exception $currentFilePath + Write-Error -ErrorRecord $errorRecord + } + + if ($null -ne $currentFileStream) { + $srcStream = New-Object System.IO.BinaryReader $currentFileStream + + $currentArchiveEntry = $zipArchive.CreateEntry($relativeFilePath, $compression) + + # Updating the File Creation time so that the same timestamp would be retained after expanding the compressed file. + # At this point we are sure that Get-ChildItem would succeed. + $currentArchiveEntry.LastWriteTime = (Get-Item -LiteralPath $currentFilePath).LastWriteTime + + $destStream = New-Object System.IO.BinaryWriter $currentArchiveEntry.Open() + + while ($numberOfBytesRead = $srcStream.Read($buffer, 0, $bufferSize)) { + $destStream.Write($buffer, 0, $numberOfBytesRead) + $destStream.Flush() + } + + $numberOfItemsArchived += 1 + $addItemtoArchiveFileMessage = ($LocalizedData.AddItemtoArchiveFile -f $currentFilePath) + } + } finally { + If ($null -ne $currentFileStream) { + $currentFileStream.Dispose() + } + If ($null -ne $srcStream) { + $srcStream.Dispose() + } + If ($null -ne $destStream) { + $destStream.Dispose() + } + } + } else { + $currentArchiveEntry = $zipArchive.CreateEntry("$relativeFilePath", $compression) + $numberOfItemsArchived += 1 + $addItemtoArchiveFileMessage = ($LocalizedData.AddItemtoArchiveFile -f $currentFilePath) + } + + if ($null -ne $addItemtoArchiveFileMessage) { + Write-Verbose $addItemtoArchiveFileMessage + } + + $currentEntryCount += 1 + ProgressBarHelper "Compress-Archive" $progressBarStatus $previousSegmentWeight $currentSegmentWeight $sourcePaths.Count $currentEntryCount + } + } finally { + If ($null -ne $zipArchive) { + $zipArchive.Dispose() + } + + If ($null -ne $archiveFileStream) { + $archiveFileStream.Dispose() + } + + # Complete writing progress. + Write-Progress -Activity "Compress-Archive" -Completed + } + + return $numberOfItemsArchived + } + + <############################################################################################ + # ValidateArchivePathHelper: This is a helper function used to validate the archive file + # path & its file format. The only supported archive file format is .zip + ############################################################################################> + function ValidateArchivePathHelper { + param + ( + [string] + $archiveFile + ) + + if ([System.IO.File]::Exists($archiveFile)) { + $extension = [system.IO.Path]::GetExtension($archiveFile) + + # Invalid file extension is specifed for the zip file. + if ($extension -ne $zipFileExtension) { + $errorMessage = ($LocalizedData.InvalidZipFileExtensionError -f $extension, $zipFileExtension) + ThrowTerminatingErrorHelper "NotSupportedArchiveFileExtension" $errorMessage ([System.Management.Automation.ErrorCategory]::InvalidArgument) $extension + } + } else { + $errorMessage = ($LocalizedData.PathNotFoundError -f $archiveFile) + ThrowTerminatingErrorHelper "PathNotFound" $errorMessage ([System.Management.Automation.ErrorCategory]::InvalidArgument) $archiveFile + } + } + + <############################################################################################ + # ExpandArchiveHelper: This is a helper function used to expand the archive file contents + # to the specified directory. + ############################################################################################> + function ExpandArchiveHelper { + param + ( + [string] + $archiveFile, + + [string] + $expandedDir, + + [ref] + $expandedItems, + + [boolean] + $force, + + [boolean] + $isVerbose, + + [boolean] + $isConfirm + ) + + Add-CompressionAssemblies + + try { + # The existance of archive file has already been validated by ValidateArchivePathHelper + # before calling this helper function. + $archiveFileStreamArgs = @($archiveFile, [System.IO.FileMode]::Open, [System.IO.FileAccess]::Read) + $archiveFileStream = New-Object -TypeName System.IO.FileStream -ArgumentList $archiveFileStreamArgs + + $zipArchiveArgs = @($archiveFileStream, [System.IO.Compression.ZipArchiveMode]::Read, $false) + $zipArchive = New-Object -TypeName System.IO.Compression.ZipArchive -ArgumentList $zipArchiveArgs + + if ($zipArchive.Entries.Count -eq 0) { + $archiveFileIsEmpty = ($LocalizedData.ArchiveFileIsEmpty -f $archiveFile) + Write-Verbose $archiveFileIsEmpty + return + } + + $currentEntryCount = 0 + $progressBarStatus = ($LocalizedData.ExpandProgressBarText -f $archiveFile) + + # The archive entries can either be empty directories or files. + foreach ($currentArchiveEntry in $zipArchive.Entries) { + $currentArchiveEntryPath = Join-Path -Path $expandedDir -ChildPath $currentArchiveEntry.FullName + $extension = [system.IO.Path]::GetExtension($currentArchiveEntryPath) + + # The current archive entry is an empty directory + # The FullName of the Archive Entry representing a directory would end with a trailing '\'. + if ($extension -eq [string]::Empty -and + $currentArchiveEntryPath.EndsWith("\", [StringComparison]::OrdinalIgnoreCase)) { + $pathExists = Test-Path -LiteralPath $currentArchiveEntryPath + + # The current archive entry expects an empty directory. + # Check if the existing directory is empty. If its not empty + # then it means that user has added this directory by other means. + if ($pathExists -eq $false) { + New-Item $currentArchiveEntryPath -ItemType Directory -Confirm:$isConfirm | Out-Null + + if (Test-Path -LiteralPath $currentArchiveEntryPath -PathType Container) { + $addEmptyDirectorytoExpandedPathMessage = ($LocalizedData.AddItemtoArchiveFile -f $currentArchiveEntryPath) + Write-Verbose $addEmptyDirectorytoExpandedPathMessage + + $expandedItems.Value += $currentArchiveEntryPath + } + } + } else { + try { + $currentArchiveEntryFileInfo = New-Object -TypeName System.IO.FileInfo -ArgumentList $currentArchiveEntryPath + $parentDirExists = Test-Path -LiteralPath $currentArchiveEntryFileInfo.DirectoryName -PathType Container + + # If the Parent directory of the current entry in the archive file does not exist, then create it. + if ($parentDirExists -eq $false) { + New-Item $currentArchiveEntryFileInfo.DirectoryName -ItemType Directory -Confirm:$isConfirm | Out-Null + + if (!(Test-Path -LiteralPath $currentArchiveEntryFileInfo.DirectoryName -PathType Container)) { + # The directory referred by $currentArchiveEntryFileInfo.DirectoryName was not successfully created. + # This could be because the user has specified -Confirm paramter when Expand-Archive was invoked + # and authorization was not provided when confirmation was prompted. In such a scenario, + # we skip the current file in the archive and continue with the remaining archive file contents. + Continue + } + + $expandedItems.Value += $currentArchiveEntryFileInfo.DirectoryName + } + + $hasNonTerminatingError = $false + + # Check if the file in to which the current archive entry contents + # would be expanded already exists. + if ($currentArchiveEntryFileInfo.Exists) { + if ($force) { + Remove-Item -LiteralPath $currentArchiveEntryFileInfo.FullName -Force -ErrorVariable ev -Verbose:$isVerbose -Confirm:$isConfirm + if ($ev -ne $null) { + $hasNonTerminatingError = $true + } + + if (Test-Path -LiteralPath $currentArchiveEntryFileInfo.FullName -PathType Leaf) { + # The file referred by $currentArchiveEntryFileInfo.FullName was not successfully removed. + # This could be because the user has specified -Confirm paramter when Expand-Archive was invoked + # and authorization was not provided when confirmation was prompted. In such a scenario, + # we skip the current file in the archive and continue with the remaining archive file contents. + Continue + } + } else { + # Write non-terminating error to the pipeline. + $errorMessage = ($LocalizedData.FileExistsError -f $currentArchiveEntryFileInfo.FullName, $archiveFile, $currentArchiveEntryFileInfo.FullName, $currentArchiveEntryFileInfo.FullName) + $errorRecord = CreateErrorRecordHelper "ExpandArchiveFileExists" $errorMessage ([System.Management.Automation.ErrorCategory]::InvalidOperation) $null $currentArchiveEntryFileInfo.FullName + Write-Error -ErrorRecord $errorRecord + $hasNonTerminatingError = $true + } + } + + if (!$hasNonTerminatingError) { + [System.IO.Compression.ZipFileExtensions]::ExtractToFile($currentArchiveEntry, $currentArchiveEntryPath, $false) + + # Add the expanded file path to the $expandedItems array, + # to keep track of all the expanded files created while expanding the archive file. + # If user enters CTRL + C then at that point of time, all these expanded files + # would be deleted as part of the clean up process. + $expandedItems.Value += $currentArchiveEntryPath + + $addFiletoExpandedPathMessage = ($LocalizedData.CreateFileAtExpandedPath -f $currentArchiveEntryPath) + Write-Verbose $addFiletoExpandedPathMessage + } + } finally { + If ($null -ne $destStream) { + $destStream.Dispose() + } + + If ($null -ne $srcStream) { + $srcStream.Dispose() + } + } + } + + $currentEntryCount += 1 + # $currentSegmentWeight is Set to 100 giving equal weightage to each file that is getting expanded. + # $previousSegmentWeight is set to 0 as there are no prior segments. + $previousSegmentWeight = 0 + $currentSegmentWeight = 100 + ProgressBarHelper "Expand-Archive" $progressBarStatus $previousSegmentWeight $currentSegmentWeight $zipArchive.Entries.Count $currentEntryCount + } + } finally { + If ($null -ne $zipArchive) { + $zipArchive.Dispose() + } + + If ($null -ne $archiveFileStream) { + $archiveFileStream.Dispose() + } + + # Complete writing progress. + Write-Progress -Activity "Expand-Archive" -Completed + } + } + + <############################################################################################ + # ProgressBarHelper: This is a helper function used to display progress message. + # This function is used by both Compress-Archive & Expand-Archive to display archive file + # creation/expansion progress. + ############################################################################################> + function ProgressBarHelper { + param + ( + [string] + $cmdletName, + + [string] + $status, + + [double] + $previousSegmentWeight, + + [double] + $currentSegmentWeight, + + [int] + $totalNumberofEntries, + + [int] + $currentEntryCount + ) + + if ($currentEntryCount -gt 0 -and + $totalNumberofEntries -gt 0 -and + $previousSegmentWeight -ge 0 -and + $currentSegmentWeight -gt 0) { + $entryDefaultWeight = $currentSegmentWeight / [double]$totalNumberofEntries + + $percentComplete = $previousSegmentWeight + ($entryDefaultWeight * $currentEntryCount) + Write-Progress -Activity $cmdletName -Status $status -PercentComplete $percentComplete + } + } + + <############################################################################################ + # CSVHelper: This is a helper function used to append comma after each path specifid by + # the SourcePath array. This helper function is used to display all the user supplied paths + # in the WhatIf message. + ############################################################################################> + function CSVHelper { + param + ( + [string[]] + $sourcePath + ) + + # SourcePath has already been validated by the calling funcation. + if ($sourcePath.Count -gt 1) { + $sourcePathInCsvFormat = "`n" + for ($currentIndex = 0; $currentIndex -lt $sourcePath.Count; $currentIndex++) { + if ($currentIndex -eq $sourcePath.Count - 1) { + $sourcePathInCsvFormat += $sourcePath[$currentIndex] + } else { + $sourcePathInCsvFormat += $sourcePath[$currentIndex] + "`n" + } + } + } else { + $sourcePathInCsvFormat = $sourcePath + } + + return $sourcePathInCsvFormat + } + + <############################################################################################ + # ThrowTerminatingErrorHelper: This is a helper function used to throw terminating error. + ############################################################################################> + function ThrowTerminatingErrorHelper { + param + ( + [string] + $errorId, + + [string] + $errorMessage, + + [System.Management.Automation.ErrorCategory] + $errorCategory, + + [object] + $targetObject, + + [Exception] + $innerException + ) + + if ($innerException -eq $null) { + $exception = New-object System.IO.IOException $errorMessage + } else { + $exception = New-Object System.IO.IOException $errorMessage, $innerException + } + + $exception = New-Object System.IO.IOException $errorMessage + $errorRecord = New-Object System.Management.Automation.ErrorRecord $exception, $errorId, $errorCategory, $targetObject + $PSCmdlet.ThrowTerminatingError($errorRecord) + } + + <############################################################################################ + # CreateErrorRecordHelper: This is a helper function used to create an ErrorRecord + ############################################################################################> + function CreateErrorRecordHelper { + param + ( + [string] + $errorId, + + [string] + $errorMessage, + + [System.Management.Automation.ErrorCategory] + $errorCategory, + + [Exception] + $exception, + + [object] + $targetObject + ) + + if ($null -eq $exception) { + $exception = New-Object System.IO.IOException $errorMessage + } + + $errorRecord = New-Object System.Management.Automation.ErrorRecord $exception, $errorId, $errorCategory, $targetObject + return $errorRecord + } + #endregion Utility Functions + + $isVerbose = $psboundparameters.ContainsKey("Verbose") + $isConfirm = $psboundparameters.ContainsKey("Confirm") + + $isDestinationPathProvided = $true + if ($DestinationPath -eq [string]::Empty) { + $resolvedDestinationPath = $pwd + $isDestinationPathProvided = $false + } else { + $destinationPathExists = Test-Path -Path $DestinationPath -PathType Container + if ($destinationPathExists) { + $resolvedDestinationPath = GetResolvedPathHelper $DestinationPath $false $PSCmdlet + if ($resolvedDestinationPath.Count -gt 1) { + $errorMessage = ($LocalizedData.InvalidExpandedDirPathError -f $DestinationPath) + ThrowTerminatingErrorHelper "InvalidDestinationPath" $errorMessage ([System.Management.Automation.ErrorCategory]::InvalidArgument) $DestinationPath + } + + # At this point we are sure that the provided path resolves to a valid single path. + # Calling Resolve-Path again to get the underlying provider name. + $suppliedDestinationPath = Resolve-Path -Path $DestinationPath + if ($suppliedDestinationPath.Provider.Name -ne "FileSystem") { + $errorMessage = ($LocalizedData.ExpandArchiveInValidDestinationPath -f $DestinationPath) + ThrowTerminatingErrorHelper "InvalidDirectoryPath" $errorMessage ([System.Management.Automation.ErrorCategory]::InvalidArgument) $DestinationPath + } + } else { + $createdItem = New-Item -Path $DestinationPath -ItemType Directory -Confirm:$isConfirm -Verbose:$isVerbose -ErrorAction Stop + if ($createdItem -ne $null -and $createdItem.PSProvider.Name -ne "FileSystem") { + Remove-Item "$DestinationPath" -Force -Recurse -ErrorAction SilentlyContinue + $errorMessage = ($LocalizedData.ExpandArchiveInValidDestinationPath -f $DestinationPath) + ThrowTerminatingErrorHelper "InvalidDirectoryPath" $errorMessage ([System.Management.Automation.ErrorCategory]::InvalidArgument) $DestinationPath + } + + $resolvedDestinationPath = GetResolvedPathHelper $DestinationPath $true $PSCmdlet + } + } + + $isWhatIf = $psboundparameters.ContainsKey("WhatIf") + if (!$isWhatIf) { + $preparingToExpandVerboseMessage = ($LocalizedData.PreparingToExpandVerboseMessage) + Write-Verbose $preparingToExpandVerboseMessage + + $progressBarStatus = ($LocalizedData.ExpandProgressBarText -f $DestinationPath) + ProgressBarHelper "Expand-Archive" $progressBarStatus 0 100 100 1 + } + } + PROCESS { + switch ($PsCmdlet.ParameterSetName) { + "Path" { + $resolvedSourcePaths = GetResolvedPathHelper $Path $false $PSCmdlet + + if ($resolvedSourcePaths.Count -gt 1) { + $errorMessage = ($LocalizedData.InvalidArchiveFilePathError -f $Path, $PsCmdlet.ParameterSetName, $PsCmdlet.ParameterSetName) + ThrowTerminatingErrorHelper "InvalidArchiveFilePath" $errorMessage ([System.Management.Automation.ErrorCategory]::InvalidArgument) $Path + } + } + "LiteralPath" { + $resolvedSourcePaths = GetResolvedPathHelper $LiteralPath $true $PSCmdlet + + if ($resolvedSourcePaths.Count -gt 1) { + $errorMessage = ($LocalizedData.InvalidArchiveFilePathError -f $LiteralPath, $PsCmdlet.ParameterSetName, $PsCmdlet.ParameterSetName) + ThrowTerminatingErrorHelper "InvalidArchiveFilePath" $errorMessage ([System.Management.Automation.ErrorCategory]::InvalidArgument) $LiteralPath + } + } + } + + ValidateArchivePathHelper $resolvedSourcePaths + + if ($pscmdlet.ShouldProcess($resolvedSourcePaths)) { + $expandedItems = @() + + try { + # StopProcessing is not avaliable in Script cmdlets. However the pipleline execution + # is terminated when ever 'CTRL + C' is entered by user to terminate the cmdlet execution. + # The finally block is executed whenever pipleline is terminated. + # $isArchiveFileProcessingComplete variable is used to track if 'CTRL + C' is entered by the + # user. + $isArchiveFileProcessingComplete = $false + + # The User has not provided a destination path, hence we use '$pwd\ArchiveFileName' as the directory where the + # archive file contents would be expanded. If the path '$pwd\ArchiveFileName' already exists then we use the + # Windows default mechanism of appending a counter value at the end of the directory name where the contents + # would be expanded. + if (!$isDestinationPathProvided) { + $archiveFile = New-Object System.IO.FileInfo $resolvedSourcePaths + $resolvedDestinationPath = Join-Path -Path $resolvedDestinationPath -ChildPath $archiveFile.BaseName + $destinationPathExists = Test-Path -LiteralPath $resolvedDestinationPath -PathType Container + + if (!$destinationPathExists) { + New-Item -Path $resolvedDestinationPath -ItemType Directory -Confirm:$isConfirm -Verbose:$isVerbose -ErrorAction Stop | Out-Null + } + } + + ExpandArchiveHelper $resolvedSourcePaths $resolvedDestinationPath ([ref]$expandedItems) $Force $isVerbose $isConfirm + + $isArchiveFileProcessingComplete = $true + } finally { + # The $isArchiveFileProcessingComplete would be set to $false if user has typed 'CTRL + C' to + # terminate the cmdlet execution or if an unhandled exception is thrown. + if ($isArchiveFileProcessingComplete -eq $false) { + if ($expandedItems.Count -gt 0) { + # delete the expanded file/directory as the archive + # file was not completly expanded. + $expandedItems | ForEach-Object { Remove-Item $_ -Force -Recurse } + } + } + } + } + } } -function Write-LocalMessage -{ +function Write-LocalMessage { [CmdletBinding()] Param ( [string]$Message @@ -2371,61 +2068,56 @@ function Write-LocalMessage } #endregion Utility Functions -try -{ - [System.Net.ServicePointManager]::SecurityProtocol = "Tls12" - - Write-LocalMessage -Message "Downloading repository from '$($BaseUrl)/archive/$($Branch).zip'" - Invoke-WebRequest -Uri "$($BaseUrl)/archive/$($Branch).zip" -UseBasicParsing -OutFile "$($env:TEMP)\$($ModuleName).zip" -ErrorAction Stop - - Write-LocalMessage -Message "Creating temporary project folder: '$($env:TEMP)\$($ModuleName)'" - $null = New-Item -Path $env:TEMP -Name $ModuleName -ItemType Directory -Force -ErrorAction Stop - - Write-LocalMessage -Message "Extracting archive to '$($env:TEMP)\$($ModuleName)'" - Expand-Archive -Path "$($env:TEMP)\$($ModuleName).zip" -DestinationPath "$($env:TEMP)\$($ModuleName)" -ErrorAction Stop - - $basePath = Get-ChildItem "$($env:TEMP)\$($ModuleName)\*" | Select-Object -First 1 - if ($SubFolder) { $basePath = "$($basePath)\$($SubFolder)" } - - # Only needed for PS v5+ but doesn't hurt anyway - $manifest = "$($basePath)\$($ModuleName).psd1" - $manifestData = Invoke-Expression ([System.IO.File]::ReadAllText($manifest)) - $moduleVersion = $manifestData.ModuleVersion - Write-LocalMessage -Message "Download concluded: $($ModuleName) | Branch $($Branch) | Version $($moduleVersion)" - - # Determine output path - $path = "$($env:ProgramFiles)\WindowsPowerShell\Modules\$($ModuleName)" - if ($doUserMode) { $path = "$(Split-Path $profile.CurrentUserAllHosts)\Modules\$($ModuleName)" } - if ($PSVersionTable.PSVersion.Major -ge 5) { $path += "\$moduleVersion" } - - if ((Test-Path $path) -and (-not $Force)) - { - Write-LocalMessage -Message "Module already installed, interrupting installation" - return - } - - Write-LocalMessage -Message "Creating folder: $($path)" - $null = New-Item -Path $path -ItemType Directory -Force -ErrorAction Stop - - Write-LocalMessage -Message "Copying files to $($path)" - foreach ($file in (Get-ChildItem -Path $basePath)) - { - Move-Item -Path $file.FullName -Destination $path -ErrorAction Stop - } - - Write-LocalMessage -Message "Cleaning up temporary files" - Remove-Item -Path "$($env:TEMP)\$($ModuleName)" -Force -Recurse - Remove-Item -Path "$($env:TEMP)\$($ModuleName).zip" -Force - - Write-LocalMessage -Message "Installation of the module $($ModuleName), Branch $($Branch), Version $($moduleVersion) completed successfully!" -} -catch -{ - Write-LocalMessage -Message "Installation of the module $($ModuleName) failed!" - - Write-LocalMessage -Message "Cleaning up temporary files" - Remove-Item -Path "$($env:TEMP)\$($ModuleName)" -Force -Recurse - Remove-Item -Path "$($env:TEMP)\$($ModuleName).zip" -Force - - throw +try { + [System.Net.ServicePointManager]::SecurityProtocol = "Tls12" + + Write-LocalMessage -Message "Downloading repository from '$($BaseUrl)/archive/$($Branch).zip'" + Invoke-WebRequest -Uri "$($BaseUrl)/archive/$($Branch).zip" -UseBasicParsing -OutFile "$($env:TEMP)\$($ModuleName).zip" -ErrorAction Stop + + Write-LocalMessage -Message "Creating temporary project folder: '$($env:TEMP)\$($ModuleName)'" + $null = New-Item -Path $env:TEMP -Name $ModuleName -ItemType Directory -Force -ErrorAction Stop + + Write-LocalMessage -Message "Extracting archive to '$($env:TEMP)\$($ModuleName)'" + Expand-Archive -Path "$($env:TEMP)\$($ModuleName).zip" -DestinationPath "$($env:TEMP)\$($ModuleName)" -ErrorAction Stop + + $basePath = Get-ChildItem "$($env:TEMP)\$($ModuleName)\*" | Select-Object -First 1 + if ($SubFolder) { $basePath = "$($basePath)\$($SubFolder)" } + + # Only needed for PS v5+ but doesn't hurt anyway + $manifest = "$($basePath)\$($ModuleName).psd1" + $manifestData = Invoke-Expression ([System.IO.File]::ReadAllText($manifest)) + $moduleVersion = $manifestData.ModuleVersion + Write-LocalMessage -Message "Download concluded: $($ModuleName) | Branch $($Branch) | Version $($moduleVersion)" + + # Determine output path + $path = "$($env:ProgramFiles)\WindowsPowerShell\Modules\$($ModuleName)" + if ($doUserMode) { $path = "$(Split-Path $profile.CurrentUserAllHosts)\Modules\$($ModuleName)" } + if ($PSVersionTable.PSVersion.Major -ge 5) { $path += "\$moduleVersion" } + + if ((Test-Path $path) -and (-not $Force)) { + Write-LocalMessage -Message "Module already installed, interrupting installation" + return + } + + Write-LocalMessage -Message "Creating folder: $($path)" + $null = New-Item -Path $path -ItemType Directory -Force -ErrorAction Stop + + Write-LocalMessage -Message "Copying files to $($path)" + foreach ($file in (Get-ChildItem -Path $basePath)) { + Move-Item -Path $file.FullName -Destination $path -ErrorAction Stop + } + + Write-LocalMessage -Message "Cleaning up temporary files" + Remove-Item -Path "$($env:TEMP)\$($ModuleName)" -Force -Recurse + Remove-Item -Path "$($env:TEMP)\$($ModuleName).zip" -Force + + Write-LocalMessage -Message "Installation of the module $($ModuleName), Branch $($Branch), Version $($moduleVersion) completed successfully!" +} catch { + Write-LocalMessage -Message "Installation of the module $($ModuleName) failed!" + + Write-LocalMessage -Message "Cleaning up temporary files" + Remove-Item -Path "$($env:TEMP)\$($ModuleName)" -Force -Recurse + Remove-Item -Path "$($env:TEMP)\$($ModuleName).zip" -Force + + throw } \ No newline at end of file