Background
I've been building a lot of SharePoint 2013 Online Apps lately (both provider and SharePoint hosted). Like most developers, I initially struggled with a few OAuth App Token challenges when trying to persist data between page loads. And just when I thought I had a good methodology down, I ran into an issue downloading documents from a document library.
I kept receiving a 403 (forbidden) HTTP response code when I attempted to read the file from the document library. I checked the document library permissions and they were correct. So, I did some digging online and found others complaining about the issue.
In order to isolate the exact issue, first, I created a document library and uploaded a few PDF documents. Then, I created a sample SharePoint 2013 App in visual studio. If you are unfamiliar with the process, here is a good link to get you started:
How to: Create a basic provider-hosted app for SharePoint. In order to limit the scope of this post, I will not cover specifically how I obtained the Microsoft.SharePoint.Client.ClientContext instance in the sample code here. Please reference the
Sources Consulted section below for related reading.
The Original Code
Here was my original sample code I used to isolate the issue:
/// <summary>
/// Downloads a file from a document library and saves it to disk
/// </summary>
/// <param name="context"></param>
/// <param name="fileNameToSaveToDisk"></param>
public void DownloadFileTest(ClientContext context, string fileNameInDocumentLib)
{
var list = context.Web.Lists.GetByTitle("DocumentLibraryName");
context.Load(list);
context.Load(list.RootFolder);
context.ExecuteQuery();
string serverRelativeUrl = list.RootFolder.ServerRelativeUrl;
serverRelativeUrl += "/" + fileNameInDocumentLib;
// Save file to a temporary location to prove all file contents were received
string localFilePath = "C:\\Temp\\test.pdf";
int position = 1;
int bufferSize = 200000;
// Open the file
using (FileInformation fileInfo = Microsoft.SharePoint.Client.File.OpenBinaryDirect(context, serverRelativeUrl))
{
Byte[] readBuffer = new Byte[bufferSize];
// code below causes 403 error
using (System.IO.Stream stream = System.IO.File.Create(localFilePath))
{
while (position > 0)
{
position = fileInfo.Stream.Read(readBuffer, 0, bufferSize);
stream.Write(readBuffer, 0, position);
readBuffer = new Byte[bufferSize];
}
fileInfo.Stream.Flush();
stream.Flush();
}
}
}
The problem was stemming from the use of the Microsoft.SharePoint.Client.File.
OpenBinaryDirect() method. This is a
static method that is supposed to grab the file without having to call the ExecuteQuery() Method. However, since I was using the oAuth App authentication model, this method apparently doesn't pass my app token along like the ExecuteQuery() method does.
After pouring over lots of MSDN documentation, various blogs and asking a friend or two, I modified the code above and it works as expected:
The Working Code
/// <summary>
/// Grab a document from my document library utilizing the Microsoft.SharePoint.Client.File.OpenBinaryStream() method
/// </summary>
/// <param name="context"></param>
/// <param name="fileNameInDocumentLib"></param>
public void DownloadFileTest2(ClientContext context, string fileNameInDocumentLib)
{
// Find the file by name
CamlQuery camlQuery = new CamlQuery();
camlQuery.ViewXml = @"<View>
<Query>
<Where>
<Eq>
<FieldRef Name=""FileLeafRef"" />
<Value Type=""Text"">" + fileNameInDocumentLib + @"</Value>
</Eq>
</Where>
</Query>
</View>";
List list = context.Web.Lists.GetByTitle("DocumentLibraryName");
ListItemCollection listItems = list.GetItems(camlQuery);
context.Load(listItems);
context.ExecuteQuery();
if (listItems.Count > 0)
{
var item = listItems.First();
Microsoft.SharePoint.Client.File file = item.File;
ClientResult<Stream> data = file.OpenBinaryStream();
// Load the Stream data for the file
context.Load(file);
context.ExecuteQuery();
// If data received, write it to disk so I can verify the contents
if (data != null)
{
int position = 1;
int bufferSize = 200000;
Byte[] readBuffer = new Byte[bufferSize];
string localFilePath = "C:\\Temp\\test.pdf";
using (System.IO.Stream stream = System.IO.File.Create(localFilePath))
{
while (position > 0)
{
// data.Value holds the Stream
position = data.Value.Read(readBuffer, 0, bufferSize);
stream.Write(readBuffer, 0, position);
readBuffer = new Byte[bufferSize];
}
stream.Flush();
}
}
}
}
Concluding Remarks
Notice in the example above I load the resulting Microsoft.SharePoint.Client.File object's Stream data by calling the OpenBinaryStream() method and then loading the contents by calling the ExecuteQuery() method on the current context. This approach allowed me to successfully return the contents of the file. The only reason I wrote the file to my local hard drive was to verify that the file contents were correct. It's kinda hard to validate I'm getting an un-corrupted PDF file simply by viewing a byte array.
As an aside, since it's probably more useful to return the file contents as a byte array, you could replace the code that writes the file to disk with something like:
if (data != null)
{
MemoryStream memStream = new MemoryStream();
data.Value.CopyTo(memStream);
// returns a byte[]
return memStream.ToArray();
}
Like/Dislike what you see? Feel free to drop me a line.
Sources Consulted
Microsoft.SharePoint.Client.File.OpenBinaryDirect() Method
Inside Microsoft OAuth Context Tokens
Authorization and authentication for apps in SharePoint 2013
OAuth authentication and authorization flow for cloud-hosted apps in SharePoint 2013
How to: Create a basic provider-hosted app for SharePoint