BillP Posted September 9, 2020 Share Posted September 9, 2020 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 More sharing options...
SamS Posted September 9, 2020 Share Posted September 9, 2020 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 More sharing options...
BillP Posted September 9, 2020 Author Share Posted September 9, 2020 Thanks, @SamS That's got me further, and I now get a 401 error. I'm using the same API key as I use a few lines earlier when pulling down the report, so I'm guessing that permissions or roles might need to be added for the account? Link to comment Share on other sites More sharing options...
Victor Posted September 9, 2020 Share Posted September 9, 2020 AFAIK API keys don't work with webdav requests, only xmlmc but @SamS knows better... Link to comment Share on other sites More sharing options...
BillP Posted September 9, 2020 Author Share Posted September 9, 2020 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 More sharing options...
Victor Posted September 9, 2020 Share Posted September 9, 2020 They API is being checked but the service (webdav) does not allow calling webdav APIs with an API key (iirc) Link to comment Share on other sites More sharing options...
BillP Posted September 9, 2020 Author Share Posted September 9, 2020 @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 More sharing options...
Steve G Posted September 10, 2020 Share Posted September 10, 2020 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 2 Link to comment Share on other sites More sharing options...
BillP Posted September 11, 2020 Author Share Posted September 11, 2020 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 1 Link to comment Share on other sites More sharing options...
BillP Posted January 31, 2022 Author Share Posted January 31, 2022 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 More sharing options...
BillP Posted January 31, 2022 Author Share Posted January 31, 2022 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 More sharing options...
Guest Ehsan Posted January 31, 2022 Share Posted January 31, 2022 @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: Login to Hornbill on Chrome Press F12 to open the Developer Tools In the Console, type in the below and press enter espData.DOWNLOAD_CONTENT 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 More sharing options...
BillP Posted January 31, 2022 Author Share Posted January 31, 2022 @Ehsan thank you for the prompt and full response. I'll work through the steps and let you know how I get on. Link to comment Share on other sites More sharing options...
BillP Posted February 1, 2022 Author Share Posted February 1, 2022 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 More sharing options...
Guest Ehsan Posted February 1, 2022 Share Posted February 1, 2022 @BillP, That's great to hear! Nice code snippet, hopefully others will find it useful too. Link to comment Share on other sites More sharing options...
Recommended Posts
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 accountSign in
Already have an account? Sign in here.
Sign In Now