Modules/businessdev.ALbuild.Apps/Private/Publish-BcAppToDevEndpoint.ps1
|
function Publish-BcAppToDevEndpoint { <# .SYNOPSIS POSTs an .app to a Business Central development service endpoint (the VS Code 'Publish' mechanism). .DESCRIPTION Uploads the .app as multipart/form-data to a '<devUrl>/dev/apps' endpoint with HTTP Basic auth, which publishes + synchronises + installs it as a development (ModernDev) extension - replaceable without a version bump. Mirrors ALbuild V1 / BcContainerHelper -useDevEndpoint. Kept as its own function so callers can resolve the URL however they like (e.g. a container's IP) and so it is mockable in tests. .PARAMETER Url The dev endpoint URL including query string, e.g. 'https://172.17.0.2:7049/BC/dev/apps?SchemaUpdateMode=synchronize'. .PARAMETER AppFile Host path to the .app file to upload. .PARAMETER Credential BC user credential for Basic authentication. .PARAMETER IgnoreSslErrors Accept the endpoint's (self-signed) TLS certificate. Uses the per-handler validator on PowerShell 7 (.NET) and the global ServicePointManager hook on Windows PowerShell 5.1 (.NET Framework). #> [CmdletBinding()] param( [Parameter(Mandatory)] [ValidateNotNullOrEmpty()] [string] $Url, [Parameter(Mandatory)] [ValidateNotNullOrEmpty()] [string] $AppFile, [Parameter(Mandatory)] [pscredential] $Credential, [switch] $IgnoreSslErrors ) Add-Type -AssemblyName System.Net.Http $handler = [System.Net.Http.HttpClientHandler]::new() if ($IgnoreSslErrors) { # PowerShell 7 (.NET) exposes a per-handler validator; Windows PowerShell 5.1 (.NET Framework - the # pipeline runtime) has no such property, so fall back to the global ServicePointManager hook. if ([System.Net.Http.HttpClientHandler].GetProperty('ServerCertificateCustomValidationCallback')) { $handler.ServerCertificateCustomValidationCallback = [System.Net.Http.HttpClientHandler]::DangerousAcceptAnyServerCertificateValidator } [System.Net.ServicePointManager]::ServerCertificateValidationCallback = { $true } [System.Net.ServicePointManager]::SecurityProtocol = [System.Net.ServicePointManager]::SecurityProtocol -bor [System.Net.SecurityProtocolType]::Tls12 } $client = [System.Net.Http.HttpClient]::new($handler) $client.Timeout = [System.Threading.Timeout]::InfiniteTimeSpan $client.DefaultRequestHeaders.ExpectContinue = $false $pair = "$($Credential.UserName):$($Credential.GetNetworkCredential().Password)" $client.DefaultRequestHeaders.Authorization = [System.Net.Http.Headers.AuthenticationHeaderValue]::new( 'Basic', [System.Convert]::ToBase64String([System.Text.Encoding]::ASCII.GetBytes($pair))) $leaf = Split-Path -Path $AppFile -Leaf $multipart = [System.Net.Http.MultipartFormDataContent]::new() $fs = [System.IO.FileStream]::new((Resolve-Path -LiteralPath $AppFile).ProviderPath, [System.IO.FileMode]::Open, [System.IO.FileAccess]::Read) try { $disp = [System.Net.Http.Headers.ContentDispositionHeaderValue]::new('form-data') $disp.Name = $leaf; $disp.FileName = $leaf $content = [System.Net.Http.StreamContent]::new($fs) $content.Headers.ContentDisposition = $disp $multipart.Add($content) $resp = $client.PostAsync($Url, $multipart).GetAwaiter().GetResult() if (-not $resp.IsSuccessStatusCode) { $detail = '' try { $detail = $resp.Content.ReadAsStringAsync().GetAwaiter().GetResult() } catch { $detail = '' } try { $j = $detail | ConvertFrom-Json; if ($j.Message) { $detail = $j.Message } } catch { $null = $_ } throw "Dev endpoint returned $([int]$resp.StatusCode) $($resp.ReasonPhrase). $($detail.Trim())" } } finally { $fs.Dispose() } } |