Wesley Bakker

Interesting things I encounter doing my job...

Sponsors

News

Wesley Bakker
motion10
Rivium Quadrant 151
2909 LC Capelle aan den IJssel
Region of Rotterdam
The Netherlands
Phone: +31 10 2351035

(feel free to chat with me)

Add to Technorati Favorites

DownloadAsZip SharePoint Feature Part II

Sometimes I get a very simple question which results in a not so simple solution. And this was one of those.

“How can I download multiple files and or folder from a SharePoint list at once?”

My first response was to use ‘explorer view’ but it lacks a lot of functionality. How about filters or views applied? As soon as you select the explorer view, all filters are dropped. How about cross browser support? Explorer view is available on IE only. So explorer view is more an ‘all or nothing IE only’ solution.

The idea for another SharePoint customization was born….

Two issues

Actually we’re dealing with two issues here. The first one is that we do not have a way to select multiple items from a SharePoint list view. The second one is to actually download those selected files.

I decided to really cut them loose. Because the feature of selecting multiple items could come in handy for other features than downloading as well. I can think of mailing them as attachment, deleting multiple items at once, send them to the repository etc. etc. etc.

First things first, so I solved the selection of multiple items in one of my previous posts. Today we’re going to create the code for our http handler.

Considerations

Just to be short. We do NOT want to create a file on the server and then send it to the client and we definitely do NOT want to write the zip file to a memory stream before sending it to the client either for multiple reasons.

  1. Where to leave the temporary files and when to cleanup?
  2. What will a client request for 3Gb. of data will do with my memory?
  3. What about timeouts if the creation of a file takes 3 minutes?

We have only one option left and that’s to create the zip stream on the fly and hook it to our response stream. We don’t have any timeout issues because the client will start to receive bytes right away. We don’t have any issues with file storage and cleanup and if we use a buffer of only 4k for our stream we don’t have any memory considerations left.

! Do keep in mind though that compacting a file does cost a lot of processing power. Unfortunately that’s not something we can solve. !

Http Handler

I can’t stress this out enough: Don’t use a Page to stream data to your client. Just don’t. It is an http handler indeed but it will introduce a lot of overhead for nothing. You should create your own class that implements the IHttpHandler interface. So that’s what we’ll do.

We need some query string variable in there for us to know which files to add to the zip. So let’s outline them:

  • List
    - required id of the list
  • View
    - optional id of the view. We’ll use the default view if no view is given.
  • Item
    - optional comma separated list of item ids. If no items are selected the complete view will be added to the zip.

So the start of the ProcessRequest method in our custom handler looks like this:

/// <summary>
/// Enables processing of HTTP Web requests by a custom HttpHandler that implements the <see cref="T:System.Web.IHttpHandler"/> interface.
/// </summary>
/// <param name="context">An <see cref="T:System.Web.HttpContext"/> object that provides references to the intrinsic server objects (for example, Request, Response, Session, and Server) used to service HTTP requests.</param>
public void ProcessRequest(HttpContext context) {
    HttpResponse response = context.Response;
 
    string fileName = string.Concat(DateTime.Now.ToFileTimeUtc(), ".zip");
    string contentDisposition = string.Concat("attachment; filename=\"", fileName, "\"");
 
    response.Clear();
    response.ContentType = "application/zip";
    response.AddHeader("Content-Disposition", contentDisposition);
    response.Buffer = false;
 
    NameValueCollection queryString = context.Request.QueryString;
    string listId = queryString["List"];
    if (string.IsNullOrEmpty(listId)) {
        throw new WebException("Required query string parameter 'List' is not defined.");
    }
 
    string viewId = queryString["View"];
    string itemsString = queryString["Item"];

We clear the response stream and add the necessary headers before we send any content to the response stream. I decided to use DateTime.Now.ToFileTimeUtc() as the proposed filename to the client. We simply extract the query string parameters and check if the required List parameter is indeed there. If not we throw a WebException which SharePoint will graciously handle for us.

! What’s very important to notice here is that we set response.Buffer to false. In this way, everything we write to the response stream will be sent to the client right away. If we don’t set response.Buffer to false… we’ll have memory issues in no time. !

Zip Library

Although the .NET framework has a DeflateStream class to compress streams it still lacks the creation of zip files. In order to create a zip file you need to create file headers and stuff so I decided to use another library for it. First I went for the SharpZipLib but it has a GPL license which is not always permissible in a company so I looked around a little and there’s a very good alternative in the form of DotNetZip. Have a look at the conversation I had with one of the guys over here on how to solve some issues with it. They are really helpful! I didn’t have the time to rewrite my initial version of DownloadAsZip but I probably will do so in the future.

For now the code is based on the SharpZipLib and the rest of the ProcessRequest method looks like this:

SPWeb currentWeb = SPContext.Current.Web;
SPList selectedList = currentWeb.Lists[new Guid(listId)];
 
using (ZipOutputStream zipOutputStream = new ZipOutputStream(context.Response.OutputStream)) {
    zipOutputStream.SetLevel(9);
 
    if (!string.IsNullOrEmpty(itemsString)) {
        string[] items = itemsString.Split(',');
        foreach (string item in items) {
            int itemId;
            if (int.TryParse(item, out itemId)) {
                SPListItem listItem = selectedList.GetItemById(itemId);
                switch (listItem.FileSystemObjectType) {
                    case SPFileSystemObjectType.File:
                        zipOutputStream.AddSPFile(listItem.File);
                        break;
                    case SPFileSystemObjectType.Folder:
                        zipOutputStream.AddSPFolder(listItem.Folder);
                        break;
                    default:
                        throw new FileNotFoundException("No such file or folder.");
                }
            }
        }
    }
    else {
        SPView selectedView = selectedList.DefaultView;
        if (!string.IsNullOrEmpty(viewId)) {
            selectedView = selectedList.GetView(new Guid(viewId));
        }
 
        SPListItemCollection selectedItems = selectedList.GetItems(selectedView);
 
        zipOutputStream.AddSPListItemCollection(selectedItems);
    }
}
 
context.Response.End();
}

