Adding, Updating, or Deleting Attachments in FeatureLayer applyEdits in REST API?

2132
8
05-01-2017 01:28 PM
CalebMackey1
Regular Contributor II

I was looking over the documentation for the supported operations of the FeatureService and noticed that starting at 10.4, the applyEdits method started supporting an "attachments" parameter.  I am following the documentation and cannot get this to work by doing it manually against the REST endpoint, or programmatically with Python.

I cannot see where I'm going wrong, I have tried to do both add and delete using the apply edits and neither one has worked. I am using this test feature layer (open access):

The documentation has this as an example for the attachments parameter:

{
  "adds": [{
      "globalId": "{55E85F98-FBDD-4129-9F0B-848DD40BD911}",
      "parentGlobalId": "{02041AEF-4174-4d81-8A98-D7AC5B9F4C2F}",
      "contentType": "image/pjpeg",
      "name": "Pothole.jpg",
      "uploadId": "{DD1D0A30-CD6E-4ad7-A516-C2468FD95E5E}"
    },
    {
      "globalId": "{3373EE9A-4619-41B7-918B-DB54575465BB}",
      "parentGlobalId": "{6FA4AA68-76D8-4856-971D-B91468FCF7B7}",
      "contentType": "image/pjpeg",
      "name": "Debree.jpg",
      "data": "<base 64 encoded data>"
    }
  ],
  "updates": [{
    "globalId": "{8FDD9AEF-E05E-440A-9426-1D7F301E1EBA}",
    "contentType": "image/pjpeg",
    "name": "IllegalParking.jpg",
    "uploadId": "{57860BE4-3B85-44DD-A0E7-BE252AC79061}"
  }],
  "deletes": [
    "{95059311-741C-4596-88EF-C437C50F7C00}",
    " {18F43B1C-2754-4D05-BCB0-C4643C331C29}"
  ]
}‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍

I have tried doing both update and add to no avail.  The features have attachments related by GlobalId, and the "useGlobalIds" parameter is set to "true" when I try it.  When I do the "adds", I am not including a GlobalId because I'm assuming those will be generated automatically, however, I am including the parentGlobalId and have encoded the image in base 64.  

When the adds failed, I thought maybe it was because I was not including a globalId for the new feature, so I tried an update which would have an existing global id.  This is what I passed into the "attachments" parameter for an update:

{
  "parentGlobalId": "266f0b2c-88c2-4ac7-8e40-10ff461279dc",
  "globalId": "a91a085b-e30c-4146-af96-eb792c6269b5"
  "data": "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAABNoA...", //the rest is ommitted for clarity
  "contentType": "image/png",
  "name": "routing.png",
}‍‍‍‍‍‍‍

When trying this with Python, I use Fiddler to watch my request to make sure they are correctly formatted and the screenshot below shows the other parameters I'm using but the attachments parameter does not show up all the way because Fiddler truncates the value which is massive in this case due to the base64 encoded image:

the updates call does work when I do not try to add the attachments parameter.  Has anyone had success using the attachments parameter in apply edits?  I know I can add/update/delete attachments one at a time, but it would be nice how to figure out how to batch edit attachments in the applyEdits method.

0 Kudos
8 Replies
RyanGatchell1
New Contributor II

462 Views of a two year old post and no replies? Come on Esri staff, break away from the UC for a minute and at least take a stab at it!

ArapahoeAdmin
New Contributor II

I know I'm likely barking up a useless tree here since no help was forthcoming from ESRI two years ago, as noted by Ryan, but did you come to any solution or insight on this matter Caleb?

I'm trying to do something similar, use the ArcGIS REST API to add an attachment to an existing point.  The method to use is "addAttachment" (Add Attachment—ArcGIS REST API: Services Directory | ArcGIS for Developers)

Sadly, the online documentation is next to useless... according to it, "The input parameter attachment to this operation is a file"  Wow... that's great, no duh... I know, I'm trying to attach a file.  How exactly do I send said file over a REST API POST request?  There simply aren't any good examples out there of the exact JSON to use to encapsulate an attachment to get the REST "addAttachment" interface to recognize it.

