2009-03-30

How To: Upload to SharePoint Document Library

SharePoint, in my opinion, is a pain the butt. Is it necessarily a bad tool? No. Is it not capable of a lot of powerful things? No. Is it something that your average developer wants to develop for? No. But, there's a lot of powerful things that you can do using SharePoint, and unfortunately there's not a lot of great documentation out there. Ubercode to the rescue!

Document Libraries, in SharePoint, are like Network File Shares. They're accessible across an organization, with security policies managed by the Beast. The most significant feature that a SharePoint Document Library gives you over a standard Network File Share--versioning. If you're using SharePoint, but not using versioning, you'd better have a good reason [and there's a few].

From a developer's scope, you can access many of the common SharePoint features using the built in Web Services. This is a comprehensive tutorial of how-to Upload a Document into a Document Library.

First, you'll need to add a Web Service Reference to Lists.asmx.

  1. Create a new Project in Visual Studio (in this example, 2008). I've created a Web app for my project.
  2. Right click on your project in your Solution Explorer and choose "Add Web Reference".
  3. In the Url Box, enter this text:
    http://SharePointMachineName/_vti_bin/Lists.asmx
    Replacing SharePointMachineName with the Url of your SharePoint Server.
    image
  4. In my picture, Web reference name is grayed out, but you should be able to name it what you'd like. I'd recommend naming this "ListsService".
  5. Click Add Reference.

