Knowledge is power. We love to share it.

News related to Mono products, services and latest developments in our community.

Jasmin

Multiple download in Telerik RadFileExplorer

07/26/2011
Remote file management via RadFileExplorer

We have a special relation love & hate relation with Telerik in my company. Their occasional bugs and introduction of new ones with new versions (due to correction of previous bugs) are source of our endless frustrations, but when it comes to power and functionality, I don't think there's any other product that comes even close to their ASP.NET controls. Amongst a handful of controls they offer, their RadFileExplorer stands out as one of the best out there - with just a few lines of code one can easily provide a complete file management of a distant file system through a browser. All the standard options are present - cut, copy, paste, rename, delete, move (drag & drop) and sort. Since it's a web interface, one must have a way to transfer the files to the server and RadFileExplorer has a multiple upload feature included by default. Recently one of our clients asked me for a multiple download feature. Since a file download dialog box in a browser can be raised only for a single document, the only way to do this is to zip all the selected files and folders into a single .zip file. Our client and his users were fine with that, unzipping the file was a small extra effort for such a useful feature. 

Extending the default menu with the download option

Searching through Telerik forums, I found this post as a starting point of my search. The first step was to extend the context menu with an additional download option which would post back to the server and pass the information about all the selected files and folders. The mentioned post pointed to 2 KB articles (article1 and article2) on how to do this. It uses a classic method of a hidden field and hidden button control. Summed up, here are the steps:

1. Create a RadMenuItem and add it to the GridContextMenu's item collection, I did it in Page_Init:

RadMenuItem item = new RadMenuItem("Download");
item.PostBack =
false;
item.Value =
"Download";
ctlFileExplorer.GridContextMenu.Items.Add(item);

2. Also in Page_Init, configure a custom client event handler for context menu:

ctlFileExplorer.GridContextMenu.OnClientItemClicked = "extendedFileExplorer_onGridContextItemClicked";

3. Add the following 2 controls next to the RadFileExplorer:

<asp:HiddenField ID="ctlHiddenField" runat="server" />
<asp:Button ID="btnDownload" runat="server" style="display:none" />

and also attach the server event handler for the button click in Page_Init. The actual button is invisible, but it's click action will be triggered via javascript.

btnDownload.Click += new EventHandler(btnDownload_Click);

4. Add the following javascript block:

function extendedFileExplorer_onGridContextItemClicked(oGridMenu, args) {
    var menuItemText = args.get_item().get_text();
    if (menuItemText == "Download") {// 'Download' command
        extendedFileExplorer_sendItemsPathsToServer();
    }
}
 
function extendedFileExplorer_sendItemsPathsToServer() {
    var oExplorer = $find("<%= ctlFileExplorer.ClientID %>"); // Find the RadFileExplorer ; 
    var selectedItems = oExplorer.get_selectedItems(); // Retrieve the selected items ;
    var selectedItemsPaths = extendedFileExplorer_combinePathsInAString(selectedItems); // Get the paths as a string in specific format ;
 
    var hiddenField = $get("<%= ctlHiddenField.ClientID %>"); // Find the hiddenField 
    hiddenField.value = selectedItemsPaths;
    __doPostBack("<%= btnDownload.UniqueID %>", ""); // Call the 'btnDownload_Click' function on the server ;
}
 
function extendedFileExplorer_combinePathsInAString(arrayOfSelectedItems) {
    var itemPaths = new Array();
    for (var i = 0; i < arrayOfSelectedItems.length; i++) {
        // Pass all the selected paths ; 
        itemPaths.push(arrayOfSelectedItems[i].get_path());
    }
 
    // Return a string that contains the paths ; 
    return itemPaths.join("|");
}

5. Handle the download button click on the server:

void btnDownload_Click(object sender, EventArgs e)
{
    //Continued below...
}

Folder hierarchy in a zip file

The next step was facing the problem of combining multiple files and folders and keeping the folder hierarchy in a zip archive. One can't do that with .NET default GZip and Deflate stream classes, so I had to turn to the third party libraries. There are many available, both free and commercial, we've been using SharpZipLib a lot in my company because it's fast and free. However, it wouldn't do in this case, since it lacks the support for the folder hierarchy. Consulting with a colleague with more experience in this area, he pointed me to the DotNetZip library, which is also free. The only thing I didn't like about it was the fact that one can't zip from a stream, the file needs to be physically present on a disk instead. This would cause a slight performance issue in case of needing a different folder hierarchy in a zip archive than on disk, or if, for example, the files are read from database and archived. The only way to do this is to save the files to a temporary location, do the zipping logic and then delete them. Anyway, for RadFileExplorer multi download scenario, the files & folders are residing on a disk and the hierarchy in an archive file needs to match the actual one, so I didn't have to do this additional step.

Download logic

The following needs to be done in the btnDownload_Click method - pick up all the paths from the hidden field, zip them and all their contents and return the Combined.zip as a result. If a single file is selected, there's no need for zipping. Additionally, a special care needs to be taken regarding the empty folders, they need to be picked up separately since DotNetZip has a different method for adding the empty folders than the one used for adding the files within an archive. When adding both files and folders, the path in the archive is relative to their physical path, depending on what the root, i.e. currently selected folder is in the RadFileExplorer. Therefore the logic needs to subtract the current folder path from all the paths in the archive. Here's the code:

void btnDownload_Click(object sender, EventArgs e)
{
    string[] paths = ctlHiddenField.Value.Split('|');
    if (paths.Length > 0 && !String.IsNullOrEmpty(paths[0]))
    {
        byte[] downloadFile = null;
        string downloadFileName = String.Empty;
        if (paths.Length == 1)
        {
            //Single path
            string path = Request.MapPath(paths[0]);
            downloadFile = File.ReadAllBytes(path);
            downloadFileName = new FileInfo(path).Name;
        }
        else
        {
            //Multiple paths
            List<FileInfo> fileInfos = new List<FileInfo>();
            List<DirectoryInfo> emptyDirectoryInfos = new List<DirectoryInfo>();
 
            foreach (string relativePath in paths)
            {
                string path = Request.MapPath(relativePath);
                FileAttributes attr = File.GetAttributes(path);
                if ((attr & FileAttributes.Directory) == FileAttributes.Directory)
                {
                    DirectoryInfo di = new DirectoryInfo(path);
                    //All the files recursively within a directory
                    FileInfo[] pathFileInfos = di.GetFiles("*", SearchOption.AllDirectories);
                    if (pathFileInfos.Length > 0)
                    {
                        fileInfos.AddRange(pathFileInfos);
                    }
                    else
                    {
                        emptyDirectoryInfos.Add(di);
                    }
                    //All the folders recursively within a directory
                    DirectoryInfo[] pathDirectoryInfos = di.GetDirectories("*", SearchOption.AllDirectories);
                    foreach (DirectoryInfo pathDirectoryInfo in pathDirectoryInfos)
                    {
                        if (pathDirectoryInfo.GetFiles().Length == 0)
                        {
                            emptyDirectoryInfos.Add(pathDirectoryInfo);
                        }
                    }
                }
                else
                {
                    fileInfos.Add(new FileInfo(path));
                }
            }
 
            //Needed for constructing the directory hierarchy by the DotNetZip requirements
            string currentFolder = Request.MapPath(ctlFileExplorer.CurrentFolder);
            string zipFileName = Path.Combine(Path.GetTempPath(), String.Format("{0}.zip", new Guid()));
 
            using (Ionic.Zip.ZipFile zip = new Ionic.Zip.ZipFile(zipFileName))
            {
                foreach (FileInfo fileInfo in fileInfos)
                {
                    zip.AddFile(fileInfo.FullName, !fileInfo.Directory.FullName.Equals(currentFolder, StringComparison.InvariantCultureIgnoreCase) ? fileInfo.Directory.FullName.Substring(currentFolder.Length + 1) : String.Empty);
                }
                foreach (DirectoryInfo directoryInfo in emptyDirectoryInfos)
                {
                    zip.AddDirectoryByName(
directoryInfo.FullName.Substring(currentFolder.Length + 1));
                }
 
                zip.TempFileFolder = Path.GetTempPath();
                zip.Save();
            }
 
            downloadFile = File.ReadAllBytes(zipFileName);
            File.Delete(zipFileName);
            downloadFileName = "Combined.zip";
        }
 
        Response.Clear();
        Response.AppendHeader("Content-Disposition", String.Format("attachment; filename=\"{0}\"", downloadFileName));
        Response.ContentType = String.Format("application/{0}", downloadFileName);
        Response.BinaryWrite(downloadFile);
        Response.End();
    }
}
Rated 5.00, 4 vote(s). 
Hey There, Your solution worked great for us. Thanks for taking time to post it. One question. I am throwing an exception when I right click on a folder in the "grid" area of the telerik radfileexplorer control and then left click on Download. The solution works fine for files but throws the exception on folders. It is not necessary for me to download whole folders. I would be happy just to hide the "download" menu item when someone right clicks on a folder. The error I am getting is a security exception. I am having a heck of a time isolating the menu item through telerik's client side api. Did you run into this when you were implementing your solution. If so, how did you work around it? Any insight would be greatly appreciated.
By Edwin
Hi,
I have 1 question, I am trying to implement this, but where does Path come from?
Jasmin
By Jasmin
To Mike Gold:

Hi, sorry for not answering earlier, but I somehow missed an email notification about this. Yes, it seems that there is a flaw in my logic, I'm handling directories and their recursive content correctly but only if there are multiple items selected. In case of a single item I'm assuming it's a file and I'm not doing any recursive reading or file zipping. So, although I'm quite sure that there is a decent client-side way of hiding download option for the folder items, I haven't done it and don't have the time right now to check it. The proper solution would still be to correct the logic above with something like this:

Instead of this:

if (paths.Length == 1)
{
...

Use this:

if (paths.Length == 1 && ((File.GetAttributes(paths[0]) & FileAttributes.Directory) != FileAttributes.Directory))
{
...
Jasmin
By Jasmin
To Edwin:

if you refer to the class Path, it comes from the System.IO namespace, it's a standard .NET class.
By Edwin
Yes, I did find a property called Path, which did it fail to work, anyways thanks for your reply but its solved now
By Edwin
Hi,
I have another issue, you say multiple download, but I am only able to select 1 thing at a time, any suggestions how I can resolve this?
Jasmin
By Jasmin
I don't know how you are trying to select them, it works like in windows explorer, by clicking and holding either SHIFT or CONTROL key. I don't think there are any properties that need to be enabled, it just works like this by default. You can check it here:

http://demos.telerik.com/aspnet-ajax/fileexplorer/examples/default/defaultcs.aspx

Please understand that this is not a Telerik support forum, so I'd advise you to ask these and similar kind of questions there.
By Hessel
Hi Jasmin,
great article indeed. It is actualy possible to add files to the ZIP file using a stream / buff[]

byte[] buff = GetVarCharDataFromDatabase(someParam);
if (buff != null)
zipFile.AddEntry(FileStoreBestandnaam, buff);

Works great! Can you please be so kind to explain how I now get the data instead of the path? Must this also be done client side or is it possible to use a GetFile function from the DBContentProvider that I have overridden? Thanks in advance for your input.
By Hessel
Me again. Already found the answer. Simply recreate a DataServer object again and you're in business.
Thank you very much!
This was very helpful

Regard!