Functions/Publish-BcAddin.ps1

<#
    .Synopsis
    Installs DotNet Assemblies into the Microsoft Dynamics 365 Business Central Addin folder.
     
    .Description
    The parameter Path is the source path of all dotnet addins.
    The target folder (root add-in folder) can be supplied directly with param Destination,
     or can be received dynamically based on the BC version or a Business Central Service Tier.
 
    .Example
    Publish-BcAddin -Path 'c:\release\dotnet\add-in' -ServerInstance 'BC170Test'
 
    .Example
    Publish-BcAddin `
        -Path @('C:\dotnet\PDF On-Premises', 'C:\dotnet\SharePoint On-Premises') `
        -Destination 'C:\Program Files\Microsoft Dynamics 365 Business Central\160\Service\Add-ins'
 
    .Example
    Publish-BcAddin -Path 'c:\release\dotnet' -BcVersion 'bc15'
 
#>


function Publish-BcAddin {

    [CmdletBinding(DefaultParameterSetName='ServerInstance')]
    param(
        # The path to the folder(s) to publish to the add-in folder.
        # E.g. 'C:\dotnet\PDF On-Premises' or @('C:\dotnet\PDF On-Premises', 'C:\dotnet\SharePoint On-Premises')
        [Parameter(Mandatory = $true)]
        [string[]] $Path,

        # Publishes the assemblies into the supplied BC addinfolder.
        # E.g. 'C:\Program Files\Microsoft Dynamics 365 Business Central\160\Service\Add-ins'
        [Parameter(ParameterSetName='Destination', Mandatory = $true)]
        [string] $Destination,

        # Publishes the assemblies into the BC addinfolder compatible with the supplied serverinstance.
        # E.g. 'BC170'
        [Parameter(ParameterSetName='ServerInstance', Mandatory = $true)]
        [string] $ServerInstance,
        
        # Publishes the assemblies into the BC addinfolder compatible with the supplied BcVersion.
        # E.g. 'bc17' or 'bc15'.
        [Parameter(ParameterSetName='BcVersion', Mandatory = $true)]
        [String] $BcVersion,

        # Location to write the logfile to. Default location is '?:\ProgramData\4ps\dotnetdeployment'.
        # E.g. 'C:\BcInstallation\Log'
        [string] $LogFilePath = (Join-Path -Path $env:ProgramData -ChildPath '4ps\dotnetdeployment')
    )

    "
      _____ _ _ _ _ _ _ _ _ _ _
     | __ \ | | | (_) | | | \ | | | | /\ | | | (_)
     | |__) | _| |__ | |_ ___| |__ | \| | ___| |_ / \ __| | __| |_ _ __ ___
     | ___/ | | | '_ \| | / __| '_ \ | . `` |/ _ \ __| / /\ \ / _`` |/ _`` | | '_ \/ __|
     | | | |_| | |_) | | \__ \ | | | _| |\ | __/ |_ / ____ \ (_| | (_| | | | | \__ \
     |_| \__,_|_.__/|_|_|___/_| |_| (_)_| \_|\___|\__| /_/ \_\__,_|\__,_|_|_| |_|___/
        4PS v{0}
    "
 -f $MyInvocation.MyCommand.Module.Version | Write-Host

    # Validate PowerShell version
    if (-not $PSVersionTable.PSVersion.Major -ge '5') {

        $msg = 'Powershell 5.0 or higher required to continue. Current version is {0}.{1}' -f `
            $PSVersionTable.PSVersion.Major,
            $PSVersionTable.PSVersion.Minor
        throw $msg
    }
    'PowerShell version {0} is compatible.' -f $PSVersionTable.PSVersion.ToString() | Write-Host

    # Validate if PowerShell is started as administrator
    $windowsIdentity = ([Security.Principal.WindowsPrincipal][Security.Principal.WindowsIdentity]::GetCurrent())
    $elevated = ($windowsIdentity.IsInRole([Security.Principal.WindowsBuiltInRole]::Administrator))
    if (-not $elevated) {
        $msg  = 'You need administrative privileges to deploy Business Central add-ins. '
        $msg += 'Start the script in a Powershell session launched as administrator.'
        throw $msg
    }
    'Script is executed as administrator.' | Write-Host

    # Validate if script is executed in a 64 bit process.
    if(-not [Environment]::Is64BitProcess){
        $msg = 'This script needs to be executed as a 64 bit process. Current process is x86 (32bit).'
        throw $msg
    }
    'Session is 64 bit.' | Write-Host
    
    try{ Stop-Transcript }catch{}

    # Create logfile
    $logFile = Join-Path -Path $LogFilePath -ChildPath ('{0}_fps.log' -f 
                                (Get-Date).ToString('yyyy-MM-dd_HH.mm.ss'))
    
    New-Item -Path $logFile -ItemType File -Force | Out-Null

    # Remove old logfiles (keep max 10 logfiles)
    $LogFiles = Get-ChildItem $LogFilePath -File -Filter '*_fps.log'
    if($LogFiles.Count -gt 10){
        $Exclude = $LogFiles | Sort-Object -Property CreationTime -Descending | Select-Object -First 10
        Get-ChildItem (Split-Path $logFile -Parent) -Exclude $Exclude | Remove-Item -Recurse -Force -ErrorAction SilentlyContinue
    }

    Start-Transcript -path $logFile -append


    ### Start script

    if(!$Destination){

        'Destination is not set. Retreiving destination dynamically...' | Write-Host
    
        if($BcVersion){
            if((Get-BcVersion -BcVersion $BcVersion) -eq $false){
                "The supplied BC/NAV version '{0}' is not valid. Valid values are for example 'BC17', 'BC13', 'NAV2017'" -f $BcVersion | Write-Warning
                return
            }
            [hashtable] $BcVersion = Get-BcVersion -BcVersion $BcVersion
        }

        if($ServerInstance){
            $BcVersionFolder = ([version](Get-BCServerInstance -ServerInstance $ServerInstance).Version).Major * 10
            [hashtable] $BcVersion = Get-BcVersion -VersionFolder $BcVersionFolder
        }

        # Get installation folder
        $Service = Get-BcComponent -BcVersion $BcVersion.Version | Where-Object Component -eq 'NST'

        if (-not $Service.IsInstalled) {
            Write-Warning 'Cannot find BC add-in folder because the BC Service folder is not found. Make sure the BC Service is installed.'    
            return $false
        }

        $Destination = Join-Path $Service.InstallLocation 'Add-ins\_4PS'

    } else {
        'Supplied destination: ''{0}''.' -f $Destination | Write-Host
        
        if((Split-Path $Destination -Leaf) -ne '_4PS'){
            $Destination = Join-Path $Destination '_4PS'
        }
    }

    'Destination is set to ''{0}''.' -f $Destination | Write-Host

    # Test if source and target folder exists
    $Path | ForEach-Object {
        if((Test-Path $_) -eq $false){
            $msg = 'Cannot find assembly path. Please check the path ''{0}''' -f $_
            Write-Error $msg
        } else {
            'Source path ''{0}'' exists.' -f $_ | Write-Host
        }
    }

    if((Test-Path (Split-Path $Destination -Parent)) -eq $false){
        $msg = 'Cannot find the destination path. Please check the path ''{0}''' -f (Split-Path $Destination -Parent)
        Write-Error $msg
    } else {
        'Root add-in directory ''{0}'' exists.' -f (Split-Path $Destination -Parent) | Write-Host
    }

    if((Test-Path $Destination) -eq $false){
        'Folder ''{0}'' doesn''s exist in root add-in folder. Creating folder..' -f (Split-Path $Destination -Leaf) | Write-Host
        New-Item $Destination -ItemType Directory -Force
    } else {
        'Destination ''{0}'' exists.' -f $Destination | Write-Host
    }

    # Copy source folders to destination

    foreach($dir in $Path){
        
        # If the folder starts with a number, add a '_' prefix.
        if((Split-Path $dir -Leaf) -match '^[0-9]+.*'){
            $target = Join-Path $Destination ('_{0}' -f (Split-Path $dir -Leaf))
        } else {
            $target = Join-Path $Destination (Split-Path $dir -Leaf)
        }

        # If the folder exists, remove it first.
        if((Test-Path $target) -eq $true){
            if ($Force){
                'The destination folder ''{0}'' already exists. Removing the folder..' -f $target | Write-Host
                # TOD: Try Catch on locked item, if lock ask to stop all BC services.
                Remove-Item -Path $target -Recurse -Force
            } else {
                $msg = 'The destination folder ''{0}'' already exists. Please remove the folder first or use the -Force parameter to replace the folder.' -f $target
                Write-Warning $msg
                continue
            }
        }
        
        "`nCopying content from: `n'{0}' to `n'{1}'" -f $dir, $target | Write-Host
        Copy-Item -Path $dir -Destination $target -Recurse -Force

        # If the add-in folder contains .zip files, extract the archives.
        $archives = Get-ChildItem -Path $target -Recurse -Filter '*.zip'
        
        if($archives.Count -ge 1){
            'There are {0} .zip archives found in the folder ''{1}''. Extracting the content...' -f $archives.Count, (Split-Path $target -Leaf) | Write-Host
        }

        $archives | ForEach-Object {
            
            $archiveTarget = Join-Path $target ([io.path]::GetFileNameWithoutExtension($_))

            if((Test-Path $archiveTarget) -eq $false){
                New-Item $archiveTarget -ItemType Directory -Force
            }
            
            if ((Get-Command Expand-Archive).Source -eq 'Pscx'){
                Expand-Archive -Path $_.FullName -OutputPath $archiveTarget -Force
            } else {
                Expand-Archive -Path $_.FullName -DestinationPath $archiveTarget -Force
            }

            Remove-Item $_.FullName -Force
            
        }
    }
    
    'Installation completed.' | Write-Host

    try{ Stop-Transcript }catch{}
}

Export-ModuleMember -Function Publish-BcAddin