ProgramManager.psm1
# Create some global variables $script:ModuleRoot = $PSScriptRoot $script:ModuleVersion = (Import-PowerShellDataFile -Path "$($script:ModuleRoot)\ProgramManager.psd1").ModuleVersion $script:DataPath = "$env:APPDATA\Powershell\ProgramManager" if ((Test-Path -Path $script:DataPath) -eq $false) { # Create the module data storage folders if they don't exist New-Item -ItemType Directory -Path "$env:APPDATA" -Name "Powershell" -ErrorAction SilentlyContinue New-Item -ItemType Directory -Path "$env:APPDATA\Powershell" -Name "ProgramManager" } # Detect whether at some level dotsourcing was enforced $script:doDotSource = $global:ModuleDebugDotSource $script:doDotSource = $true # Needed to make code coverage tests work # Detect whether at some level loading individual module files, rather than the compiled module was enforced $importIndividualFiles = $global:ModuleDebugIndividualFiles # Resolve-Path function which deals with non-existent paths function Resolve-Path_i { [CmdletBinding()] Param ( [Parameter(Mandatory = $true, Position = 0, ValueFromPipeline = $true)] [string] $Path # Path to resolve ) # Run the command silently $resolvedPath = Resolve-Path $Path -ErrorAction SilentlyContinue # Variable will be null if $Path doesn't exist # In that case set it to an empty string if ($null -eq $resolvedPath) { $resolvedPath = "" } $resolvedPath } # If script detects its running from original dev environment, import individually since module won't be compiled if (Test-Path (Resolve-Path_i -Path "$($script:ModuleRoot)\..\.git")) { $importIndividualFiles = $true } if ("<was compiled>" -eq '<was not compiled>') { $importIndividualFiles = $true } # Imports a module file, either through dot-sourcing or through invoking the script 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 # Path of module file ) # Get the resolved path to avoid any cross-OS issues $resolvedPath = $ExecutionContext.SessionState.Path.GetResolvedPSPathFromPSPath($Path).ProviderPath if ($doDotSource) { # Load the script through dot-sourcing . $resolvedPath }else { # Load the script through different method (unknown atm) $ExecutionContext.InvokeCommand.InvokeScript($false, ([scriptblock]::Create([io.file]::ReadAllText($resolvedPath))), $null, $null) } } # Load individual files if not compiled if ($importIndividualFiles) { # Execute Preimport actions . Import-ModuleFile -Path "$ModuleRoot\internal\scripts\preimport.ps1" # 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 . Import-ModuleFile -Path "$ModuleRoot\internal\scripts\postimport.ps1" # End execution here, do not load compiled code below return } #region Load compiled code function Export-PackageList { <# .SYNOPSIS Exports package list .DESCRIPTION Exports ProgramManager.Package List to xml database. .PARAMETER PackageList The System.Collections.Generic.List[psobject] of packages to serialise. .EXAMPLE PS C:\> Export-Data -PackageList $packages Exports the $packages list to the module root folder #> [CmdletBinding()] Param ( [Parameter(Mandatory = $true, Position = 0)] [System.Collections.Generic.List[psobject]] [AllowEmptyCollection()] $PackageList ) Export-Clixml -Path "$script:DataPath\packageDatabase.xml" -InputObject $PackageList } function Import-PackageList { <# .SYNOPSIS Imports the package list .DESCRIPTION Imports all ProgramManager.Package objects from the xml database. .EXAMPLE PS C:\> $list = Import-PackageList This populates the $list with all existing packages. #> # Create list of all PMPackage objects $packageList = [System.Collections.Generic.List[psobject]]@() # Check if the xml database exists if ((Test-Path -Path "$script:DataPath\packageDatabase.xml") -eq $true) { # The xml database exists # Load all existing PMPrograms into a list $xmlData = Import-Clixml -Path "$script:DataPath\packageDatabase.xml" # Iterate through all imported objects foreach ($obj in $xmlData) { # Only operate on PMPackage objects if ($obj.psobject.TypeNames[0] -eq "Deserialized.ProgramManager.Package") { # Create new PMPackage objects $existingPackage = New-Object -TypeName psobject $existingPackage.PSObject.TypeNames.Insert(0, "ProgramManager.Package") # Copy the properties from the Deserialized object into the new one foreach ($property in $obj.psobject.Properties) { # Copy over the deserialised object properties over to new object $existingPackage | Add-Member -Type NoteProperty -Name $property.Name -Value $property.Value } $packageList.Add($existingPackage) } } } # Return the list object; -NoEnumerate is used to avoid powershell converting list to Object[] array Write-Output $packageList -NoEnumerate } function Write-Message { <# .SYNOPSIS Writes a message to the screen. .DESCRIPTION Writes a message to the screen, as text, a warning, or an error. .PARAMETER Message The message to print to screen. .PARAMETER DisplayText Writes the message as standard text. .PARAMETER DisplayWarning Writes the message as a warning. .PARAMETER DisplayError Writes the message as an error. .EXAMPLE PS C:\> Write-Message -Message "invalid argument" -DisplayError Prints the error message to screen by invoking Write-Error. #> [CmdletBinding()] Param( [Parameter(ParameterSetName = "DisplayText", Mandatory = $true)] [Parameter(ParameterSetName = "DisplayWarning", Mandatory = $true)] [Parameter(ParameterSetName = "DisplayError", Mandatory = $true)] [string] $Message, [Parameter(ParameterSetName = "DisplayText", Mandatory = $true)] [switch] $DisplayText, [Parameter(ParameterSetName = "DisplayWarning", Mandatory = $true)] [switch] $DisplayWarning, [Parameter(ParameterSetName = "DisplayError", Mandatory = $true)] [switch] $DisplayError ) if ($DisplayText -eq $true) { Write-Host -Message $Message }elseif ($DisplayWarning -eq $true) { Write-Warning -Message $Message }elseif ($DisplayError -eq $true) { Write-Error -Message $Message } } function Get-PMPackage { <# .SYNOPSIS Get information about a specified ProgramManager package. .DESCRIPTION Returns the specified ProgramManager.Package object, for display to terminal, or for passing down the pipeline. .PARAMETER PackageName The name of the package to retrieve. .PARAMETER ShowFullDetail Toggles whether it shows a overview of the package with the "usually" important properties, or whether it shows every single property of the package, some of which will not have much use for the user. .EXAMPLE PS C:\> Get-PMPackage -PackageName "notepad" Returns information about the "notepad" package. .INPUTS None .OUTPUTS None #> [CmdletBinding()] Param ( [Parameter(Mandatory = $true, Position = 0)] [AllowEmptyString()] [string] $PackageName, [Parameter(Position = 1)] [switch] $ShowFullDetail ) # Import all PMPackage objects from the database file Write-Verbose "Loading existing packages from database" $packageList = Import-PackageList # Check that the name is not empty if ([System.String]::IsNullOrWhiteSpace($PackageName) -eq $true) { Write-Message -Message "The name cannot be empty" -DisplayWarning return } # Check if package exists $package = $packageList | Where-Object { $_.Name -eq $PackageName } if ($null -eq $package) { Write-Message -Message "There is no package called: $PackageName" -DisplayWarning return } # Append the View object type to control the visual output of the object depending on the user's preference if ($ShowFullDetail -eq $true) { Write-Verbose "Detected -ShowFullDetail flag." $package.PSObject.TypeNames.Insert(1, "ProgramManager.Package-View.Full") }else { Write-Verbose "Not detected -ShowFullDetail flag." $package.PSObject.TypeNames.Insert(1, "ProgramManager.Package-View.Overview") } # Output the package object Write-Output $package } function Invoke-PMInstall { <# .SYNOPSIS Installs a ProgramManager package. .DESCRIPTION Invokes an installation process on a ProgramManager.Package which has been earlier added to the database. .PARAMETER PackageName The name of the ProgramManager package to install. .PARAMETER Confirm If this switch is enabled, you will be prompted for confirmation before executing any operations that change state. .PARAMETER WhatIf If this switch is enabled, no actions are performed but informational messages will be displayed that explain what would happen if the command were to run. .EXAMPLE PS C:\> Invoke-PMInstall -Name "notepad" This command will install the package named "notepad", executing any scriptblocks along with it. .EXAMPLE PS C:\> Get-PMPackage "notepad" | Invoke-PMInstall This command supports passing in a ProgramManager.Package object, by retrieving it using Get-PMPacakge for example. This command will install the package named "notepad", executing any scriptblocks along with it. .INPUTS System.String[] .OUTPUTS None #> [CmdletBinding(SupportsShouldProcess = $true, ConfirmImpact = "Medium")] Param ( [Parameter(Mandatory = $true, Position = 0, ValueFromPipelineByPropertyName = $true)] [AllowEmptyString()] [Alias("Name")] [string[]] $PackageName ) # Check if the xml database exists if ((Test-Path -Path "$script:DataPath\packageDatabase.xml") -eq $true) { # Load all existing PMPackages into a list Write-Verbose "Loading existing packages from database" $packageList = Import-PackageList }else { # The xml database doesn't exist; warn user Write-Message -Message "The database file doesn't exist. Run New-PMPackage to initialise it." -DisplayWarning return } # Iterate through all passed package names foreach ($name in $PackageName) { Write-Verbose "Installing package:{$name}" # Check that the name is not empty if ([System.String]::IsNullOrWhiteSpace($name) -eq $true) { Write-Message -Message "The name cannot be empty" -DisplayWarning return } # Get the package by name Write-Verbose "Retrieving the ProgramManager.Package Object" $package = $packageList | Where-Object { $_.Name -eq $name } # Warn the user if the name is invalid if ($null -eq $package) { Write-Message -Message "There is no package called: $Name" -DisplayWarning return } # Check if the package has a pre-install scriptblock to run Write-Verbose "Checking for pre-install scriptblock" if ([System.String]::IsNullOrWhiteSpace($package.PreInstallScriptblock) -eq $false) { if ($PSCmdlet.ShouldProcess("pre-install scriptblock from package:{$name}", "Execute scriptblock")) { # Convert the string into a scriptblock and execute Write-Verbose "Coverting scriptblock and executing it" $scriptblock = [scriptblock]::Create($package.PreInstallScriptblock) Invoke-Command -ScriptBlock $scriptblock -ArgumentList $package } } # If the package is a url-package, download it and define extra properties to allow the installation code to run correctly if ($package.Type -eq "UrlPackage") { # Get the absolute url, after any redirection, which points to the actual file Write-Verbose "Getting absolute url from link given" $url = [System.Net.HttpWebRequest]::Create($package.Url).GetResponse().ResponseUri.AbsoluteUri # Get the file extension in order to save it correctly $regex = [regex]::Match($url, ".*\.(.*)") $extension = $regex.Groups[1].Value if ($PSCmdlet.ShouldProcess("installer at url:$url", "Download")){ # Download the installer from the url Write-Verbose "Downloading installer to \packages\$($package.Name)\" New-Item -ItemType Directory -Path "$script:DataPath\packages\$($package.Name)\" | Out-Null Invoke-WebRequest -Uri $url -OutFile "$script:DataPath\packages\$($package.Name)\installer.$extension" } # Set executable properties in the package object to allow later code to run correctly Write-Verbose "Adding properties to allow for installation" $package | Add-Member -Type NoteProperty -Name "ExecutableName" -Value "installer.$extension" $package | Add-Member -Type NoteProperty -Name "ExecutableType" -Value ".$extension" } # Main installation logic if ($package.Type -eq "LocalPackage" -or $package.Type -eq "UrlPackage") { # Differentiate between exe and msi installers if ($package.ExecutableType -eq ".exe") { if ($PSCmdlet.ShouldProcess(".exe installer:$($package.ExecutableName)", "Start process")){ # Start the exe installer and wait for finish Write-Verbose "Starting the .exe installer" Start-Process -FilePath "$script:DataPath\packages\$($package.Name)\$($package.ExecutableName)" -Wait } }elseif ($package.ExecutableType -eq ".msi") { <# SET PROPERTIES OF MSI INSTALLER, removed for now since very few msi installers around anyway # Set the display argument for msiexec if ($ShowUI -eq $true) { $dislayArgument = "/qr " }else { $dislayArgument = "/qn " } # Set the logging argument for msiexec if ($NoLog -eq $true) { $logArgument = "" }else { $logArgument = "/l*v `"$script:DataPath\install-$($apckage.Name)-$(Get-Date -Format "yyyy/MM/dd HH:mm").txt`"" } # If the package has a defined install directory, set the msiexec argument(s) to that if ([System.String]::IsNullOrWhiteSpace($package.InstallDirectory) -eq $false) { $paramArgument += "INSTALL_PREFIX_1=`"$($package.InstallDirectory)`" " $paramArgument += "TARGETDIR=`"$($package.InstallDirectory)`" " $paramArgument += "INSTALLDIR=`"$($package.InstallDirectory)`" " $paramArgument += "INSTALLDIRECTORY=`"$($package.InstallDirectory)`" " $paramArgument += "TARGETDIRECTORY=`"$($package.InstallDirectory)`" " $paramArgument += "TARGETPATH=`"$($package.InstallDirectory)`" " $paramArgument += "INSTALLPATH=`"$($package.InstallDirectory)`" " } # Set the msiexec arguments $processStartupInfo = New-Object System.Diagnostics.ProcessStartInfo -Property @{ FileName = "msiexec.exe" Arguments = "$script:DataPath\packages\$($package.Name)\$($package.ExecutableName) " + $dislayArgument + $logArgument + $paramArgument UseShellExecute = $false }#> if ($PSCmdlet.ShouldProcess(".msi installer:$($package.ExecutableName)", "Start process")) { # Start the msiexec process and wait for finish Write-Verbose "Starting the .msi installer" Start-Process -FilePath "msiexec.exe" -ArgumentList "/i $script:DataPath\packages\$($package.Name)\$($package.ExecutableName) /qf /l*v `"$script:DataPath\log-$($package.Name)-$(Get-Date -Format "yyyy-MM-dd HH:mm").txt`"" -Wait } } }elseif ($package.Type -eq "PortablePackage") { # Check that the install directory doesn't contain any characters which could cause potential issues if ($package.InstallDirectory -like "*.``**" -or $package.InstallDirectory -like "*``**" -or $package.InstallDirectory -like "*.*") { Write-Message -Message "The package install directory contains invalid characters" -DisplayWarning return } # Check that the install directory exists, otherwise abort if ((Test-Path -Path $package.InstallDirectory) -eq $false) { Write-Message -Message "The install directory doesn't exist: $($package.InstallDirectory)" -DisplayWarning return } if ($PSCmdlet.ShouldProcess("Package:{$name} files", "Copy to the installation directory")) { # Copy package folder to install directory Write-Verbose "Copying over the package files to $($package.InstallDirectory)" Copy-Item -Path "$script:DataPath\packages\$($package.Name)" -Destination $package.InstallDirectory -Container -Recurse } }elseif ($package.Type -eq "ChocolateyPackage") { # TODO: Invoke chocolatey install } # Clean up temporary url-package properties if ($package.Type -eq "UrlPackage") { # Remove the executable properties Write-Verbose "Cleaning up temporary properties" $package.psobject.Properties.Remove("ExecutableName") $package.psobject.Properties.Remove("ExecutableType") # Check in-case the installer wasn't actually downloaded if ((Test-Path -Path "$script:DataPath\packages\$($package.Name)") -eq $true) { if ($PSCmdlet.ShouldProcess("package:{$name} installer", "Delete")) { # Remove the temporarily downloaded installer Write-Verbose "Deleting the downloaded installer" Remove-Item -Path "$script:DataPath\packages\$($package.Name)" -Recurse -Force } } } # Check if the package has a post-install scriptblock to run Write-Verbose "Checking for post-install scriptblock" if ([System.String]::IsNullOrWhiteSpace($package.PostInstallScriptblock) -eq $false) { if ($PSCmdlet.ShouldProcess("post-install scriptblock from package:{$name}", "Execute scriptblock")) { # Convert string to scriptblock and execute Write-Verbose "Coverting scriptblock and executing it" $scriptblock = [scriptblock]::Create($package.PostInstallScriptblock) Invoke-Command -ScriptBlock $scriptblock -ArgumentList $package } } # Set the installed flag for the package Write-Verbose "Setting installed flag to true" $package.IsInstalled = $true } if ($PSCmdlet.ShouldProcess("$script:DataPath\packageDatabase.xml", "Update the package:{$PackageName} installation status")) { # Override xml database with updated package property Write-Verbose "Writing-out data back to database" Export-PackageList -PackageList $packageList } } function Invoke-PMUninstall { <# .SYNOPSIS Uninstalls a ProgramManager package. .DESCRIPTION Invokes an uninstallation process on a ProgramManager.Package that is already installed. .PARAMETER PackageName The name of the ProgramManager package to uninstall. .PARAMETER Confirm If this switch is enabled, you will be prompted for confirmation before executing any operations that change state. .PARAMETER WhatIf If this switch is enabled, no actions are performed but informational messages will be displayed that explain what would happen if the command were to run. .EXAMPLE PS C:\> Invoke-PMUninstall -Name "notepad" This command will uninstall the package named "notepad". .EXAMPLE PS C:\> Get-PMPackage "notepad" | Invoke-PMUninstall This command supports passing in a ProgramManager.Package object, by retrieving it using Get-PMPacakge for example. This command will uninstall the package named "notepad". .INPUTS System.String[] .OUTPUTS None #> [CmdletBinding(SupportsShouldProcess = $true, ConfirmImpact = "Medium")] Param ( [Parameter(Mandatory = $true, Position = 0, ValueFromPipelineByPropertyName = $true)] [AllowEmptyString()] [Alias("Name")] [string[]] $PackageName ) # Check if the xml database exists if ((Test-Path -Path "$script:DataPath\packageDatabase.xml") -eq $true) { # Load all existing PMPackages into a list Write-Verbose "Loading existing packages from database" $packageList = Import-PackageList }else { # The xml database doesn't exist; warn user Write-Message -Message "The database file doesn't exist. Run New-PMPackage to initialise it." -DisplayWarning return } # Iterate through all passed package names foreach ($name in $PackageName) { Write-Verbose "Uninstalling package:{$name}" # Check that the name is not empty if ([System.String]::IsNullOrWhiteSpace($name) -eq $true) { Write-Message -Message "The name cannot be empty" -DisplayWarning return } # Get the package by name Write-Verbose "Retrieving the ProgramManager.Package Object" $package = $packageList | Where-Object { $_.Name -eq $name } # Warn the user if the name is invalid if ($null -eq $package) { Write-Message -Message "There is no package called: $name" -DisplayWarning return } # Check that the package is actually installed if ($package.IsInstalled -eq $false) { Write-Message -Message "The package:{$name} is not installed." -DisplayWarning return } # Check if the package has a on-uninstall scriptblock to run Write-Verbose "Checking for uninstall scriptblock" if ([System.String]::IsNullOrWhiteSpace($package.UninstallScriptblock) -eq $false) { if ($PSCmdlet.ShouldProcess("uninstall scriptblock from package:{$name}", "Execute scriptblock")) { # Convert the string into a scriptblock and execute Write-Verbose "Coverting scriptblock and executing it" $scriptblock = [scriptblock]::Create($package.UninstallScriptblock) Invoke-Command -ScriptBlock $scriptblock -ArgumentList $package } } # Main uninstallation logic if ($package.Type -eq "LocalPackage" -or $package.Type -eq "UrlPackage") { # Launch control panel to allow user to uninstall program, since there is no real # way of uninstalling programs installed through a standard exe installer Write-Verbose "Launching Control Panel" Start-Process appwiz.cpl }elseif ($package.Type -eq "PortablePackage") { # Check whether the folder the package was installed to actually exists if ((Test-Path -Path "$($package.InstallDirectory)\$($package.Name)") -eq $false) { Write-Message -Message "Can't find the package at the expected directory. Was the package folder renamed?" -DisplayWarning return } if ($PSCmdlet.ShouldProcess("Package:{$name} files", "Delete from the installation directory")) { # Delete the package files Write-Verbose "Deleting package files" Remove-Item -Path "$($package.InstallDirectory)\$($package.Name)" -Recurse -Force } }elseif ($package.Type -eq "ChocolateyPackage") { # TODO: invoke chocolatey uninstall } # Set the installed flag for the package Write-Verbose "Setting installed flag to false" $package.IsInstalled = $false } if ($PSCmdlet.ShouldProcess("$script:DataPath\packageDatabase.xml", "Update the package:{$PackageName} installation status")) { # Override xml database with updated package property Write-Verbose "Writing-out data back to database" Export-PackageList -PackageList $packageList } } function New-PMPackage { <# .SYNOPSIS Adds a program to the ProgramManager database. .DESCRIPTION Adds a new ProgramManager.Package to the database for future installation. Accepts the following: - msi/exe installer (local file or url download) - zip binary - chocolatey package .PARAMETER Name The name of the program to add to the database. .PARAMETER LocalPackage Specifies the use of a local installer file located at an available path. .PARAMETER UrlPackage Specifies the use of an installer file located at a url. .PARAMETER PortablePackage Specifies the use of a portable binary file located at a path or url. .PARAMETER ChocolateyPackage Specifies the use of a chocolatey package. .PARAMETER PackageLocation The location of the package. - For Local-Package: file path pointing to the executable - For Url-Package: url pointing to download link - For Portable-Package: file path pointing to the folder/archive/executable .PARAMETER InstallDirectory The directory to which install the pacakge to. .PARAMETER PackageName The name of a chocolatey package. To be used with the -ChocolateyPackage switch. .PARAMETER Note A short note/description to explain what the package entry is. .PARAMETER PreInstallScriptblock A script block which will be executed before the main package installation process. .PARAMETER PostInstallScriptblock A script block which will be executed after the main package installation process. .PARAMETER UninstallScriptblock A script block which will be executed before the uninstallation process. .PARAMETER Confirm If this switch is enabled, you will be prompted for confirmation before executing any operations that change state. .PARAMETER WhatIf If this switch is enabled, no actions are performed but informational messages will be displayed that explain what would happen if the command were to run. .EXAMPLE PS C:\> New-PMPackage -Name "notepad" -LocalPackage -PackageLocation "~\Downloads\notepad.msi" -Note "Notepad msi installer" Adds the locally stored program to the database with the specified name and short note. .EXAMPLE PS C:\> New-PMPackage -Name "notepad" -UrlPackage -PackageLocation "https://download/" -PreInstallScriptblock {Write-Host "installing notepad"} Adds the url-downloaded program to the database with the specified pre-install scriptblock. .EXAMPLE PS C:\> New-PMPackage -Name "notepad" -PortablePackage -PackageLocation "~\Downloads\program.zip" -InstallDirectory "D:\Programs\" Adds the portable program to the database with the specified install directory. .INPUTS None .OUTPUTS None .NOTES For detailed information regarding the scriptblocks, see: about_ProgramManager_scriptblocks #> [CmdletBinding(DefaultParameterSetName = "LocalInstaller", SupportsShouldProcess = $true, ConfirmImpact = "Medium")] Param ( [Parameter(ParameterSetName = "LocalPackage", Mandatory = $true, Position = 0)] [Parameter(ParameterSetName = "UrlPackage", Mandatory = $true, Position = 0)] [Parameter(ParameterSetName = "PortablePackage", Mandatory = $true, Position = 0)] [Parameter(ParameterSetName = "ChocolateyPackage", Mandatory = $true, Position = 0)] [AllowEmptyString()] [string] $Name, [Parameter(ParameterSetName = "LocalPackage", Mandatory = $true, Position = 1)] [switch] $LocalPackage, [Parameter(ParameterSetName = "UrlPackage", Mandatory = $true, Position = 1)] [switch] $UrlPackage, [Parameter(ParameterSetName = "PortablePackage", Mandatory = $true, Position = 1)] [switch] $PortablePackage, [Parameter(ParameterSetName = "ChocolateyPackage", Mandatory = $true, Position = 1)] [switch] $ChocolateyPackage, [Parameter(ParameterSetName = "LocalPackage", Mandatory = $true, Position = 2)] [Parameter(ParameterSetName = "UrlPackage", Mandatory = $true, Position = 2)] [Parameter(ParameterSetName = "PortablePackage", Mandatory = $true, Position = 2)] [AllowEmptyString()] [string] $PackageLocation, [Parameter(ParameterSetName = "LocalPackage", Position = 3)] [Parameter(ParameterSetName = "UrlPackage", Position = 3)] [Parameter(ParameterSetName = "PortablePackage", Mandatory = $true, Position = 3)] [AllowEmptyString()] [string] $InstallDirectory, [Parameter(ParameterSetName = "ChocolateyPackage", Mandatory = $true, Position = 2)] [string] $PackageName, [Parameter(ParameterSetName = "LocalPackage")] [Parameter(ParameterSetName = "UrlPackage")] [Parameter(ParameterSetName = "PortablePackage")] [Parameter(ParameterSetName = "ChocolateyPackage")] [AllowEmptyString()] [string] $Note, [Parameter(ParameterSetName = "LocalPackage")] [Parameter(ParameterSetName = "UrlPackage")] [Parameter(ParameterSetName = "PortablePackage")] [Parameter(ParameterSetName = "ChocolateyPackage")] [AllowEmptyString()] [scriptblock] $PreInstallScriptblock, [Parameter(ParameterSetName = "LocalPackage")] [Parameter(ParameterSetName = "UrlPackage")] [Parameter(ParameterSetName = "PortablePackage")] [Parameter(ParameterSetName = "ChocolateyPackage")] [AllowEmptyString()] [scriptblock] $PostInstallScriptblock, [Parameter(ParameterSetName = "LocalPackage")] [Parameter(ParameterSetName = "UrlPackage")] [Parameter(ParameterSetName = "PortablePackage")] [Parameter(ParameterSetName = "ChocolateyPackage")] [AllowEmptyString()] [scriptblock] $UninstallScriptblock ) # Import all PMPackage objects from the database file Write-Verbose "Loading existing packages from database" $packageList = Import-PackageList # Check that the name is not empty if ([System.String]::IsNullOrWhiteSpace($Name) -eq $true) { Write-Message -Message "The name cannot be empty" -DisplayWarning return } # Check that the name doesn't contain any characters which could cause potential issues further down the line if ($Name -like "*.*" -or $Name -like "*``**" -or $Name -like "*.``**") { Write-Message -Message "The name contains invalid characters" -DisplayWarning return } # Check if name is already taken to avoid conflicts $package = $packageList | Where-Object { $_.Name -eq $Name } if ($null -ne $package) { Write-Message -Message "There already exists a package called: $Name" -DisplayWarning return } # Check that the scriptblocks actually contain code if ($null -ne $PreInstallScriptblock) { if ([System.String]::IsNullOrWhiteSpace($PreInstallScriptblock.ToString()) -eq $true) { Write-Message -Message "The Pre-Install Scriptblock cannot be empty" -DisplayWarning return } if ($PreInstallScriptblock.ToString() -like "``*") { Write-Message -Message "The Pre-Install Scriptblock cannot just be '*'" -DisplayWarning return } } if ($null -ne $PostInstallScriptblock) { if ([System.String]::IsNullOrWhiteSpace($PostInstallScriptblock.ToString()) -eq $true) { Write-Message -Message "The Post-Install Scriptblock cannot be empty" -DisplayWarning return } if ($PostInstallScriptblock.ToString() -like "``*") { Write-Message -Message "The Post-Install Scriptblock cannot just be '*'" -DisplayWarning return } } if ($null -ne $UninstallScriptblock) { if ([System.String]::IsNullOrWhiteSpace($UninstallScriptblock.ToString()) -eq $true) { Write-Message -Message "The Uninstall Scriptblock cannot be empty" -DisplayWarning return } if ($UninstallScriptblock.ToString() -like "``*") { Write-Message -Message "The Uninstall Scriptblock cannot just be '*'" -DisplayWarning return } } # Create new PMpackage object and set its object type Write-Verbose "Creating new ProgramManager.Package Object" $package = New-Object -TypeName psobject $package.PSObject.TypeNames.Insert(0, "ProgramManager.Package") # Add compulsory properties which all packages will have irrespective of type Write-Verbose "Adding universal properties to object: name,type,isinstalled" $package | Add-Member -Type NoteProperty -Name "Name" -Value $Name $package | Add-Member -Type NoteProperty -Name "Type" -Value $PSCmdlet.ParameterSetName $package | Add-Member -Type NoteProperty -Name "IsInstalled" -Value $false if ((Test-Path -Path "$script:DataPath\packages\") -eq $false) { if ($PSCmdlet.ShouldProcess("$script:DataPath\packages", "Create the package store")) { # The packages subfolder doesn't exist. Create it to avoid errors with Move-Item New-Item -ItemType Directory -Path "$script:DataPath\packages\" -Confirm:$false | Out-Null } } # Check that the path is not empty if ([System.String]::IsNullOrWhiteSpace($Path) -eq $true) { Write-Message -Message "The path cannot be empty" -DisplayWarning return } # Check that the path doesn't contain any characters which could cause potential issues or undesirable effects if ($PackageLocation -like "." -or $PackageLocation -like "~" -or $PackageLocation -like ".." ` -or $PackageLocation -like "..." -or $PackageLocation -like "*``**" -or $PackageLocation -like "*.") { Write-Message -Message "The path provided is not accepted for safety reasons" -DisplayWarning return } if ($LocalPackage -eq $true) { Write-Verbose "Detected Local-Pacakge" # Check that the installer path is valid if ((Test-Path -Path $PackageLocation) -eq $false) { Write-Message -Message "There is no valid path pointing to: $PackageLocation" -DisplayWarning return } # Get the details of the executable and check whether it is actually a file $executable = Get-Item -Path $PackageLocation if ($executable.PSIsContainer -eq $true -or $executable.GetType().Name -eq "Object[]") { Write-Message -Message "There is no (single) executable located at the path: $PackageLocation" -DisplayWarning return } if (($executable.Extension -match ".exe|.msi") -eq $false) { Write-Message -Message "There is no installer file located at the path: $PackageLocation" -DisplayWarning return } if ($PSCmdlet.ShouldProcess("File: $PackageLocation", "Move the installer to the package store")) { # Move the executable to the package store Write-Verbose "Copying over installer to \packages\$Name\" New-Item -ItemType Directory -Path "$script:DataPath\packages\$Name\" -Confirm:$false | Out-Null Move-Item -Path $PackageLocation -Destination "$script:DataPath\packages\$Name\$($executable.Name)" -Confirm:$false } # Add executable properties Write-Verbose "Adding properties: executable name & type" $package | Add-Member -Type NoteProperty -Name "ExecutableName" -Value $executable.Name $package | Add-Member -Type NoteProperty -Name "ExecutableType" -Value $executable.Extension # Add install directory if passed in if ([System.String]::IsNullOrWhiteSpace($InstallDirectory) -eq $false) { Write-Verbose "Adding property: install directory" $package | Add-Member -Type NoteProperty -Name "InstallDirectory" -Value $InstallDirectory } }elseif ($UrlPackage -eq $true) { Write-Verbose "Detected Url-Pacakge" # Add url property Write-Verbose "Adding property: download link" $package | Add-Member -Type NoteProperty -Name "Url" -Value $PackageLocation # Add install directory if passed in if ([System.String]::IsNullOrWhiteSpace($InstallDirectory) -eq $false) { Write-Verbose "Adding property: install directory" $package | Add-Member -Type NoteProperty -Name "InstallDirectory" -Value $InstallDirectory } }elseif ($PortablePackage -eq $true) { Write-Verbose "Detected Portable-Pacakge" # Check that a install directory parameter is given in if ([System.String]::IsNullOrWhiteSpace($InstallDirectory) -eq $true) { Write-Message -Message "The install directory path must not be empty" -DisplayWarning return } # Check that the file path is valid if ((Test-Path -Path $PackageLocation) -eq $false) { Write-Message -Message "There is no folder/file located at the path: $PackageLocation" -DisplayWarning return } Write-Verbose "Retrieving item located at: $PackageLocation" $item = Get-Item -Path $PackageLocation # There are multiple items collected under this file path so reject it if ($item.GetType().Name -eq "Object[]") { Write-Message -Message "You cannot specify multiple items in the filepath" -DisplayWarning return } if ((Get-Item -Path $PackageLocation).PSIsContainer -eq $true) { if ($PSCmdlet.ShouldProcess("Folder: $PackageLocation", "Move the package container to the package store")) { # This is a folder so can be moved straight to the package store Write-Verbose "Detected container. Moving folder to \packages\$Name\" Move-Item -Path $PackageLocation -Destination "$script:DataPath\packages\$Name" -Confirm:$false } }else { # This is a file so check if its an archive to extract $file = Get-Item -Path $PackageLocation # Check if the file has an 'archive' attribute if ($file.Extension -eq ".zip" -or $file.Extension -eq ".tar") { if ($PSCmdlet.ShouldProcess("Archive: $PackageLocation", "Extract the archive to temporary path")){ # Extract archive to parent location and delete the original # Must set do this trickery to stop confirmation prompts, since passing -Confirm:$false to Expand-Archive # doen't propogate that to the individual New-Item -Directory commands, and each one would generate a prompt Write-Verbose "Detected archive. Extracting archive to \temp\" $originalConfirmPrefrence = $ConfirmPreference $ConfirmPreference = "None" Expand-Archive -Path $PackageLocation -DestinationPath "$script:DataPath\temp" $ConfirmPreference = $originalConfirmPrefrence Remove-Item -Path $PackageLocation -Force -Confirm:$false } # Set the current directory to the extracted-archive location, initialising loop $currentDir = "$script:DataPath\temp" # Recursively look into the folder heirarchy until there is no more folders containing a single folder # i.e. stops having structure: folder1 -> folder2 -> folder3 -> contents(exe,libs,etc) do { # Get all children within the current folder $children = Get-ChildItem -Path $currentDir # If there is only a single child and its a folder, move down the tree if ($children.Count -eq 1 -and $children[0].PSIsContainer -eq $true) { $currentDir = $children.FullName }else { # Otherwise move the item to the package store if ($PSCmdlet.ShouldProcess("Folder: $currentDir", "Move the package container to the package store")) { Write-Verbose "Moving folder to \packages\$Name\" Move-Item -Path $currentDir -Destination "$script:DataPath\packages\$Name" -Confirm:$false } } } while ($children.Count -eq 1 -and $children[0].PSIsContainer -eq $true) # Clean up the left-over folders Write-Verbose "Cleaning up \temp\" Remove-Item -Path "$script:DataPath\temp\" -Recurse -Force -Confirm:$false }elseif ($file.Extension -eq ".exe") { if ($PSCmdlet.ShouldProcess("File: $PackageLocation", "Move the executable to the package store")) { # This is a portable package with only a single exe file, so move it straight to package store Write-Verbose "Detected executable. Moving program to \packages\$Name\" New-Item -ItemType Directory -Path "$script:DataPath\packages\$Name\" -Confirm:$false | Out-Null Move-Item -Path $PackageLocation -Destination "$script:DataPath\packages\$Name\$($file.Name)" -Confirm:$false } }else { # This is a file of an invalid type for this package type Write-Message -Message "The file specified is neither a executable nor an archive" -DisplayWarning return } } # Add necessary properties Write-Verbose "Adding property: install directory" $package | Add-Member -Type NoteProperty -Name "InstallDirectory" -Value $InstallDirectory }elseif ($ChocolateyPackage -eq $true) { Write-Verbose "Detected Chocolatey-Pacakge" # Add necessary info for chocolatey to work Write-Verbose "Adding property: chocolatey package name" $package | Add-Member -Type NoteProperty -Name "PackageName" -Value $PackageName } # Add optional note property if passed in if ([System.String]::IsNullOrWhiteSpace($Note) -eq $false) { Write-Verbose "Adding property: note" $package | Add-Member -Type NoteProperty -Name "Note" -Value $Note } # Add optional scriptblock proprties if passed in if ($null -ne $PreInstallScriptblock) { Write-Verbose "Adding property: pre-install scriptblock" $package | Add-Member -Type NoteProperty -Name "PreInstallScriptblock" -Value $PreInstallScriptblock.ToString() } if ($null -ne $PostInstallScriptblock) { Write-Verbose "Adding property: post-install scriptblock" $package | Add-Member -Type NoteProperty -Name "PostInstallScriptblock" -Value $PostInstallScriptblock.ToString() } if ($null -ne $UninstallScriptblock) { Write-Verbose "Adding property: post-install scriptblock" $package | Add-Member -Type NoteProperty -Name "UninstallScriptblock" -Value $UninstallScriptblock.ToString() } if ($PSCmdlet.ShouldProcess("$script:DataPath\packageDatabase.xml", "Add the package `'$Name`'")) { # Add new PMPackage to list $packageList.Add($package) # Export-out package list to xml file Write-Verbose "Writing-out data back to database" Export-PackageList -PackageList $packageList } } function Remove-PMPackage { <# .SYNOPSIS Removes a ProgramManager package from the database. .DESCRIPTION Erases the data of a ProgramManager.Package object from the database. .PARAMETER PackageName The name of the package to remove. .PARAMETER RetainFiles If removing a local or portable package, which has files/installers saved within the package store, this switch will move those files to a specific location rather than deleting them. .PARAMETER Path To be used in conjunction with -RetainFiles. This is the path where the package files will be moved into. .PARAMETER Confirm If this switch is enabled, you will be prompted for confirmation before executing any operations that change state. .PARAMETER WhatIf If this switch is enabled, no actions are performed but informational messages will be displayed that explain what would happen if the command were to run. .EXAMPLE PS C:\> Remove-PMPackage -PackageName "notepad" This command will remove the package "notepad" from the database and delete any physical files, such as the installer executable. .EXAMPLE PS C:\ Remove-PMPackage -PackageName "notepad" -RetainFiles -Path "~\Downloads\" This command will remove the package "notepad" from the database, but will not delete any physical files, such as the installer executable. It will move the files to the ~\Download\ foder .EXAMPLE PS C:\ Get-PMPackage "notepad" | Remove-PMPackage This command supports passing in a ProgramManager.Package object, by retrieving it using Get-PMPacakge for example. This command will remove the package "notepad" from the database and delete any physical files, such as the installer executable. .INPUTS System.String .OUTPUTS None #> [CmdletBinding(SupportsShouldProcess = $true, ConfirmImpact = "Medium")] Param ( [Parameter(Mandatory = $true, Position = 0, ValueFromPipelineByPropertyName = $true)] [AllowEmptyString()] [Alias("Name")] [string] $PackageName, [Parameter(Position = 1)] [switch] $RetainFiles, [Parameter(Position = 2)] [AllowEmptyString()] [string] $Path ) # Import all PMPackage objects from the database file Write-Verbose "Loading existing packages from database" $packageList = Import-PackageList # Check that the name is not empty if ([System.String]::IsNullOrWhiteSpace($PackageName) -eq $true) { Write-Message -Message "The package name cannot be empty" -DisplayWarning return } # Check if package name exists Write-Verbose "Retrieving the ProgramManager.Package Object" $package = $packageList | Where-Object { $_.Name -eq $PackageName } if ($null -eq $package) { Write-Message -Message "There is no package called: $PackageName" -DisplayWarning return } # Check that the path is not empty if ($RetainFiles -eq $true -and [System.String]::IsNullOrWhiteSpace($Path) -eq $true) { Write-Message -Message "The path cannot be empty" -DisplayWarning return } # Url or chocolatey packages dont store any files if ($package.Type -eq "LocalPackage" -or $package.Type -eq "PortablePackage") { Write-Verbose "Detected Local-Package or Portable-Package" # Check in case there is no folder for some reason? if ((Test-Path -Path "$script:DataPath\packages\$PackageName\") -eq $false) { Write-Message -Message "There are no files for this package in the package store. This should not happen." -DisplayError return } if ($RetainFiles -eq $true) { Write-Verbose "Detected -RetainFiles flag" # Check that the path to move the files to is valid if ((Test-Path -Path $Path) -eq $false) { Write-Message -Message "The file path does not exist" -DisplayWarning return } # Check that the path doesn't contain any characters which could cause potential issues if ($Path -like "*.*" -or $Path -like "*``**") { Write-Message -Message "The path contains invalid characters" -DisplayWarning return } if ($PSCmdlet.ShouldProcess("Package `'$PackageName`'", "Move the package files to $Path")){ # Move the package files to the specified path Write-Verbose "Moving package files to $Path" Move-Item -Path "$script:DataPath\packages\$PackageName\" -Destination $Path } }else { if ($PSCmdlet.ShouldProcess("Package `'$PackageName`'", "Delete the package files")) { # Remove the package from the package store Write-Verbose "Deleting package files at \packages\$PackageName" Remove-Item -Path "$script:DataPath\packages\$PackageName\" -Recurse -Force } } }elseif ($package.Type -eq "UrlPackage" -and $RetainFiles -eq $true) { # Notify user that -RetainFiles flag has no effect on a url package Write-Message -Message "The flag -RetainFiles has no effect on a url package" -DisplayWarning } if ($PSCmdlet.ShouldProcess("$script:DataPath\packageDatabase.xml", "Remove the package `'$PackageName`'")){ # Remove the PMPackage from the list Write-Verbose "Removing package object from list" $packageList.Remove($package) | Out-Null # Export-out package list to xml file Write-Verbose "Writing-out data back to database" Export-PackageList -PackageList $packageList } } function Set-PMPackage { <# .SYNOPSIS Sets a property of a ProgramManager package. .DESCRIPTION Modifies an existing property for a ProgramManager.Package object. .PARAMETER PackageName The name of the pacakge to modify. .PARAMETER PropertyName The name of the property to modify. .PARAMETER PropertyValue The new value of the property to set. .PARAMETER Confirm If this switch is enabled, you will be prompted for confirmation before executing any operations that change state. .PARAMETER WhatIf If this switch is enabled, no actions are performed but informational messages will be displayed that explain what would happen if the command were to run. .EXAMPLE PS C:\> Set-PMPackage -PackageName "notepad" -PropertyName "Note" -PropertyValue "A new description" This will set the 'Note' property to the newly passed in value for the 'notepad' package. If this property already exists, it will be modified. .EXAMPLE PS C:\ Get-PMPackage "notepad" | Set-PMPackage -PropertyName "Note" -PropertyValue "A new description" This command supports passing in a ProgramManager.Package object, by retrieving it using Get-PMPacakge for example. This will set the 'Note' property to the newly passed in value for the 'notepad' package. .INPUTS System.String .OUTPUTS None #> [CmdletBinding(SupportsShouldProcess = $true, ConfirmImpact = "Medium")] Param ( [Parameter(Mandatory = $true, Position = 0, ValueFromPipelineByPropertyName = $true)] [AllowEmptyString()] [Alias("Name")] [string] $PackageName, [Parameter(Mandatory = $true, Position = 1)] [AllowEmptyString()] [string] $PropertyName, [Parameter(Mandatory = $true, Position = 2)] [AllowEmptyString()] [string] $PropertyValue ) # Import all PMPackage objects from the database file Write-Verbose "Loading existing packages from database" $packageList = Import-PackageList # Check that the name is not empty if ([System.String]::IsNullOrWhiteSpace($PackageName) -eq $true) { Write-Message -Message "The package name cannot be empty" -DisplayWarning return } # Check if package name exists Write-Verbose "Retrieving the ProgramManager.Package Object" $package = $packageList | Where-Object { $_.Name -eq $PackageName } if ($null -eq $package) { Write-Message -Message "There is no package called: $PackageName" -DisplayWarning return } # Check that the property name is not empty if ([System.String]::IsNullOrWhiteSpace($PropertyName) -eq $true) { Write-Message -Message "The property name cannot be empty" -DisplayWarning return } # Check that the property name is valid Write-Verbose "Retrieving property from ProgramManager.Package Object" $property = $package.psobject.properties | Where-Object { $_.Name -eq $PropertyName } if ($null -eq $property) { Write-Message -Message "There is no property called: $PropertyName in package $PackageName" -DisplayWarning return } # Set the value to the newly specified value Write-Verbose "Setting property:{$PropertyName} value to $PropertyValue" $property.Value = $PropertyValue if ($PSCmdlet.ShouldProcess("$script:DataPath\packageDatabase.xml", "Edit the package `'$PackageName`'")){ # Export-out package list to xml file Write-Verbose "Writing-out data back to database" Export-PackageList -PackageList $packageList } } <# Stored scriptblocks are available in [PsfValidateScript()] attributes. This makes it easier to centrally provide the same scriptblock multiple times, without having to maintain it in separate locations. It also prevents lengthy validation scriptblocks from making your parameter block hard to read. Set-PSFScriptblock -Name 'ProgramManager.ScriptBlockName' -Scriptblock { } #> $argCompleter_PackageNames = { param ($commandName, $parameterName, $wordToComplete, $commandAst, $fakeBoundParameters) # Import all PMPackage objects from the database file $packageList = Import-PackageList if ($packageList.Count -eq 0) { Write-Output "" } $packageList.Name | Where-Object { $_ -like "$wordToComplete*" } } $argCompleter_PackagePropertyName = { param ($commandName, $parameterName, $wordToComplete, $commandAst, $fakeBoundParameters) # Get the already typed in package name $packageName = $fakeBoundParameters.PackageName if ($null -ne $packageName) { # Import all PMPackage objects from the database file $packageList = Import-PackageList # Get the package object $package = $packageList | Where-Object { $_.Name -eq $packageName } if ($null -ne $package){ # Get all properties and return ones which match the filter $properties = $package.psobject.properties.Name $properties | Where-Object { $_ -like "$wordToComplete*" } } } } Register-ArgumentCompleter -CommandName Invoke-PMInstall -ParameterName PackageName -ScriptBlock $argCompleter_PackageNames Register-ArgumentCompleter -CommandName Invoke-PMUninstall -ParameterName PackageName -ScriptBlock $argCompleter_PackageNames Register-ArgumentCompleter -CommandName Get-PMPackage -ParameterName PackageName -ScriptBlock $argCompleter_PackageNames Register-ArgumentCompleter -CommandName Set-PMPackage -ParameterName PackageName -ScriptBlock $argCompleter_PackageNames Register-ArgumentCompleter -CommandName Remove-PMPackage -ParameterName PackageName -ScriptBlock $argCompleter_PackageNames Register-ArgumentCompleter -CommandName Set-PMPackage -ParameterName PropertyName -ScriptBlock $argCompleter_PackagePropertyName #endregion Load compiled code |