It may not be the most common thing, however it may happen that you would need to upload an artifact to a maven repository in Nexus via PowerShell. In order to achieve that, we will use Nexus REST API which for this task requires a multipart/form-data POST to /service/local/artifact/maven/content
resource. This is however not a trivial task. It is notoriously difficult to manage a Multipart/form-data standard in PowerShell, as I already described in one of my previous post PowerShell tips and tricks – Multipart/form-data requests. As you can see this type of call is necessary in order to upload an artifact to your Nexus server. I will not get in a details about Multipart/form-data requests and if interested about the details, you can check just mentioned post.
For this purpose I created two cmdlets that will allow me to upload an artifact by supplying GAV parameters or by passing the GAV definition in a POM file. A couple of simple functions do support both of these cmdlets.
GAV definition in a POM file
As the heading suggests, this cmdlet will let you upload your artifact and specify the GAV parameters via a POM file.
function Import-ArtifactPOM()
{
[CmdletBinding()]
param
(
[string][parameter(Mandatory = $true)][ValidateNotNullOrEmpty()]$EndpointUrl,
[string][parameter(Mandatory = $true)][ValidateNotNullOrEmpty()]$Repository,
[string][parameter(Mandatory = $true)][ValidateNotNullOrEmpty()]$PomFilePath,
[string][parameter(Mandatory = $true)][ValidateNotNullOrEmpty()]$PackagePath,
[System.Management.Automation.PSCredential][parameter(Mandatory = $true)]$Credential
)
BEGIN
{
if (-not (Test-Path $PackagePath))
{
$errorMessage = ("Package file {0} missing or unable to read." -f $PackagePath)
$exception = New-Object System.Exception $errorMessage
$errorRecord = New-Object System.Management.Automation.ErrorRecord $exception, 'ArtifactUpload', ([System.Management.Automation.ErrorCategory]::InvalidArgument), $PackagePath
$PSCmdlet.ThrowTerminatingError($errorRecord)
}
if (-not (Test-Path $PomFilePath))
{
$errorMessage = ("POM file {0} missing or unable to read." -f $PomFilePath)
$exception = New-Object System.Exception $errorMessage
$errorRecord = New-Object System.Management.Automation.ErrorRecord $exception, 'ArtifactUpload', ([System.Management.Automation.ErrorCategory]::InvalidArgument), $PomFilePath
$PSCmdlet.ThrowTerminatingError($errorRecord)
}
Add-Type -AssemblyName System.Net.Http
}
PROCESS
{
$repoContent = CreateStringContent "r" $Repository
$groupContent = CreateStringContent "hasPom" "true"
$pomContent = CreateStringContent "file" "$(Get-Content $PomFilePath)" ([system.IO.Path]::GetFileName($PomFilePath)) "text/xml"
$streamContent = CreateStreamContent $PackagePath
$content = New-Object -TypeName System.Net.Http.MultipartFormDataContent
$content.Add($repoContent)
$content.Add($groupContent)
$content.Add($pomContent)
$content.Add($streamContent)
$httpClientHandler = GetHttpClientHandler $Credential
return PostArtifact $EndpointUrl $httpClientHandler $content
}
END { }
}
In order to invoke this cmdlet you will need to supply the following parameters:
- EndpointUrl – Address of your Nexus server
- Repository – Name of your repository in Nexus
- PomFilePath – Full, literal path pointing to your POM file
- PackagePath – Full, literal path pointing to your Artifact
- Credential – Credentials in the form of PSCredential object
I will create a POM file with the following content:
<project>
<modelVersion>4.0.0</modelVersion>
<groupId>com.mycompany.app</groupId>
<artifactId>my-app</artifactId>
<version>3</version>
</project>
And invoke my cmdlet in the following way:
$server = "http://nexus.maio.com"
$repoName = "maven"
$pomFile = "C:\Users\majcicam\Desktop\pom.xml"
$package = "C:\Users\majcicam\Desktop\junit-4.12.jar"
$credential = Get-Credential
Import-ArtifactPOM $server $repoName $pomFile $package $credential
If everything is correctly setup, you will be first asked to provide the credentials, then the upload will start. If it succeeds, you will receive back a string like this:
{"groupId":"com.mycompany.app","artifactId":"my-app","version":"3","packaging":"jar"}
It is a json representation of the information about imported package.
Manually supplying GAV parameters
The other cmdlet is based on a similar principle, however it doesn’t require a POM file to be passed in, instead it let you provide GAV values as parameter to the call.
function Import-ArtifactGAV()
{
[CmdletBinding()]
param
(
[string][parameter(Mandatory = $true)][ValidateNotNullOrEmpty()]$EndpointUrl,
[string][parameter(Mandatory = $true)][ValidateNotNullOrEmpty()]$Repository,
[string][parameter(Mandatory = $true)][ValidateNotNullOrEmpty()]$Group,
[string][parameter(Mandatory = $true)][ValidateNotNullOrEmpty()]$Artifact,
[string][parameter(Mandatory = $true)][ValidateNotNullOrEmpty()]$Version,
[string][parameter(Mandatory = $true)][ValidateNotNullOrEmpty()]$Packaging,
[string][parameter(Mandatory = $true)][ValidateNotNullOrEmpty()]$PackagePath,
[System.Management.Automation.PSCredential][parameter(Mandatory = $true)]$Credential
)
BEGIN
{
if (-not (Test-Path $PackagePath))
{
$errorMessage = ("Package file {0} missing or unable to read." -f $packagePath)
$exception = New-Object System.Exception $errorMessage
$errorRecord = New-Object System.Management.Automation.ErrorRecord $exception, 'XLDPkgUpload', ([System.Management.Automation.ErrorCategory]::InvalidArgument), $packagePath
$PSCmdlet.ThrowTerminatingError($errorRecord)
}
Add-Type -AssemblyName System.Net.Http
}
PROCESS
{
$repoContent = CreateStringContent "r" $Repository
$groupContent = CreateStringContent "g" $Group
$artifactContent = CreateStringContent "a" $Artifact
$versionContent = CreateStringContent "v" $Version
$packagingContent = CreateStringContent "p" $Packaging
$streamContent = CreateStreamContent $PackagePath
$content = New-Object -TypeName System.Net.Http.MultipartFormDataContent
$content.Add($repoContent)
$content.Add($groupContent)
$content.Add($artifactContent)
$content.Add($versionContent)
$content.Add($packagingContent)
$content.Add($streamContent)
$httpClientHandler = GetHttpClientHandler $Credential
return PostArtifact $EndpointUrl $httpClientHandler $content
}
END { }
}
In order to invoke this cmdlet you will need to supply the following parameters:
- EndpointUrl – Address of your Nexus server
- Repository – Name of your repository in Nexus
- Group – Group Id
- Artifact – Maven artifact Id
- Version – Artifact version
- Packaging – Packaging type (at ex. jar, war, ear, rar, etc.)
- PackagePath – Full, literal path pointing to your Artifact
- Credential – Credentials in the form of PSCredential object
An example of invocation:
$server = "http://nexus.maio.com"
$repoName = "maven"
$group = "com.test"
$artifact = "project"
$version = "2.4"
$packaging = "jar"
$package = "C:\Users\majcicam\Desktop\junit-4.12.jar"
$credential = Get-Credential
Import-ArtifactGAV $server $repoName $group $artifact $version $packaging $package $credential
If all goes as expected you will again receive a response confirming the imported values.
{"groupId":"com.test","artifactId":"project","version":"2.4","packaging":"jar"}
Supporting functions
As you could see, my cmdlets relay on a couple of functions. They are essential in this process so I will analyse them one by one.
function CreateStringContent()
{
param
(
[string][parameter(Mandatory = $true)][ValidateNotNullOrEmpty()]$Name,
[string][parameter(Mandatory = $true)][ValidateNotNullOrEmpty()]$Value,
[string]$FileName,
[string]$MediaTypeHeaderValue
)
$contentDispositionHeaderValue = New-Object -TypeName System.Net.Http.Headers.ContentDispositionHeaderValue -ArgumentList @("form-data")
$contentDispositionHeaderValue.Name = $Name
if ($FileName)
{
$contentDispositionHeaderValue.FileName = $FileName
}
$content = New-Object -TypeName System.Net.Http.StringContent -ArgumentList @($Value)
$content.Headers.ContentDisposition = $contentDispositionHeaderValue
if ($MediaTypeHeaderValue)
{
$content.Headers.ContentType = New-Object -TypeName System.Net.Http.Headers.MediaTypeHeaderValue $MediaTypeHeaderValue
}
return $content
}
function CreateStreamContent()
{
param
(
[string][parameter(Mandatory = $true)][ValidateNotNullOrEmpty()]$PackagePath
)
$packageFileStream = New-Object -TypeName System.IO.FileStream -ArgumentList @($PackagePath, [System.IO.FileMode]::Open)
$contentDispositionHeaderValue = New-Object -TypeName System.Net.Http.Headers.ContentDispositionHeaderValue "form-data"
$contentDispositionHeaderValue.Name = "file"
$contentDispositionHeaderValue.FileName = Split-Path $packagePath -leaf
$streamContent = New-Object -TypeName System.Net.Http.StreamContent $packageFileStream
$streamContent.Headers.ContentDisposition = $contentDispositionHeaderValue
$streamContent.Headers.ContentType = New-Object -TypeName System.Net.Http.Headers.MediaTypeHeaderValue "application/octet-stream"
return $streamContent
}
First two functions are there to create a correct HTTP content. Each content object we create will correspond to a content-disposition header. The first function will return a string type values for the just mentioned header, meanwhile the Stream content will return a stream that will be consumed later on, having as a value octet-stream representation of our artifact.
GetHttpClientHandler
function is a helper that will create the right http client handler that contains the credentials to be used for our call.
function GetHttpClientHandler()
{
param
(
[System.Management.Automation.PSCredential][parameter(Mandatory = $true)]$Credential
)
$networkCredential = New-Object -TypeName System.Net.NetworkCredential -ArgumentList @($Credential.UserName, $Credential.Password)
$httpClientHandler = New-Object -TypeName System.Net.Http.HttpClientHandler
$httpClientHandler.Credentials = $networkCredential
return $httpClientHandler
}
Last but not least, a function that actually invokes the post call to our server.
function PostArtifact()
{
param
(
[string][parameter(Mandatory = $true)]$EndpointUrl,
[System.Net.Http.HttpClientHandler][parameter(Mandatory = $true)]$Handler,
[System.Net.Http.HttpContent][parameter(Mandatory = $true)]$Content
)
$httpClient = New-Object -TypeName System.Net.Http.Httpclient $Handler
try
{
$response = $httpClient.PostAsync("$EndpointUrl/service/local/artifact/maven/content", $Content).Result
if (!$response.IsSuccessStatusCode)
{
$responseBody = $response.Content.ReadAsStringAsync().Result
$errorMessage = "Status code {0}. Reason {1}. Server reported the following message: {2}." -f $response.StatusCode, $response.ReasonPhrase, $responseBody
throw [System.Net.Http.HttpRequestException] $errorMessage
}
return $response.Content.ReadAsStringAsync().Result
}
catch [Exception]
{
$PSCmdlet.ThrowTerminatingError($_)
}
finally
{
if($null -ne $httpClient)
{
$httpClient.Dispose()
}
if($null -ne $response)
{
$response.Dispose()
}
}
}
This is all the necessary to upload an artifact to our Nexus server. You can find the just show scripts also on GitHub in the following repository tfs-build-tasks. Further information about programtically uploading artifacts into Nexus can be found in the following post, How can I programatically upload an artifact into Nexus?.