functions/New-PMPackage.ps1

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 LocalPackage: file path pointing to the executable
        - For UrlPackage: url pointing to download link
        - For PortablePackaage: file path pointing to the folder
         
    .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 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
         
    #>
    
    
    [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
        
    )
    
    # 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
    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
    $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
            
        }
        
    }
    
    # Create PMpackage object
    Write-Verbose "Creating new ProgramManager.Package Object"
    $package = New-Object -TypeName psobject 
    $package.PSObject.TypeNames.Insert(0, "ProgramManager.Package")
    
    # Add compulsory properties
    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 "...") {
            
        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 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 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 for the do-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 folder1 -> folder2 -> folder3 -> contents
                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
                    # Otherwise move the item to the package store
                    if ($children.Count -eq 1 -and $children[0].PSIsContainer -eq $true) {
                        
                        $currentDir = $children.FullName
                        
                    }else {
                        
                        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)
                
                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
                    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 ($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
        
    }
    
    
}