Thursday 14 May 2015

Create REST for Upload file and Transferring Parameter

We used SharePoint as a repository for some of our documents and was using CSOM as a way to upload it from another application. The problem came when we upload file and set some of its property after upload it. It turned out that update property after upload take a longer time than when you upload 11MB file. Doing a 11MB file upload using CSOM took 9.7s, while updating its property took 23.38s. We came out with the idea of using built-in SharePoint REST service, but we got bumped into 403 forbidden access error. And it wasn't the best solution because, upload and updating property will required 2 process (upload 1st than update property). If we could create a custom REST, you can just sent those properties in URL and send the stream of file into http request.

There were many tutorial about uploading files to REST service but there wasn't much on how to create service that upload file inside SharePoint 2013. Base on many reference on internet I've created a REST service which run under SharePoint 2013 project (Full trust code). So how do you add a svc file inside SharePoint 2013 project? You can search using this keyword "creating a custom svc in SharePoint 2013" on google. Or just create that svc on another project and put it under ISAPI directory on SharePoint project.



Then we start with setting up interface and define class definition that will become the output from our REST service.


using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Runtime.Serialization;
using System.ServiceModel;
using System.ServiceModel.Web;
using System.Text;
using System.Threading.Tasks;

namespace UploadFileUsingREST
{
    [ServiceContract]
    interface IUploadRest
    {
        [OperationContract]
        [WebInvoke(Method = "POST",
            UriTemplate = "UploadFile/{fileName}/{title}")]
        ConfirmationFileUpload Upload(string fileName, string title, Stream fileContent);
    }
    [DataContract]
    public class ConfirmationFileUpload
    {
        [DataMember]
        public long FileReceivedLength;
        [DataMember]
        public bool IsSuccess;
        [DataMember]
        public string ErrorMessage;
    }
}

After that we begin implementing code that will receive stream and update property of that file.


using Microsoft.SharePoint;
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.ServiceModel.Activation;
using System.Text;
using System.Threading.Tasks;

namespace UploadFileUsingREST
{
    [AspNetCompatibilityRequirements(RequirementsMode = AspNetCompatibilityRequirementsMode.Required)]
    public class UploadRest : IUploadRest
    {
        public const string SiteURL = http://siteurl;
        public ConfirmationFileUpload Upload(string fileName, string title, System.IO.Stream fileContent)
        {
            ConfirmationFileUpload result = new ConfirmationFileUpload();
            try
            {
                using (SPSite site = new SPSite(SiteURL))
                {
                    using (SPWeb web = site.OpenWeb())
                    {
                        using (MemoryStream writer = new MemoryStream())
                        {
                            int readCount;
                            byte[] buffer = new byte[8192];
                            while ((readCount = fileContent.Read(buffer, 0, buffer.Length)) != 0)
                                writer.Write(buffer, 0, readCount);
                            web.AllowUnsafeUpdates = true;
                            SPFile file = web.Files.Add(string.Format("{0}/Shared Documents/{1}", SiteURL, fileName), writer, true);
                            file.Update();
                            SPListItem item = file.ListItemAllFields;
                            item[SPBuiltInFieldId.Title] = title;
                            item.Update();
                            web.AllowUnsafeUpdates = false;
                            result.FileReceivedLength = file.Length;
                        }
                    }
                }
                result.IsSuccess = true;
            }
            catch (Exception e)
            {
                //log to ULS
                result.IsSuccess = false;
                result.ErrorMessage = e.Message;
            }
            finally
            {
                fileContent.Close();
            }
            return result;
        }
    }
}

The code was simple that it will receive the stream and save it to MemoryStream first before add it using Files.Add. Then we get listitem and update its property. You need to setting AllowUnsafeUpdates to true before add files to prevent error page need revalidation.

At this point you can try upload files using this REST service, but SharePoint limit max receive content length to only 18KB. Googling it out many people suggesting to define service reference in web.config, but it won't work. You can't change svc endpoint on web.config or on anything else as I aware of, it took me two days to figure this out before I found this site. It needs to be done on code (or PowerShell?) . We need to set that maxReceiveMessageSize larger than it's default value 18KB. How do you add those FeaturedInstalled method in SharePoint 2013? I googled and found another site that explained this. Basically you need to override a FeaturedInstalled method and set that endpoint in there. Wrapping it all together, this is what I did. We need to set maxreceivecontent on wcfServiceSetting for our REST service file. We can set it during feature deployment.
  1. Add a new class, I name it FeatureEvent.cs
  2. Inherits that class to SPFeatureReceiver
  3. Override method FeatureInstalled
  4. Then add code to change ReaderQuotasMaxStringContentLength, ReaderQuotasMaxArrayLength, ReaderQuotasMaxBytesPerRead, MaxReceivedMessageSize just like bellow

using Microsoft.SharePoint;
using Microsoft.SharePoint.Administration;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;

namespace UploadFileUsingREST
{
    public class FeatureEvent : SPFeatureReceiver
    {
        public override void FeatureInstalled(SPFeatureReceiverProperties properties)
        {
            SPWebService contentService = SPWebService.ContentService;
            SPWcfServiceSettings wcfServiceSettings = new SPWcfServiceSettings();
            wcfServiceSettings.ReaderQuotasMaxStringContentLength = Int32.MaxValue;
            wcfServiceSettings.ReaderQuotasMaxArrayLength = Int32.MaxValue;
            wcfServiceSettings.ReaderQuotasMaxBytesPerRead = Int32.MaxValue;
            wcfServiceSettings.MaxReceivedMessageSize = Int32.MaxValue;
            contentService.WcfServiceSettings["uploadrest.svc"] = wcfServiceSettings;
            contentService.Update(true);
        }
    }
}

We then need to create a feature that will run that file when the feature got installed.
  1. Create a new SharePoint feature file
  2. Open that feature and click on Manifest tab
  3. Expand Edit option
  4. Then add ReceiverAssembly and ReceiverClass just like bellow

<?xml version="1.0" encoding="utf-8" ?>
<Feature xmlns="http://schemas.microsoft.com/sharepoint/" ReceiverAssembly="UploadFileUsingREST, Version=1.0.0.0, Culture=neutral, PublicKeyToken=282a31b1b9f5b608" ReceiverClass="UploadFileUsingREST.FeatureEvent">
</Feature>

Deploy it and test it, I've created a console program to test this svc


    class Program
    {
        static void Main(string[] args)
        {
            FileInfo fi = new FileInfo(@"C:\testfile.pdf");
            Console.WriteLine(fi.Length);
            string urlFull = string.Format("http://documentrepositorysite/_vti_bin/uploadrest.svc/UploadFile/{0}/{1}", fi.Name, fi.Name);
            HttpWebRequest client = (HttpWebRequest)WebRequest.Create(urlFull);
            client.Credentials = new NetworkCredential("username", "userpassword", "userdomain");
            client.Method = "POST";
            client.UseDefaultCredentials = true;
            client.PreAuthenticate = true;
            using (var requestStream = client.GetRequestStream())
            {
                using (var file = fi.OpenRead())
                {
                    file.CopyTo(requestStream);
                }
            }
            using (var response = client.GetResponse())
            {
                StreamReader reader = new StreamReader(response.GetResponseStream());
                string respon = reader.ReadToEnd();
                Console.WriteLine(respon);
                Console.ReadLine();
            }
        }
    }

Testing it with 11MB file, and it completed in 10s.
Complete source code

No comments:

Post a Comment