Digging into Microsoft Office SharePoint Server 2007 (MOSS 2007), Windows SharePoint Services (WSS) v3.0, Windows Workflow Foundation (WF), and InfoPath 2007.

Tuesday, October 23, 2007

Upload a File to a SharePoint Document Library - Part I

The following helper class demonstrates a few techniques that allow documents to be uploaded to a SharePoint document library programmatically without using the API or a custom web service. You don't need to specify a document library name, and it will create any folders specified in the URL as required. File meta data will be updated if any properties are passed.

To use this code add a reference to the SharePoint Lists service (/_vti_bin/Lists.asmx) and name it ‘ListsService’. The code was written against MOSS 2007.

To download files use the GetItem method of the SharePoint Copy service (/_vti_bin/Copy.asmx). While it’s possible to upload files using the CopyIntoItems method of this service, it won’t create folders as needed, and you’d probably want to remove the copy link that is created.

It's also possible to use Front Page Server Extensions and RPC calls to upload files with meta data - the code for which is a bit more efficient as it doesn't require web service calls. Using RPC calls is covered in Part II.

Update: You can download a comprehensive c# class library to automate RPC calls - including uploading files to a SharePoint document library. See this blog post for more information.

 

using System;

using System.Collections;

using System.Collections.Generic;

using System.Text;

using System.Net;

using System.IO;

using System.Xml;

namespace DevHoleDemo

{

class Program

{

static void Main(string[] args)

{

DocLibHelper docLibHelper = new DocLibHelper();

Dictionary<string, object> properties = new Dictionary<string, object>();

properties.Add("Title", "Test Title");

//Create or overwrite text file test.txt in 'Docs' document library creating folder 'Test Folder' as required.

docLibHelper.Upload("http://localhost/Docs/Test Folder/test.txt", System.Text.Encoding.ASCII.GetBytes("Test text."), properties);

}

}

public class DocLibHelper

{

ListsService.Lists m_listService;

ICredentials m_credentials;

ListInfoCollection m_lists;

public DocLibHelper()

{

m_credentials = CredentialCache.DefaultCredentials;

m_listService = new ListsService.Lists();

m_listService.Credentials = m_credentials;

m_lists = new ListInfoCollection(m_listService);

}

public class ListInfo

{

public string m_rootFolder;

public string m_listName;

public string m_version;

public string m_webUrl;

public ListInfo(XmlNode listResponse)

{

m_rootFolder = listResponse.Attributes["RootFolder"].Value + "/";

m_listName = listResponse.Attributes["ID"].Value;

m_version = listResponse.Attributes["Version"].Value;

}

public bool IsMatch(string url)

{

try

{

url += "/";

return url.Substring(0, m_rootFolder.Length) == m_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.m_listName))

{

ListInfo listInfo = new ListInfo(CallService(ref webUrl, delegate { return m_listService.GetList(fileInfo.LookupName); }));

listInfo.m_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.m_webUrl = webUrl;

m_lists.Add(listInfo.m_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.m_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.m_listName))

return m_listInfo.m_listName;

return URI.LocalPath;

}

}

public FileInfo(string url, byte[] bytes, Dictionary<string, object> properties)

{

m_URL = url.Replace("%20", " ");

m_bytes = bytes;

m_properties = properties;

}

}

public bool Upload(string destinationUrl, byte[] bytes, Dictionary<string, object> properties)

{

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 = m_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.m_version);

updates.SetAttribute("PreCalc", "TRUE");

updates.InnerXml = sb.ToString();

m_listService.Url = fileInfo.m_listInfo.m_webUrl + "/_vti_bin/Lists.asmx";

XmlNode updatesResponse = m_listService.UpdateListItems(fileInfo.m_listInfo.m_listName, updates);

if (updatesResponse.FirstChild.FirstChild.InnerText != "0x00000000")

throw new Exception("Could not update properties.");

}

return true;

}

catch (WebException)

{

return false;

}

}

private bool CreateFolder(string folderURL)

{

try

{

WebRequest request = WebRequest.Create(folderURL);

request.Credentials = m_credentials;

request.Method = "MKCOL";

WebResponse response = request.GetResponse();

response.Close();

return true;

}

catch (WebException)

{

return false;

}

}

}

}


34 comments:

Anonymous said...

What's wrong with HTTP "PUT"?

txs8311 said...

Nothing - the code above does use HTTP PUT to write the file, and if a simple write is all you want to do then you're right - that's all you need.

