Uploading Attachments to SharePoint Lists Using SPServices

Easy!For years people have been asking me how they could upload files using SPServices. I’ve dodged the questions every time because I simply didn’t know how.

On a current client project,  I’m building a slick Single Page Application (SPA) to manage tasks in SharePoint 2010. It’s basically a veneer over the clunky out-of-the-box task list experience. Rather than hopping from page to page, the team using the application can accomplish everything they need to do on a single page.

Every project has specific needs, and for this one I’m using KnockoutJS. But the code snippets I give below are generic enough that you should be able to use them in any context with a little thinking.

It’s been going well, but I had been putting off implementing adding attachments because…well, as I said, I didn’t know how.

One of the benefits of the shift from all server side development to a much more significant focus on client side techniques is that some of the bright minds in the SharePoint development community have turned their eyes toward solving these things, too. Usually it’s on SharePoint 2013 or SharePoint Online at Office 365. However, at least now when I search for “SharePoint JavaScript xxx”, there are likely to be some great hits I can learn from.

In this case, there were two excellent posts, one from James Glading and one from Scot Hillier. (See the Resources section below for links.)

The first step is to enable HTML5 capabilities in IE10. This requires two small changes to the master page. This seems simple, but it can have effects that you don’t expect. In other words, don’t consider this just a “no brainer”. You need to plan for the change across your Site Collection and any impacts it may have.

The first change is to switch from using “XHTML 1.0” to “HTML” as the DOCTYPE. This is what “turns on” HTML5.

<!DOCTYPE html>

Then, in this project we set the content meta tag to IE=10 because we want to aim for IE10.

<meta http-equiv="X-UA-Compatible" content="IE=10"/>

If your browser base is different, you can, of course, set this differently. In this project, we will require all users of the application to be running IE10 or greater. Firefox and Chrome have supported the bits of HTML5 we need for so long, that there’s little concern about people who choose to use those browsers.

Once we have those two small changes in the master page (we are using a copy of v4.master with no other customizations at the moment), we “have HTML5”. It’s most obvious because some of the branding flourishes I’ve put in like rounded corners show up in IE10 now, rather than just square boxes.

Once we have HTML5 enabled, we can start to use the File API, a.k.a. the FileReader. This is such a simple little thing, that it’s hard to believe it gives us the capability it does.

<input type="file" id="attachment-file-name"/>

That’s it. That simple HTML gives us a file picker on the page. Depending on your browser, it will look something like this:

File Picker

When you make a file selection with this very familiar widget, you can query the element using jQuery.

var file = $("#attachment-file-name").files[0];

Note that we’re after a single file, so we grab the first object in the file list array. We get an object that looks something like this screen shot from Firebug:

File object from the File Picker

Once we have the file object, we can look at what the Lists SOAP Web Service needs to get in order to upload the file. Again, it’s pretty simple. Here is the list of inputs and output from the MSDN documentation page for the Lists.AddAttachment Method.

Parameters

listName
A string that contains either the title or the GUID for the list.
listItemID
A string that contains the ID of the item to which attachments are added. This value does not correspond to the index of the item within the collection of list items.
fileName
A string that contains the name of the file to add as an attachment.
attachment
A byte array that contains the file to attach by using base-64 encoding.

Return Value

A string that contains the URL for the attachment, which can subsequently be used to reference the attachment.

The first three inputs are straightforward. We pass in the name of the SharePoint list, the ID of the list item, and the name of the file we want to attach. It’s that last input parameter called “attachment” that’s a bit tricky.

When we upload a file via an HTTP POST operation – which is what all of the SOAP Web Services use – we have to pass text. But the files we want to upload are as likely as not to be binary files. That’s where the Base64 format comes in. Believe me, you don’t need to know exactly what the format actually is, but you should probably understand that it enables us to send binary files as text bytes. Those bytes are then decoded on the server end so that the file can be stored in all of its original, pristine glory.

Here’s the code I ended up with, skinnied down as much as possible to make it a little clearer to follow. I draw heavily from Scot and James’ posts for this, but nuanced for SPServices. I’ve also stripped out all of the error handling, etc.

  • First, the getFileBuffer function reads the contents of the file into the FileReader buffer. readAsArrayBuffer is an asynchronous method, so we use a jQuery Deferred (promise) to inform the calling function that the processing is done.
  • The contents of the buffer are then converted from an ArrayBuffer – which is what the FileReader gives us – into a Base64EncodedByteArray. This works by passing the buffer through a Uint8Array along the way.
  • Finally, we use the toBase64String method to convert the SP.Base64EncodedByteArray to a Base64String.

