Uploading Artifacts into Nexus Repository via PowerShell

Mario Majcica - Featured Image

Nexus Repository Manager and PowerShell

It may not be the most common thing, however it may happen that you 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 the 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 lets you provide GAV values as parameters 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 that is necessary to upload an artifact to our Nexus server. You can find the scripts also on GitHub in the following repository tfs-build-tasks. Further information about programmatically uploading artifacts into Nexus can be found in the following post, How can I programatically upload an artifact into Nexus?.

The following two tabs change content below.
Mario has shown his passion for computer and electronics since an early age. This drove his education towards IT and this dedication transformed itself into a full time job. During the past 10 years, he built tangible experience over application development and best practices, mostly on .Net platform. This also helped him understand the importance of the development process and the tooling supporting it. In the recent years, most of his assignments focused over ALM tasks helping companies and development teams with continuous integration, test automation, deployment, integration, and last but not least, enterprise application development. He has worked on all stages of the product development lifecycle, using an extensive range of technical skills, always striving for elegant solutions. Alongside the development skills, he has built hands-on experience on deploying, administering, using and maintaining the Microsoft Team Foundation Server. Mario has been successful in many projects and has worked on multiple platforms with an acute ability to pick up new technologies quickly. He is dedicated and proactive and always tries to communicate in manageable terms that are understandable to both technical and non-technical colleagues and customers. His main focus is always on bringing value to his customers. Mario shares this knowledge freely on his blog and as a speaker at numerous conferences.
Authors

Related posts

11 Comments

  1. Jon Whitwam said:

    Hello, I am trying to upload some artefacts into a Nexus repository, but am having an issue with your “CreateStringContent” function. The $Value parameter on this function does not seem to be making it through to the new System.Net.Http.StringContent object ($content), so when the function returns, the value is null, which does not work. Any ideas on this?

    • Mario Majcica said:

      Hi John,

      based on experience, if null is returned, usually is because some parameters passed into that function are not correct and the object creation fails. Could you modify that function so it prints out the values of the parameters and check them? Can you show me your invocation so that I can also try to debug it?
      Also, maybe trivial as a question, but just to be sure, is on the machine that you use to invoke this function installed .NET 4.5? HttpClient and relevant classes are available only from version 4.5 beyond.

      Regards,
      Mario

  2. Jon Whitwam said:

    Hello Mario,

    Firstly, many thanks for coming back to me on this – it’s much appreciated!

    I can confirm that I have the following .NET Framework version installed on my PC:

    Version
    ——-
    2.0.50727.5420
    3.0.30729.5420
    3.5.30729.5420
    4.5.51209

    I am using PowerGUI, which allows me to debug and inspect the variables/code, and I see that the $Value parameter is being passed (I am calling the Import-ArtifactGAV function in exactly the same way as your code) and it is being passed into the CreateStringContent function correctly, but the

    $content = New-Object -TypeName System.Net.Http.StringContent -ArgumentList @($Value)

    line does not seem to be populating that parameter into the $content variable anywhere (hence the $null value). I’m really confused! :$

    Regards,

    Jon

    • Mario Majcica said:

      Hi Jon,

      this means that an instance of the StringContent class can’t be created. This leads me to think that the problem lies in the $Value parameter. StringContent expect as an argument a string, although through the PowerShell is very permissive on types. Can you try making a Write-Host $Value.GetType() and check the type of the $Value variable? I tried the example again on a different PC than my main and it all went well, can’t get it to fail. Can you send me the whole ps1 file via email at – my first name AT my last name DOT com?
      Also can you try to make a $content = New-Object -TypeName System.Net.Http.StringContent “test” and see if the object is instantiated correctly. If so, then we have a problem with the argument we are passing in and we can focus on that.

      Mario

  3. Cansın Aldanmaz said:

    Hi Mario,
    First of all thank you for this excellent article. I am trying to implement your code into my scripts. But i got an error:
    I have created a new repository named cnsn in my local nexus. Nexus url is http://localhost:8081/cnsn/ and newly created reporsitory url is http://localhost:8081/cnsn/repository/cnsn/

    So parameters are as following:
    $server = “http://localhost:8081/cnsn/repository/cnsn/”
    $repoName = “cnsn”
    $pomFile = “C:\pom.xml”
    $package = “C:\diskraporu.jar”
    $credential = Get-Credential
    Import-ArtifactPOM $server $repoName $pomFile $package $credential

    I am getting following error from nexus:
    + return PostArtifact $EndpointUrl $httpClientHandler $content
    + ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
    + CategoryInfo : OperationStopped: (:) [PostArtifact], HttpRequestException
    + FullyQualifiedErrorId : Status code MethodNotAllowed. Reason Method Not Allowed. Server reported the followi
    ng message:

    405 – Nexus Repository Manager


    (new Image).src=”http://localhost:8081/cnsn/favicon.ico?3.0.0-03″

    Nexus Repository Manager

    OSS 3.0.0-03

    Error 405
    Method Not Allowed

    POST

    .,PostArtifact

    Thank you,
    Regards.

    • Mario Majcica said:

      Hi Cansin,

      I see that as the url to server you are pointing to the repo instead to the server itself. This will result in wrongly set url in PostArtifact cmdlet. Nexus REST API resource is set in the following way “$EndpointUrl/service/local/artifact/maven/content” and passing in the “http://localhost:8081/cnsn/repository/cnsn/” will result in wrong resource address. Try setting the server variable to $server = “http://localhost:8081″.

      Mario

    • Mario Majcica said:

      Just to wrap up, mount point for this resource is: /service/local/artifact/maven/content. You can find more details about it in https://repository.sonatype.org/nexus-restlet1x-plugin/default/docs/path__artifact_maven_content.html
      The address you need to pass in as the EndpointUrl parameter is your server URL plus the chosen context root and not the repository URL. From your question I’m not sure about the context root you have chosen so you need to check verify it on your own (it is specified in nexus.properties configuration file under nexus-webapp-context-path property.

      I hope this helps.

      Mario

  4. mark Schrijver said:

    Hello,

    Thanks for the clear article. It works as advertised. We have a slightly Different use case. We have 2 files, ons jar contains the binary class files and a second d one contains the source files. I tried adding the second file as an additional all multiparty form parameter, but the. I get the message that I’m not allowed to update the repository. How would I go about uploading two jar files with a single pom in this way?

  5. Daniel said:

    Hi,

    I have tried your script with following parameters:

    $server = “http://:8081”
    $repoName = “Services”
    $group = “AuthenticationService”
    $artifact = “AuthenticationService.Setup”
    $version = “1.1.23003”
    $packaging = “rar”
    $package = “C:\Temp\TMP\3\SentrySuiteReportService.rar”
    $credential = Get-Credential

    I always get the following error:
    PostArtifact : Status code MethodNotAllowed. Reason Method Not Allowed. Server reported the following message:

    Error 40
    Method Not Allowed
    HTTP method POST is not supported by this URL

    Server-Info:
    Nexus Repository Manager
    OSS 3.2.0-01

    Does this server-version not support the upload? Which privileges do I need?

  6. Daniel said:

    Typo in comment: **$package = “C:\Temp\TMP\3\AuthenticationServiceInstaller.rar”** is the right file I did use

  7. Kaz said:

    Hi Mario I am getting following error even for first time upload using the scripts where as with manual upload I can upload the zip file successfully

    400 – Repository with ID=’releases’ does not allow updating artifacts

    with both of your functions Can you please help and thanks in advance
    Regards,
    Kazim

*

Top