Next, you'll need to consume this Web Service. You should write some sort of wrapper class that encapsulates some of these types of common functions. I've borrowed this heavily from this fellow (http://geek.hubkey.com/2007/10/upload-file-to-sharepoint-document.html):

using System;
using System.Collections;
using System.Collections.Generic;
using System.Configuration;
using System.Data;
using System.IO;
using System.Net;
using System.Text;
using System.Xml;
using VersionsService; // TODO: Come back next week for my Version Iteration example..
using ListsService;

public class DocumentLibrary
{
    Lists _listsService;
    public ICredentials Credentials
    {
        get;
        set;
    }
    ListInfoCollection m_lists;

    public DocumentLibrary()
    {
        // TODO: Replace with your User Name and Password
        string uname = ConfigurationManager.AppSettings["uname"].ToString();
        string pword = ConfigurationManager.AppSettings["pword"].ToString();
        string domain = ConfigurationManager.AppSettings["domain"].ToString();
        NetworkCredential nc = new NetworkCredential(uname, pword, domain);

        // TODO: Alternately, if you're using Windows Authentication on your web server, you can use the DefaultCredentials.
        //_credentials = CredentialCache.DefaultCredentials;
        Credentials = nc;
        _listsService = new ListsService.Lists();
        _listsService.Credentials = Credentials;
        m_lists = new ListInfoCollection(_listsService);
    }

    #region Classes
    // TODO: these classes maybe better served in a different location.
    public class ListInfo
    {
        public string _rootFolder;
        public string _listName;
        public string _version;
        public string _webUrl;
        public ListInfo(XmlNode listResponse)
        {
            _rootFolder = listResponse.Attributes["RootFolder"].Value + "/";
            _listName = listResponse.Attributes["ID"].Value;
            _version = listResponse.Attributes["Version"].Value;
        }
        public bool IsMatch(string url)
        {
            try
            {
                url += "/";
                return url.Substring(0, _rootFolder.Length) == _rootFolder;
            }
            catch
            {
            }
            return false;
        }
    }

    public class ListInfoCollection : IEnumerable<ListInfo>
    {
        ListsService.Lists m_listService;
        Dictionary<string, ListInfo> m_lists = new Dictionary<string, ListInfo>();
        public ListInfoCollection(ListsService.Lists listService)
        {
            m_listService = listService;
        }
        public IEnumerator<ListInfo> GetEnumerator()
        {
            return m_lists.Values.GetEnumerator();
        }
        IEnumerator IEnumerable.GetEnumerator()
        {
            return this.GetEnumerator();
        }
        public ListInfo Find(FileInfo fileInfo)
        {
            if (m_lists.ContainsKey(fileInfo.LookupName))
            {
                return m_lists[fileInfo.LookupName];
            }
            foreach (ListInfo li in m_lists.Values)
            {
                if (li.IsMatch(fileInfo.LookupName))
                {
                    return li;
                }
            }
            string webUrl = fileInfo.m_URL;
            if (fileInfo.m_listInfo != null && !string.IsNullOrEmpty(fileInfo.m_listInfo._listName))
            {
                ListInfo listInfo = new ListInfo(CallService(ref webUrl, delegate { return m_listService.GetList(fileInfo.LookupName); }));
                listInfo._webUrl = webUrl;
                return listInfo;
            }
            else
            {
                XmlNode lists = CallService(ref webUrl, delegate { return m_listService.GetListCollection(); });
                if (lists == null)
                {
                    throw new Exception("Could not find web.");
                }
                //Find list by RootFolder (which doesn't seem to be populated in GetListCollection response so must iterate GetList response)
                foreach (XmlNode list in lists.ChildNodes)
                {
                    ListInfo listInfo = new ListInfo(m_listService.GetList(list.Attributes["Name"].Value));
                    listInfo._webUrl = webUrl;
                    m_lists.Add(listInfo._listName, listInfo);
                    if (listInfo.IsMatch(fileInfo.LookupName))
                    {
                        return listInfo;
                    }
                }
            }
            throw new Exception("Could not find list.");
        }
        private delegate XmlNode ServiceOperation();
        private XmlNode CallService(ref string webURL, ServiceOperation serviceOperation)
        {
            try
            {
                webURL = webURL.Substring(0, webURL.LastIndexOf("/"));
                try
                {
                    m_listService.Url = webURL + "/_vti_bin/Lists.asmx";
                    return serviceOperation();
                }
                catch
                {
                    return CallService(ref webURL, serviceOperation);
                }
            }
            catch
            {
                webURL = null;
                return null;
            }
        }
    }

    public class FileInfo
    {
        public string m_URL;
        public byte[] m_bytes;
        public Dictionary<string, object> m_properties;
        public ListInfo m_listInfo;
        public bool m_ensureFolders = true;
        private Uri m_uri;
        public bool HasProperties
        {
            get
            {
                return m_properties != null && m_properties.Count > 0;
            }
        }
        public string RelativeFilePath
        {
            get
            {
                return m_URL.Substring(m_URL.IndexOf(m_listInfo._rootFolder) + 1);
            }
        }
        public Uri URI
        {
            get
            {
                if (m_uri == null)
                {
                    m_uri = new Uri(m_URL);
                }
                return m_uri;
            }
        }
        public string LookupName
        {
            get
            {
                if (m_listInfo != null && !string.IsNullOrEmpty(m_listInfo._listName))
                {
                    return m_listInfo._listName;
                }
                return URI.LocalPath;
            }
        }
        // TODO: I commented out the property Dictionary because I did not need it. See linked Article. Your needs may vary
        public FileInfo(string url, byte[] bytes)//, Dictionary<string, object> properties)
        {
            m_URL = url.Replace("%20", " ");
            m_bytes = bytes;
            // TODO: I commented out the property Dictionary because I did not need it. See linked Article. Your needs may vary
            //m_properties = properties;
        }
    }
    #endregion Classes

    public bool Upload(string destinationUrl, byte[] bytes, Dictionary<string, object> properties)
    {
        // TODO: I commented out the property Dictionary because I did not need it. See linked Article. Your needs may vary
        return Upload(new FileInfo(destinationUrl, bytes));//, properties));
    }

    public bool Upload(FileInfo fileInfo)
    {
        if (fileInfo.HasProperties)
        {
            fileInfo.m_listInfo = m_lists.Find(fileInfo);
        }
        bool result = TryToUpload(fileInfo);
        if (!result && fileInfo.m_ensureFolders)
        {
            string root = fileInfo.URI.AbsoluteUri.Replace(fileInfo.URI.AbsolutePath, "");
            for (int i = 0; i < fileInfo.URI.Segments.Length - 1; i++)
            {
                root += fileInfo.URI.Segments[i];
                if (i > 1)
                {
                    CreateFolder(root);
                }
            }
            result = TryToUpload(fileInfo);
        }
        return result;
    }

    private bool TryToUpload(FileInfo fileInfo)
    {
        try
        {
            WebRequest request = WebRequest.Create(fileInfo.m_URL);
            request.Credentials = Credentials;
            request.Method = "PUT";
            byte[] buffer = new byte[1024];
            using (Stream stream = request.GetRequestStream())
            {
                using (MemoryStream ms = new MemoryStream(fileInfo.m_bytes))
                {
                    for (int i = ms.Read(buffer, 0, buffer.Length); i > 0; i = ms.Read(buffer, 0, buffer.Length))
                    {
                        stream.Write(buffer, 0, i);
                    }
                }
            }
            WebResponse response = request.GetResponse();
            response.Close();
            if (fileInfo.HasProperties)
            {
                StringBuilder sb = new StringBuilder();
                sb.Append("<Method ID='1' Cmd='Update'><Field Name='ID'/>");
                sb.AppendFormat("<Field Name='FileRef'>{0}</Field>", fileInfo.m_URL);
                foreach (KeyValuePair<string, object> property in fileInfo.m_properties)
                {
                    sb.AppendFormat("<Field Name='{0}'>{1}</Field>", property.Key, property.Value);
                }
                sb.Append("</Method>");
                System.Xml.XmlElement updates = (new System.Xml.XmlDocument()).CreateElement("Batch");
                updates.SetAttribute("OnError", "Continue");
                updates.SetAttribute("ListVersion", fileInfo.m_listInfo._version);
                updates.SetAttribute("PreCalc", "TRUE");
                updates.InnerXml = sb.ToString();
                _listsService.Url = fileInfo.m_listInfo._webUrl + "/_vti_bin/Lists.asmx";
                XmlNode updatesResponse = _listsService.UpdateListItems(fileInfo.m_listInfo._listName, updates);
                if (updatesResponse.FirstChild.FirstChild.InnerText != "0x00000000")
                {
                    throw new Exception("Could not update properties.");
                }
            }
            return true;
        }
        catch (WebException ex)
        {
            throw ex;
        }
    }

    private bool CreateFolder(string folderURL)
    {
        try
        {
            WebRequest request = WebRequest.Create(folderURL);
            request.Credentials = Credentials;
            request.Method = "MKCOL";
            WebResponse response = request.GetResponse();
            response.Close();
            return true;
        }
        catch (WebException)
        {
            return false;
        }
    }
}

-----------------------------------

And now to consume this... Create an ASPX file with an File Uploader control, and a Button. You're code behind will look something like this:

protected void btnUpload_Click(object sender, EventArgs e)
    {
        if (fileUpload.PostedFile.InputStream == null)
        {
            throw new Exception("You must upload a file");
        }
        // Technically, you could handle this differently, but I just needed something (in bytes) smaller than an int.
        if (fileUpload.PostedFile.InputStream.Length > int.MaxValue)
        {
            throw new Exception("File is too large to use the BinaryReader.ReadBytes function, which expects an int (32)");
        }

        DocumentLibrary doclibHelper = new DocumentLibrary();
        Dictionary<string, object> properties = new Dictionary<string, object>();
        // TODO: Should this be something that your user types in when they upload their file?
        properties.Add("Title", "Test Title");

        string destinationName = http://SharePointMachineName/ShareSiteName/DocumentLibraryName/ + fileUpload.PostedFile.FileName;
        BinaryReader b = new BinaryReader(fileUpload.PostedFile.InputStream);
        byte[] file = b.ReadBytes((int)fileUpload.PostedFile.InputStream.Length);
        doclibHelper.Upload(destinationName, file, properties);
    }

I'll be working to unify this code with bleak's standards, but this yields results. If it's not broken, don't fix it, right?

Zoidberg away!

3 comments:

Paul said...

I have searched your blog, but I don't find any reference to permission to use, etc. I know that sounds innane, but without it your code is implicitly copyrighted. Can you point me to any permissions or just reply with it?

Matthew Ozvat said...

Paul, OMG, go be a lawyer instead of a coder.

Thanks Jamal for the examples and citing the original author.

Tinu said...

Thanks for this, it really helped me...