Yeah, that’s a lot to swallow, but again, you don’t really need to understand how it works. You just need to know that it does work.

Finally, we call Lists.AddAttachment using SPServices to do the actual upload.

/* From Scot Hillier's post:
http://www.shillier.com/archive/2013/03/26/uploading-files-in-sharepoint-2013-using-csom-and-rest.aspx */
var getFileBuffer = function(file) {

  var deferred = $.Deferred();
  var reader = new FileReader();

  reader.onload = function(e) {
    deferred.resolve(e.target.result);
  }

  reader.onerror = function(e) {
    deferred.reject(e.target.error);
  }

  reader.readAsArrayBuffer(file);

  return deferred.promise();
};

getFileBuffer(file).then(function(buffer) {
  var bytes = new Uint8Array(buffer);
  var content = new SP.Base64EncodedByteArray(); //base64 encoding
  for (var b = 0; b < bytes.length; b++) {
    content.append(bytes[b]);
  }

  $().SPServices({
    operation: "AddAttachment",
    listName: "Tasks",
    listItemID: taskID,
    fileName: file.name,
    attachment: content.toBase64String()
  });

});

Very cool! And as I tweeted yesterday, far easier than I ever would have expected. Yes, it took me a good chunk of a day to figure out, but it definitely works, and pretty fast, too.

If you use this example as a base, you could fairly easily build out some other file uploading functions. Combined with the other attachment-oriented methods in the Lists Web Services, you can also build the other bits of the attachment experience:

  • GetAttachmentCollection – Returns a list of the lit item’s attachments, providing the full path to each that you can use in a list of links.
  • DeleteAttachment – Once you’ve uploaded an attachment and realized it was the wrong one, this method will allow you to delete it.

Moral of the story: Fear not what you do not know. Figure it out.

Resources

What’s the Story for HTML5 with SharePoint 2010? by Joe Li

Uploading Files Using the REST API and Client Side Techniques by James Glading

Uploading Files in SharePoint 2013 using CSOM and REST by Scot Hillier

Lists.AddAttachment Method

This article was also published on IT Unity on Jun 02, 2014. Visit the post there to read additional comments.

Update 2014-05-28 11:55 GMT-5

Hugh Wood (@HughAJWood) took one look at my code above and gave me some optimizations. Hugh could optimize just about anything; he is *very* good at it. I *think* I can even see the difference with larger files.

getFileBuffer(file).then(function(buffer) {
  var binary = "";
  var bytes = new Uint8Array(buffer);
  var i = bytes.byteLength;
  while (i--) {
    binary = String.fromCharCode(bytes[i]) + binary;
  }
  $().SPServices({
    operation: "AddAttachment",
    listName: "Tasks",
    listItemID: taskID,
    fileName: file.name,
    attachment: btoa(binary)
  });
});