If you want to write meta data though you have to do a bit more than that. The code above goes a step beyond that, because it discovers the document library name from the URL, creates any folders, and persists the list information so it doesn't have to re-query the web service.

Higo said...

It works for a document library, like intended.
But is it possible to make it work with a custom list? I've tried it it, but no success.

Higo said...

! The code DOES seem to work , because i can see the file in the custom list. But i do get an error: 0x81020016 wich seems to mean that it thinks the file does not exist.

So if i just ignore this "error" it should work.

txs8311 said...

Yeah, could be a bit dodgy. For custom lists, you might want to take a look at the AddAttachment operation that the Lists web service provides. It won't work for uploading files to document libraries but it might be what you're after.

Mike Smith said...

Oooh, I've been looking for something like this, but what I'd really like is to be able to post a chunk of HTML up to a specified Sharepoint Wiki page. How would one go about that?

Anonymous said...

Hey!

I use your code in a case. Everything works fine with doc files. But when I try to upload a docx file some bytes are lost and the document gets corrupted. Please help!

Toni said...

Hi!

Can you please test this with docx and doc documents with som images in it?

txs8311 said...

I tested for docx and it works ok for me, e.g.:

docLibHelper.Upload("http://localhost/Docs/Test Folder/Test.docx", File.ReadAllBytes(@"D:\Temp\Test.docx"), properties);

If all the bytes are getting uploaded, I would guess it's a machine related issue. Try testing on a different machine or user profile. Clearing your temporary internet files can sometimes fix browser problems. Also, there's a second post (Part II) that uses a different technique for uploading if you're convinced it's the code in this example.

Toni said...

Part II worked fine with uploading both docx and doc.
Now I have problem with uploading the metadata with docx in Part II. With doc everything workes fine.
Thank you for your help.

Anonymous said...

Receiving error 0x80070005 (Error occurred when updating list.)

I have admin privledges, so I wouldn't expect it to be a security issue, but everything I've read points to that...Any ideas?

Srinivasan said...

hi

iam getting an error like file not found(404) . i tried to upload a document to document library and even when i try to create a folder , i got the same error. can you help me in fixing this ?

Sandeep said...

Hi,

Great code! I noticed that the version history is incremented by a version once the file metadata has been added. It would be nice if when uploading it would keep the same version when the initital file metadata is added (i.e. for custom content types/columns on the document library). Do you think that this is actually possible using your code?

Sean Moran said...

First let me say that I appreciate this code a lot. Now the problem:

I am trying to hit a server elsewhere in out environment. I am creating a new NetworkCredential and passing in args that work when I enter them manually to hit my SharePoint site.

However, when I try to use them TryToUpload method I get an error at

WebResponse response = request.GetResponse();

The error is:


base {System.InvalidOperationException} = System.InvalidOperationException

base {object} = object
_COMPlusExceptionCode = -532459699

This appears to be a security issue.

Any thoughts? The credentials work fine to interrogate the lists and such. It seems to be tripping up on the upload.

Thanks,

Sean

txs8311 said...

Sean - I'm not sure that I've come across that one before. Someone posting here seemed to have a similar issue - perhaps adding a line like -

request.Proxy = m_listService.Proxy;

immediately after the line

request.Credentials = m_credentials;

- might help. If not, take a look at the RPC code posted in Part II of this article for an alternative remote upload method.

bobby said...

Have anyone got the VB version of this code.

André Rentes said...

Hi, very good code!!! But I have a problem! When I try upload a file with parameters I received the error "0x80070005" The file was upload, but my fields are not updated.

txs8311 said...

André - error code 0x80070005 can indicate that the user credentials passed to the lists web service do not have rights to update the list.

Frank Bell said...

Hey to start I want to say Thanks A LOT for this post it really helped me out getting something going.

If you have time, I was wondering what I would have to do to remove the "overwrite" fuctionality. If the file already exists, I need it to error out.

Thanks again.

txs8311 said...

Frank,

Take a look at the If-None-Match request header.

e.g.:

request.Method = "PUT";
request.Headers.Add("If-None-Match", "*");

Alternatively, the RPC method in Part II of this post includes an overwrite "putOption".

NoJ said...

Nice idea and good coding!
I used another hack for uploading to a DocLib using existing Web Services:
1. Upload the file to an ImageLib with Imaging.Upload
2. Copy/Move the file with Copy.CopyIntoItemsLocal

