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();
}
}