Jump to content

How do I retrieve an attachment using the API


Recommended Posts

I am writing a powershell script to automate one particular ticket type that can be raised by our users.  I've been able to create a report that lists all open tickets of the relevant type, and I'm able to retrieve the report using the techniques described at 

However, each ticket includes an attachment that needs to be passed to the automation.  I can use smGetAttachments to retrieve information about the attachment, but I'm stuck how to retrieve the attachment itself from powershell.

smGetAttachments returns fields like the below:

h_pk_id           : 16271
h_request_id      : SR00040620
h_contentlocation : /cafs_raw/fs_entity/8d0703328b4819440d1676c463783216de5b3605.data
h_filename        : xxxxxxxx mail merge- yyyyyy letters.doc
h_size            : 1566208
h_timestamp       : 2020-08-06 10:38:27Z
h_visibility      : trustedGuest

I'd expect to take the h_contentlocation field and concatenate this to my instance URL plus some intermediate folder  to download the actual attachment using system.net.webclient, much as per the referenced topic.  However, I'm unable to discover what to put between the instance URL and the h_contentlocation.

Or is some entirely different technique required?

Regards

Bill

Link to comment
Share on other sites

Hi @BillP,

Your intuition is quite right, you would just need to concatenate it to your instance URL - the intermediate folder you are looking for is "dav". In the full URL, "/dav/" would replace the "/xmlmc/" bit.

You will still need to pass the Authorization on via the header in your GET request.

Link to comment
Share on other sites

I did a little investigation and I tried an invalid API key, which gave a 403 (Forbidden) error, whereas my real API key gave a 401 (Unauthorized) error, from which I deduce that the API KEY is being checked, i.e. passing the Authentication (identity is valid) but is failing Authorization (identity is not allowed to do the thing).  The 401 accords with https://tools.ietf.org/html/rfc7235#page-6 (when credentials are supplied) though I'm not sure that the 403 is right for the other case.  I may be being over-picky.

Link to comment
Share on other sites

@VictorCan you clarify that, please?

https://eurapi.hornbill.com/myorg/dav/reports/109/Documotive Requests_5008.csv
succeeds

but

https://eurapi.hornbill.com/myorg/dav/cafs_raw/fs_entity/8d0703328b4819440d1676c463783216de5b3605.data
fails (401)

in both cases the calling code is based on the referenced article above, and is (in the second case):

        $url = $baseEndpoint + "/dav" + $attachmentDetails.h_contentlocation
        Try {
            $wc = New-Object System.Net.WebClient
            $key = $HornbillAPIKey
           # $key = "abcdef1234567890abcdef1234567890"
            $wc.Headers.Add("Authorization","ESP-APIKEY " + $key)
        
            $wc.DownloadFile($url,$outputLocation)
#        return -1
        }
        Catch {
            Write-Error $_.Exception.Message
            exit 0
            #echo("hey ho pip and dandy")
            #return $apiResponse
        }

you will observe the fake API KEY (currently commented out).

My deduction - possibly wrong - is that the api key's account has group rights to access dav/reports but does not have group rights to dav/cafs_raw , hence my framing my question in terms of permissions or roles that might need to be applied to the account.  Have I misinterpreted the symptoms?  Is there a different way I should be downloading from dav/cafs_raw ?

Thank you for your continuing patience...

Link to comment
Share on other sites

Hi @BillP,

GET requests to dav/cafs_raw/... do indeed work with API keys, and you're 100% correct that the 401 means that the account that is associated to the API doesn't have access, as only a super user has access to that endpoint... 

BUT! You can send a GET request to the following to programatically download attachments from requests, as long as the user who is associated to the API key has access to the request:

https://live.hornbill.com/YOURINSTANCEID/php/attachment.php?application=com.hornbill.servicemanager&entity=Requests&key=IN00012345&filepath=attachmentname.pdf&secure=true

Where YOURINSTANCEID is the ID of your Hornbill instance, IN00012345 is the request reference, and attachmentname.pdf is the name of the file attachment on the request. Make sure you send the API key in the headers as usual, and it should just work.

Let me know how you get on with this.

Cheers,

Steve

  • Like 2
Link to comment
Share on other sites

Hi @Steve G

I'm happy to confirm that your alternative works.  So we now have an end-to-end mechanism for extracting a specific class of problems from Hornbill and delivering them - with their associated artifacts - to a powershell automation.  Now to automate resolving of the ticket!

Thank you, and your colleagues for your assistance.

Regards

Bill

  • Like 1
Link to comment
Share on other sites

  • 1 year later...

Over on this thread 

I discovered that the API referenced by

has broken.

I initially reported the error on the other thread, but I think it makes more sense to continue the on this thread, with all the history in the same place. 

@Ehsan I trust this helps you identify the API that's broken.

Link to comment
Share on other sites

Over on this thread 

I discovered that the API referenced by

has broken.

I initially reported the error on the other thread, but I think it makes more sense to continue the on this thread, with all the history in the same place. 