since it uses existing Web Services, it only needs ~70 lines of code.

Source and sample project is available at http://blog.janus.cx/archives/251-Uploading-any-file-to-a-SharePoint-DocumentLibraray-using-WebServices.html

Anonymous said...

Great post!

I'm following the basic structure of the article to upload to a document library via HTTP PUT. It works great.

Here's one problem I'm having though ... if versioning is turned on for the document library AND require check out is set to yes for the document library, then after the doc is uploaded it remains checked out. See this URL for more details:

http://sharemypoint.wordpress.com/2007/12/20/automatically-checkin-files-after-uploading/

So you think solving this problem would be easy -- just use the Lists.CheckInFile web service call to execute the check in. However, this is not working. WSS is returning the following error:

The system cannot find the file specified. (Exception from HRESULT: 0x80070002)

After playing around some more, if I take the URL to the newly uploaded file (the same one I pass to CheckInFile) and paste that URL into a new browser window, the browser window returns a 404 PAGE NOT FOUND error. However, if open a new browser window, login to my SharePoint site, *then* paste the URL into the browser window, the document opens fine.

Thus, for new documents that have yet to be checked in, SP is apparently throwing a 404 unless you are already logged in. Thus, the CheckInFile web service call must be failing for the same reason.

Other ways you can think of to work around this? Can check in be done via a RPC call or something?

Here's what I'm after:

- Configure doc library to enable versioning (major OR minor/major) and Require Check Out = "yes"
- Upload document via HTTP PUT, as outlined in your post
- Immediately after upload, check document in

Thanks in advance!

txs8311 said...

Yep - check-in can be done via FrontPage Server Extensions RPC calls (see the checkin document method on MSDN).

Check out the post below - you can download code from our site that wraps most of the FPSE methods and provides additional functionality.

http://geek.hubkey.com/2008/07/programatically-manage-remote-documents.html

Mike said...

Hello!

Just wanted to start by saying Great Work! and thanks for sharing.

If you have some time, I was wondering what this was doing --

if (updatesResponse.FirstChild.FirstChild.InnerText != "0x00000000") throw new Exception("Could not update properties.");


I get that exception randomly from users. Some times they can close out and redo it and it works. So, I'm guessing that "0x00000000" means that it was successful. But, is there anyway to determine why it wasn't?

Thanks a lot!

-Mike

txs8311 said...

Mike, yep error code 0 means success. You can get a description of the error code by parsing the returned xml for the ErrorText element if the error code is non-zero, e.g.

throw new Exception(updatesResponse.FirstChild.FirstChild.NextSibling.InnerText);

Ben said...

Hi, It is working great! But i have one question: how do i fill in metadata for datetime fields?

txs8311 said...

Ben, format your DateTime fields in the ISO 8601 format with a time zone designator for the zero UTC offset after it (a 'Z'). The DateTime field should in the local time zone (i.e. not actually converted to UTC).

e.g.

properties.Add("dateandtime", DateTime.Now.ToString("s") + "Z");

j said...

This is great. Thank you!!!

APG said...

I am new to development with Sharepoint. Why would it be beneficial to write to solve the upload problem INDEPENDENT OF THE SHAREPOINT API?

txs8311 said...

apg - the API is implemented through dlls that are installed on the server only. Even if you have the dlls installed on a client machine, you'll receive a FileNotFoundException when trying to connect to a remote server. This is because the "SharePoint object model can only be used when the application is run on a server in the SharePoint farm. It cannot be executed remotely." (See the first problem resolution in the SharePoint Development and Programming FAQ.)

An example of uploading a file that resides locally on the server using the API can be found here.

Juan Alvarez said...

Thanks a lot, it worked with Doc Libs but i have a problem with Custom Lists. I just have one field "Title", when i had the property it returns:

"0x81020016Item does not exist\n\nThe page you selected contains an item that does not exist. It may have been deleted by another user."

I need help over here, how can a page "COUNTAINS" something that does not exist :S

txs8311 said...

Juan - this code will only work with document libraries. We'll be posting about an easy way to remotely update SharePoint lists in the near future. Thanks.

Anonymous said...

The code works, but I haven`t understood why some files are well uploaded, but another - otherwise!
The code retruns me the error 409, when WebResponse response = request.GetResponse(); was executing! Could you please make me clear in this problem?

Anonymous said...

What a great solution! Everything worked on first try! Thanks, it saved my day!

Please Note

The code on this blog is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.