36 Comments

  1. Hi Marc, Thanks for posting that optimisation. To explain it uses btoa function for the conversion, removing the custom Microsoft code for the byte conversion. Also a further optimisation decrements instead of increments (Decrement is faster) uses a while loop instead of a for, (These are also much faster) and removed the object lookup for the Array.length from the loop (In the previous code it is checked in every iteration).

    String concatenation is heavily optimised in new browsers, so don’t worry about that amount of concatenations. The performance gain using an Array.join(“”) method will only be noticeable in IE9 and below.

    Reply
  2. Hi Marc again, I was reading this post and comparing it to my code again and notices something.

    You use the fileReader.readAsArrayBuffer which reaturns an array buffer which you have to convert.

    I use readAsDataURL https://developer.mozilla.org/en-US/docs/Web/API/FileReader.readAsDataURL which returns a data: base64 URL which I then use data.indexOf(“;base64,”) + 8; to get the raw base64 string.

    self.uploadFile = function (files) {
    				var filereader = {},
    				file = {},
    				file = files[0];
    				filereader = new FileReader();
    				filereader.filename = file.name;
    				
    				filereader.onload = function () {
    					var data = this.result,
    					n = data.indexOf(";base64,") + 8;
    					//removing the first part of the dataurl give us the base64 bytes we need to feed to sharepoint
    					data = data.substring(n);
    					uploadFile(this.filename, data);
    				}
    				
    				filereader.onabort = function () {
    					alert("The upload was aborted.");
    				};
    				
    				filereader.onerror = function () {
    					alert("An error occured while reading the file.");
    				};
    				//fire the onload function giving it the dataurl
    				filereader.readAsDataURL(file);
    		}
    
    
        
        function uploadFile(FileName,FileData) {
        console.log("uploading file");
        var url= "http://libraryurl/"+FileName;
        
             var soapEnv =
        " \
            \
                \
                    https://libraryaddress	\
                        \
                             "+ url + "\
                        \
                        \
                            \
                        \
                    "+ FileData +"\
                \
            \
        ";
            jQuery.ajax({
                url: "https://sharepointsite/_vti_bin/copy.asmx",
                beforeSend: function (xhr) { xhr.setRequestHeader("SOAPAction", "http://schemas.microsoft.com/sharepoint/soap/CopyIntoItems"); },
                type: "POST",
                dataType: "xml",
                data: soapEnv,
                complete: processResult,
                contentType: "text/xml; charset=\"utf-8\""
            });
        }
    
    Reply
    • Bryan:

      Thanks for sharing. It looks like some of your code didn’t make it through the filters. If you want to send me the correct code via email, I can post it for you.

      Based on the optimizations Hugh gave me and your code, it’s clear that there is more than one way to get this to work. It would be great to come up with the ideal method, as I’ve seen several different examples out there, some of which work and others which don’t seem to.

      M.

      Reply
  3. FYI…
    I happen to have re-factored a widget over the weekend that is very similar to your code here… The issue I was hitting with the use of readAsArrayBuffer() is that it was causing the browser to crash for large files (ex. anything over 10MB)… After a little research, I found that (like Bryan stated above) using readAsDataURL is more efficient… In this use case, there is no need to load the file it into a binary array, just to turn around and convert it back to a String (Base64)… After the refactor, I seem to be able to upload much large files (tried with a 50MB file with no issues).

    note: new widget will be added SPWidgets soon, replacing my existing upload widgets that works only for Doc. libraries). :-)

    Reply
      • Yeah, I created a new Upload widget a few months ago and was using the Binary read approach.. but got reports of blowing up with large files (browser tab would crash – not nice)…

        This new Upload widget I have will be replacing the Frankenstein one I currently have (using iframe/screen scrapping :) ). HTML5 rock!

        Reply
  4. Can you clarify if this is only for SP2013 or if it applies for SP2010. I believe it is only 2013, but want to be sure. Otherwise I need to look at another approach.

    Thanks for all your helpful tutorials and functions.

    Reply
  5. Hi!

    Great walk through – It could just about save my bacon! I am getting a 500 error from lists.asmx and I think its because I don’t really understand what the listItemID is or where you are supposed to find it? Any help would be great!

    Thank you!
    James

    Reply
    • James:

      The listItemID is the ID of the item in the list. As you probably know, every item in a list gets an ID, which is simply a sequential number.

      If you view an item in the list, you’ll see the ID on the query string. Here’s an example: /Lists/Surveys/DispForm.aspx?ID=1

      M.

      Reply
      • Hi Marc

        Thanks for your reply. So if I am adding adding a new document to a document library, it would be a new item so wouldn’t have an ID yet but I can’t post it without one. Have I misunderstood what this function does?

        Thanks so much for your help!

        James

        Reply
        • Yes, you’re right. An item which hasn’t been saved yet, wouldn’t have an ID. If you want to add attachments using this operation, you’ll need to save the item, get the ID for it, and then add the attachments.

          M.

          Reply
  6. Hi:
    It’s great article and codes and give be a beautiful start! I now work on add attach multiple files to a list item on SharePoint 2010 using ‘AddAttachment’ with ‘filereader.readAsDataURL(file)’ inside for loop through each attachment file. But only the last file got really attached to list item. Is any parameter in ‘AddAttachment’ should be turn on, or there is another way I should to use, to add multiple files to item from attachment control?

    Reply
    • Fábio:

      It’s hard to say without seeing your code, but yes it should work in Chrome. I’d suggest you post it to a public forum like StackOverflow. I’m happy to take a look if you ping back the link.

      M.

      Reply
  7. Does anyone know of a way to use this across sites/site collections? The AddAttachment operation asks for a list name but what if the list you want to upload to is in another site or even site collection? Is there a way to specify a webURL or something?

    Reply

Have a thought or opinion?