A few important things to notice.

In line 4 we create a ZipOutputStream and tie it to the OutputStream and on line five we set the compression level to the highest possible. Unfortunately there’s no documentation on what this does to performance and what’s the benefits in terms of compression ratio. It’s something we’ll still have to figure out.

And secondly we write files and folders to our (now zipped) response stream with the methods:

  • AddSPFile(SPFile)
  • AddSPFolder(SPFolder)
  • AddSPListItemCollection(SPListItemCollection)

Extension Methods

The guys of the SharpZipLib did not implement the methods to add SharePoint files to a ZipOutputStream. That’s something we have to do ourselves. I started with a few static methods to add items and I found myself passing the ZipOutputStream object continuously from one method to the other. With extension methods this is something we can refactor to a very nice and clean solution with virtualy no extra effort. So here’s the code for the accompanying ZipUtility class:

//-----------------------------------------------------------------------
// <copyright file="ZipUtility.cs" company="motion10">
//     Copyright (c) motion10. All rights reserved.
// </copyright>
//-----------------------------------------------------------------------
 
using System.IO;
using ICSharpCode.SharpZipLib.Zip;
using Microsoft.SharePoint;
 
namespace Motion10.SharePoint2007.Zip {
    public static class ZipUtility {
        /// <summary>
        /// Adds the SPListItemCollection to the zip output stream.
        /// </summary>
        /// <param name="zipOutputStream">The zip output stream.</param>
        /// <param name="listItemCollection">The list item collection.</param>
        public static void AddSPListItemCollection(this ZipOutputStream zipOutputStream, SPListItemCollection listItemCollection) {
            foreach (SPListItem listItem in listItemCollection) {
                switch (listItem.FileSystemObjectType) {
                    case SPFileSystemObjectType.File:
                        zipOutputStream.AddSPFile(listItem.File);
                        break;
                    case SPFileSystemObjectType.Folder:
                        zipOutputStream.AddSPFolder(listItem.Folder);
                        break;
                }
            }
        }
 
        /// <summary>
        /// Adds the SPFolder to the zip output stream.
        /// </summary>
        /// <param name="zipOutputStream">The zip output stream.</param>
        /// <param name="folder">The folder.</param>
        public static void AddSPFolder(this ZipOutputStream zipOutputStream, SPFolder folder) {
            zipOutputStream.AddSPFolder(folder, string.Empty);
        }
 
        /// <summary>
        /// Adds the SPFolder to the zip output stream.
        /// </summary>
        /// <param name="zipOutputStream">The zip output stream.</param>
        /// <param name="folder">The folder.</param>
        /// <param name="path">The path.</param>
        public static void AddSPFolder(this ZipOutputStream zipOutputStream, SPFolder folder, string path) {
            path = Path.Combine(path, folder.Name);
 
            ZipEntry entry = new ZipEntry(path + "/");
            zipOutputStream.PutNextEntry(entry);
 
            foreach (SPFile file in folder.Files) {
                zipOutputStream.AddSPFile(file, path);
            }
 
            foreach (SPFolder subFolder in folder.SubFolders) {
                AddSPFolder(zipOutputStream, subFolder, path);
            }
        }
 
        /// <summary>
        /// Adds the SPFile to the zip output stream.
        /// </summary>
        /// <param name="zipOutputStream">The zip output stream.</param>
        /// <param name="file">The file.</param>
        public static void AddSPFile(this ZipOutputStream zipOutputStream, SPFile file) {
            zipOutputStream.AddSPFile(file, string.Empty);
        }
 