The impact to the business is that file GET requests still return 200, but the content is garbage, and this garbage has been posted to the next stage of the automation (an archival process) since I don't know when. I'm currently working out how much archival is going to have to be re-done, and telling the support desk how many archival requests are going to have to be re-done. Fortunately we still have the original documents (which Hornbill has at least preserved). Nonetheless, I am on the wrong end of a difficult conversation.

@Ehsan I trust this helps you identify the API that's broken.

Link to comment
Share on other sites

@BillP,

Reading through this thread, I believe you are calling the smGetAttachments API, which returns a JSON object in form of a string as an output parameter called attachments

The API payload (example):

<methodCall service="apps/com.hornbill.servicemanager/Requests" method="smGetAttachments">
   <params>
      <requestId>IN00039274</requestId>
      <startRow>0</startRow>
      <rowCount>10</rowCount>
   </params>
</methodCall>

And the API response:

{
	"@status": true,
	"params": {
		"attachments": "[{\"h_pk_id\":\"428\",\"h_request_id\":\"IN00039274\",\"h_filename\":\"Test 1.txt\",\"h_size\":\"4\",\"h_timestamp\":\"2022-01-31T09:52:49.000Z\",\"h_visibility\":\"trustedGuest\"},{\"h_pk_id\":\"429\",\"h_request_id\":\"IN00039274\",\"h_filename\":\"Test 2.txt\",\"h_size\":\"4\",\"h_timestamp\":\"2022-01-31T09:52:50.000Z\",\"h_visibility\":\"trustedGuest\"}]"
	}
}

I have beautified the attachments output parameter for better presentation:

[
	{
		"h_pk_id":"428",
		"h_request_id":"IN00039274",
		"h_filename":"Test 1.txt",
		"h_size":"4",
		"h_timestamp":"2022-01-31T09:52:49.000Z",
		"h_visibility":"trustedGuest"
	},
	{
		"h_pk_id":"429",
		"h_request_id":"IN00039274",
		"h_filename":"Test 2.txt",
		"h_size":"4",
		"h_timestamp":"2022-01-31T09:52:50.000Z",
		"h_visibility":"trustedGuest"
	}
]

 

To access an attachment, first and foremost, you will need the h_filename property for an attachment from the above output.

The API Requests::entityAttachGetInfo needs to be made to retrieve the file's access token.

The API payload (Notice that the filePath input parameter in the API payload is set with the h_filename value for an attachment from the previous API call):

<methodCall service="apps/com.hornbill.servicemanager/Requests" method="entityAttachGetInfo">
   <params>
      <keyValue>IN00039274</keyValue>
      <filePath>Test 1.txt</filePath>
   </params>
</methodCall>

 The API response:

{
	"@status": true,
	"params": {
		"accessToken": "u5TVN1rIoitFgP2uwfyDW_hR95ydhW6YTDidFykOTYK1J3UWxEa-wDfDN55TGBVVEnZXnyeKPlHDZjkSOommMs7fJe-xKt3p34TX0NkaFmZR-MouATGZ9ZcUTd_Tz2II_vLB0LaxYpge9mMfkG52UF1WgG2Y95SCCSRj-OyG",
		"fileSource": "\/secure-content\/inline\/u5TVN1rIoitFgP2uwfyDW_hR95ydhW6YTDidFykOTYK1J3UWxEa-wDfDN55TGBVVEnZXnyeKPlHDZjkSOommMs7fJe-xKt3p34TX0NkaFmZR-MouATGZ9ZcUTd_Tz2II_vLB0LaxYpge9mMfkG52UF1WgG2Y95SCCSRj-OyG",
		"fileSize": 4,
		"fileName": "Test 1.txt"
	}
}

Notice that the accessToken output parameter is returned by the API.

Now, we move on to the last step, and that is to construct the URL to access the file.

The URL is made up of your instance's download content URL + the accessToken value. Every instance's download content URL is different. You can find out yours as below:

  1. Login to Hornbill on Chrome
  2. Press F12 to open the Developer Tools
  3. In the Console, type in the below and press enter
espData.DOWNLOAD_CONTENT

image.png

In my example, the URL is https://hhq-p01-api.hornbill.com/ehsans/dav/secure-content/download/.

So to access the attachment, the URL is my instance's download URL + the accessToken value from the API response I shared above. 

https://hhq-p01-api.hornbill.com/ehsans/dav/secure-content/download/u5TVN1rIoitFgP2uwfyDW_hR95ydhW6YTDidFykOTYK1J3UWxEa-wDfDN55TGBVVEnZXnyeKPlHDZjkSOommMs7fJe-xKt3p34TX0NkaFmZR-MouATGZ9ZcUTd_Tz2II_vLB0LaxYpge9mMfkG52UF1WgG2Y95SCCSRj-OyG

To formulate this as a variable:

"https://hhq-p01-api.hornbill.com/ehsans/dav/secure-content/download/" + the accessToken value from the API response I shared above.

I hope this helps.

Ehsan

Link to comment
Share on other sites

Hi @Ehsan

I can confirm that the above process works fine. Thank you again.

For the benefit of others attempting a similar task, the code looks like this:

 

#
# previously - fetch a list of requests from h_itsm_requests 