I need to figure out what goes at the end of this: https://some.site.com/arcgis/rest/services/yourmap/featureservice/layernumber/objectid/addAttachment?attachment= 

What goes after attachment=?  I've tried things like 

'attachment={"file": "' + sigData1 + '", "contentType": "image/png", "name": "test.png"}'

Where sigData1 is a png image in binary.  I've tried it with and without a file name. I've tried spoofing an uploadDirectory (which it sometimes complains about) Honestly I've tried too many iterations to count here, and I get a number of different errors.

The JSAPI doesn't really expose what's going on behind the scenes... it expects a form with an "attachment" object pointing preferable to a file on disk.  I'm working on a mobile platform with digital signatures created during run time so that's not an option.

Whatever... it's late here and I'm rambling.  I'll press post and send this digital cry for help onto the interwebz in the hopes that someone somewhere may have a similar problem and we can figure it out together.

Dom

0 Kudos
RyanGatchell1
New Contributor II

I ended up using the REST API's appyEdits method. I had to loop through the form, get the files from the input (user could upload more than one image), upload the images to the service and get back the upload id's, then do the post using applyEdits with the attachments in the callback from the upload posts. The docs say that base64 strings will work too, but I hacked away at that for hours before giving up and using the upload method

Here's my function for getting the files from a form:

 function getTheFiles() {
var filesArr = [];
var inputForms = attachmentsNode.querySelectorAll("form");
for(var i = 0; i < inputForms.length; i++) {
var input = inputForms[i].querySelectorAll("input[type=file]");
if(input[0].files.length > 0) {
filesArr.push(input[0].files[0])
}
}
return filesArr;
}‍‍‍‍‍‍‍‍‍‍‍

And here's the upload:

function uploadFilesFunction(filesArr, config, callback){
uploadItems = [];
if(filesArr.length > 0){
var counter = 0;
filesArr.forEach(function(file, filesIndex) {
var uploadFormData = new FormData(); // add key value pairs for the request to this object
var xhr = new XMLHttpRequest();
var baseEditUrl = config.serviceUrls.edit.substring(0, config.serviceUrls.edit.length -2);
var uploadUrl = baseEditUrl + "/uploads/upload";
xhr.open("POST", uploadUrl);
xhr.onerror = function(err){
console.error("this is the error handler from xhr for file upload", err);
resetMainControls();
hideLoadingDiv();
showMessages(errorReportMessage, false);
};
var fileType = filesArr[filesIndex].type;
xhr.onreadystatechange = function(evt) { // Call a function when the state changes.
if (this.readyState === XMLHttpRequest.DONE && this.status === 200) {
var response = JSON.parse(xhr.response);
if (response.hasOwnProperty("success")) {
if (response.success){
uploadItems.push({
"globalId": uuidv4(),
"parentGlobalId": parentGlobalId,
"contentType": fileType,
"name": response.item.itemName,
"uploadId": response.item.itemID
});
counter += 1;
if(callback){
if(counter == filesArr.length){
callback(uploadItems, config);
}
}
}
} else {
console.log("item didn't upload");
console.log(response);
}

}
}
uploadFormData.append("f", "json");
uploadFormData.append("file", filesArr[filesIndex]);
// counter += 1;
xhr.send(uploadFormData);
});
} else {
callback(uploadItems, config)
}
}