        /// <summary>
        /// Adds the SPFile to the zip output stream.
        /// </summary>
        /// <param name="zipOutputStream">The zip output stream.</param>
        /// <param name="file">The file.</param>
        /// <param name="path">The path.</param>
        public static void AddSPFile(this ZipOutputStream zipOutputStream, SPFile file, string path) {
            string filePath = Path.Combine(path, file.Name);
 
            ZipEntry entry = new ZipEntry(filePath);
            entry.DateTime = file.TimeCreated;
            zipOutputStream.PutNextEntry(entry);
 
            using (Stream fileStream = file.OpenBinaryStream()) {
                byte[] buffer = new byte[4096];
                int sourceBytes;
                do {
                    sourceBytes = fileStream.Read(buffer, 0, buffer.Length);
                    zipOutputStream.Write(buffer, 0, sourceBytes);
                } while (sourceBytes > 0);
            }
        }
    }
}

 

Please take a look at the code carefully. What’s really important to notice here is that we always add one file at a time and while doing so we always have just one file stream opened inside a using statement(see AddSPFile method). Another important thing to notice is that we use a small buffer of only 4k. This is to make sure we don’t create a huge memory monster or choke SharePoint with many open file streams.

Implementing the handler

We have the code for the handler now but we have not yet registered this handler with IIS. There are two way to do so.

  1. Register the handler in the Web.config
    (we’ve edited the web.config before)
  2. Create an ashx file and add it to the _layouts directory

This time I decided to go for the last option. Because it has some minor advantages. The content of the DownloadAsZip.ashx file looks like this:

<%@ Assembly Name="SharePointSolutionPack, Version=1.0.0.0, Culture=neutral, PublicKeyToken=4a7cd02bdf107f7a"%>
<%@ WebHandler Language="C#" Class="Motion10.SharePoint2007.DownloadAsZipHandler" %>

This of course doesn’t contain any real code as that’s all in our DownloadAsZipHandler class.

Conclusion

I know I promised a complete solution in my previous post but you guys and girls have to wait a few more days for the button. This post will be way to long if I’ll go through the code for the DownloadAsZip custom action as well. Just bare with me for a few more days.

And please DO leave comments if you like or dislike anything you read on this blog. I’m always in for improvements.

Cheers and have fun,

Wes

Comments

DownloadAsZip SharePoint Feature Part II - Wesley Bakker said:

Pingback from  DownloadAsZip SharePoint Feature Part II - Wesley Bakker

# March 9, 2009 1:20 PM

Select Multiple List Items in SharePoint Feature - Wesley Bakker said:

Pingback from  Select Multiple List Items in SharePoint Feature - Wesley Bakker

# March 10, 2009 4:54 AM

Stjepan said:

I can't see how is the ZipUtility class connected with the rest of the solution. I understand the meaning of it but I just can't make it work.

I get an error on following line:

public static void AddSPListItemCollection(this ZipOutputStream zipOutputStream, SPListItemCollection listItemCollection)...

stating that the the type is expected (this is underlined)

Have I missed something?

# September 20, 2009 9:49 PM

JC said:

I am seeing the same issue as Stjepan.  Is this something Visual Studio 2008 can compile but not 2005?  

I have never seen this approach of adding methods to an existing class since DownloadAsZipHandler is actually calling ZipOutputStream class from ICSharpCode dll but calling the method AddSPFile which is defined in the ZipUtility class.

Any explanation would be helpful since this doesn't work in Visual Studio 2005.

# October 15, 2009 7:32 PM

webbes said:

Hi,

Have a look over here for extension methods:

weblogs.asp.net/.../new-orcas-language-feature-extension-methods.aspx

Cheers,

Wes

# October 16, 2009 3:35 AM

ItsMeSri said:

I created WSP file for download documents from my sharepoint site. Now If I open the zipped file it is giving error "The Compressed (Zipped) Folder is invalid or corrupted". But if I open same with Winrar software, it is working? What is wrong in the code? I just followed your postings.

# November 23, 2009 2:39 PM

webbes said:

@Sri:

I cannot see what you did wrong of course, but my samples only have the "It works on my machine" approval and well, it works on my machine. You could download the solution package and see if you still have the same error.

Cheers,

Wes

# November 23, 2009 2:49 PM

ItsMeSri said:

Where can I find solution in this post? Can please link to me?

# November 23, 2009 4:13 PM

webbes said:

@Sri:Look at the end of this post:

weblogs.asp.net/.../downloadaszip-sharepoint-feature-rtw.aspx

Cheers,

Wes

# November 24, 2009 3:57 PM
Leave a Comment

(required) 

(required) 

(optional)

(required)