#
# iterate through the requests
$DIRequests | 
  where {$_."ServiceName" -eq "Documotive"} |
  where {$_."Catalog" -eq "Request Documents to be Indexed"} |
  foreach {
    $entry = $_ |
       Add-Member NoteProperty FileLocation "" -PassThru

    $requestID = $entry."RequestID"
    Write-Host "Request: $requestID for $($entry.CustomerName) is $($entry.Status)"
    if ($requestID -eq "SR00058943") {
        $entry | Out-Null
    }

  #  if (-not [string]::IsNullOrWhiteSpace($entry.AttachmentLocation)) {
    if ($true) {
        Add-HB-Param -ParamName "requestId" -ParamValue $requestID
    
        Add-HB-Param -ParamName "startRow" -ParamValue 0
    
        Add-HB-Param -ParamName "rowCount" -ParamValue 1
    
        # Invoke XMLMC call, output returned as PSObject type
        $requestOutput = Invoke-HB-XMLMC "apps/com.hornbill.servicemanager/Requests" "smGetAttachments"
        if ($requestOutput.Status -eq "fail") {
            Write-PSFMessage -Level Debug -Tag "ERROR" -Message "failed to retrieve attachments: $($requestOutput.Error)"
        }

        if ($requestOutput.Params -ne $null) {
            $attachmentDetails = $requestOutput.Params.attachments | ConvertFrom-Json
            $outputFolder = Join-Path $AttachmentFolder $entry.RequestID
            if (-not (Test-Path -LiteralPath $outputFolder)) {
                mkdir $outputFolder | Out-Null
            }
            if (-not [string]::IsNullOrWhiteSpace($attachmentDetails.h_filename)) {
                $outputLocation = Join-Path $outputFolder $($attachmentDetails.h_filename)
                #
                # new mechanism, as described at https://community.hornbill.com/topic/19200-how-do-i-retrieve-an-attachment-using-the-api/ (response from Ehsan)
                Add-HB-Param -ParamName "keyValue" -ParamValue $requestID
                Add-HB-Param -ParamName "filePath" -ParamValue $attachmentDetails.h_filename
                $AttachInfoOutput = Invoke-HB-XMLMC "apps/com.hornbill.servicemanager/Requests" "entityAttachGetInfo"
                if ($AttachInfoOutput.Status -eq "fail") {
                    Write-PSFMessage -Level Debug -Tag "ERROR" -Message "failed to retrieve attachment info: $($AttachInfoOutput.Error)"
                }
                else {
                    $attachmentInfo = $AttachInfoOutput.Params
                    $fileAccessToken = $attachmentInfo.accessToken
                    $root = 'https://mdh-p01-api.hornbill.com/xxxxxx/dav/secure-content/download/' # root obtained as per Ehsan's post above
                    $url = $root + $fileAccessToken

                    if (-not (Test-Path -LiteralPath $outputLocation)) {
                        Try {
                            $wc = New-Object System.Net.WebClient
                            $key = $HornbillAPIKey
                           # $key = "abcdef1234567890abcdef1234567890"
                            $wc.Headers.Add("Authorization","ESP-APIKEY " + $key)
            
                            $wc.DownloadFile($url,$outputLocation)
                            Write-Host "Downloaded from $url to $outputLocation"
                            Write-PSFMessage -Level Debug -Tag "INFO" -Message "Downloaded from $url to $outputLocation"
                            if ($outputLocation -like "*.zip") {
                                [System.IO.Compression.ZipFile]::ExtractToDirectory($outputLocation,$outputFolder)
                                Get-ChildItem $outputFolder -Include '*.doc*' -Recurse | Select-Object -First 1 | foreach { $outputLocation = $_.FullName}
                            }
                            $entry.FileLocation = $outputLocation

                        }
                        Catch {
                            Write-Error "Aborting: $($_.Exception.Message)"
                            Write-PSFMessage -Level Debug -Tag "ERROR" -Message "Aborting: $($_.Exception.Message)"
                            exit 0
                            #echo("hey ho pip and dandy")
                            #return $apiResponse
                        }
                    }
                    else {
                        Write-Host "Already downloaded: $outputLocation"
                        Write-PSFMessage -Level Debug -Tag "INFO" -Message "Already downloaded: $outputLocation"
                    }
                    # other stuff
                }
            }
            else {
                # no attachment
                $message = "Request: $requestID for $($entry.CustomerName) - attachment name is blank"
                Write-PSFMessage -Level Debug -Tag "ERROR" -Message $message
                Write-Host @errorColours $message
            }
        }
        else {
            # no attachment
            $message = "Request: $requestID for $($entry.CustomerName) - no attachment found on request"
            Write-PSFMessage -Level Debug -Tag "ERROR" -Message $message
            Write-Host @errorColours $message
        }
    }

 

Link to comment
Share on other sites

Create an account or sign in to comment

You need to be a member in order to leave a comment

Create an account

Sign up for a new account in our community. It's easy!

Register a new account

Sign in

Already have an account? Sign in here.

Sign In Now
×
×
  • Create New...