// second function to call when first function resolves:
function doThePostPart(uploadItems, config){
console.log("doThePostPart started")

var feature = {
"geometry": graphic.geometry,
"attributes": graphic.attributes
};

var applyEditsFormData = new FormData();

applyEditsFormData.append("f","json");
applyEditsFormData.append("useGlobalIds", true);
applyEditsFormData.append("adds",JSON.stringify([feature]));
applyEditsFormData.append("attachments",JSON.stringify({"adds":uploadItems}));

var xhrApplyEdits = new XMLHttpRequest();
xhrApplyEdits.open("POST", config.serviceUrls.edit + "/applyEdits");
xhrApplyEdits.onreadystatechange = function(response) { // Call a function when the state changes.
if (this.readyState === XMLHttpRequest.DONE && this.status === 200) {
resetMainControls();
hideLoadingDiv();
showMessages(reportMessage, false);
}
};
xhrApplyEdits.onerror = function(err){
console.error("this is the error handler from xhrApplyEdits", err);
resetMainControls();
hideLoadingDiv();
showMessages(errorReportMessage, false);
};
xhrApplyEdits.send(applyEditsFormData);
}
var theFiles = getTheFiles();
uploadFilesFunction(theFiles, this.config, doThePostPart) // call to start the chain

Hope this helps if anyone out there is in the same predicament, and thanks Dom for reminding me to share when we find working solutions .

DominickCisson
Occasional Contributor

Thanks Ryan.  There's a lot there and I'll have to delve into it.  I'm saddened by your inability to get the base64 strings to work as well with addAttachment.    The hurdle I'm trying to tackle is capturing a user's digital signature and then uploading the png of it up to a feature in a feature service via REST.  The JS library I use has no problem creating the base64 of the png, but being mobile-only, I'm not sure I can save the png somewhere locally to something like a phone so that can then be scraped and uploaded using your method.

It's a technical challenge for sure... thanks for your help!

0 Kudos
ArapahoeAdmin
New Contributor II

I figured out the issue with bending base64 data through REST for attachments… and it's something that should probably be noted in the documentation for REST attachments somewhere.

 

When adding image attachments you are instructed to attach them as base64 encoded image files.  Problem is, in its native form base64 encoded data contains slashes and pluses.  When that is sent as an Ajax request via an http POST, those characters get mangled, and what comes out the other end is no longer the same string and therefore no longer a valid image.  To prevent this, it is necessary to use the function encodeURIComponent() to encode the base64 string so it can be cleanly transmitted via HTTP.  Do that, and the base64 gets sent correctly, the attachment gets stored correctly, and your image looks as expected.

Hopefully someone finds this someday and it saves them a couple of days of frustration.

RobertWeber
New Contributor III

I am in the midst of doing the same thing.  Very simple thing to apply feature edits via python  but when it comes to attachments the parameter itself appears to be invalid even though it is in the documentation.  If anyone gets this to work that would be great.  Thanks for the example above 

0 Kudos
BethLott
New Contributor II

I was able to use the Add Attachment REST API call using RestSharp. I write C#, not Python, but the key for me was the AddFile method instead of AddParameter. The file itself was a byte array, and it also accepts the MIME type as an optional parameter. Otherwise, it creates the attachment with a MIME of application/octetstream. Here's my function; theMimeType function is not included - it just looks at the end of the file name to see if it's BMP, JPEG or PNG. Not elegant, but there is control elsewhere over what kind of files are submitted.

public string SaveAttachment(FeatureServiceLayer resource, int objectId, string token, byte[] theImageBytes, string imageFilename)
{
string theContentType = theMimeType(imageFilename);
var client = new RestClient(baseUrl);
var request = new RestRequest(resource.ToString("D") + "/" + objectId + "/addAttachment", Method.POST);
request.AddHeader("Authorization", $"Bearer {token}");
request.AddParameter("f", "json");
if (theContentType != "")
{
request.AddFile("attachment", theImageBytes, imageFilename, theContentType);
}
else
{
request.AddFile("attachment", theImageBytes, imageFilename); //unknown content type
}
var response = client.Execute(request);
if (response.ErrorException != null)
{
var gisException = new Exception("Error retrieving response. Check inner details for more info.", response.ErrorException);
throw gisException;
}
return response.Content;
}
RandyBonds_Jr_
New Contributor III

Thanks a ton! I fought with this for WAY too long. Life. Saver.

0 Kudos