Before:
This article is a codeprject best asp.net article of March 2010, you could find all the mothly best articles here:
http://www.codeproject.com/script/Surveys/VoteForm.aspx?srvid=1026
Introduction
This article, aimed at the beginner level, describes a simple web file manager that administers the files and folders on a hosted web site where, for instance, the web host does not provide a file management utility and you do not have access to any other means of managing or administering those files and folders.
The objective is to create a small, simple application that employs as few objects as possible so that it is easy to deploy with no reliance on any other resources such as images or style sheets as these have been embedded directly into the assembly.
Caveat 1: The article does not cover the security with which you should surround this and any other utilities that you use to administer your web site.
Caveat 2: I have tested WebFiler on a number of hosted web sites that have no more than 50 folders/sub-folders and less than a thousand files and images. Whilst, in theory, it should work without a hitch on any size web site, if your web site is much larger than this it is probable that a commercial application would serve you better. Test thoroughly before final deployment and use.
Background
I'm sure that, like most people reading Asp.Net articles, you have, at one time or another, created a web site which you are expected to administer; either in terms of adding content (not covered here) or by making changes to physical files and having to upload/download them from the site as well as maintaining the folder structure. Many web hosts will offer a rudimentary file management utility for your web site but it's normally up to you to implement your own solution.
There are a number of approaches to this. You could use an FTP application such as FileZilla. You could use a free application like WebApplication Lite (though that is a classic asp application) or you could buy a commercial application like FileVista. The subject has also been covered here in the past and I leave you free to wander the hallowed halls of CodeProject to judge those articles for yourself.
The difference between WebApplication Lite and File Vista is that the latter is the more modern of the 2 (Asp.Net/Ajax v Asp Classic) with far more features including a drop in control version. However, it is not a free product. WebApplication Lite is free but is an ASP Classic application with a large footprint (many files and folders).
You have probably had to use a mixture of the web hosts own file manager utilities and/or tools like WebApplication Lite simply because it is a drop in application and takes zero maintenance. However, there are times (holiday/vacation/new client site) when you may not have reasonable access to FileZilla (or similar applications) or might not have one of the many passwords with you that you would need to access the web hosts' utility directly. WebFiler attempts to address that with a simple http solution that has a small footprint and is simple to deploy with minimal configuration.
Requirements
First and foremost the solution has to be simple with as few files and dependencies as possible; that meant embedding any images, etc. Whilst this does have a slight performance impact it is negligible: there are 5 small images and a 13 line stylesheet - these will not break the bank.
This could have been done with just one aspx page but I decided that it would be easier to maintain and understand as 2 pages: one to display the files and folders, the other to carry out simple tasks such as renaming files or uploading new files.
The application consists of 4 objects: Filer.aspx, FilerEvents.aspx, Web.config (optional) and WebFiler.dll. The only other task for users would, optionally, be to change a value in the root Web.config but more on that later.
Using the code
As mentioned above this a simple application predicated on obtaining a list of files and folders on the server and presenting them, with various options, in as simple a manner as possible.
Web.config
Firstly, let's look at the changes that are required to be made in the config file. There are 2 ways that this can be accomplished:
- Make changes to the root level web.config
- Add a new Web.config in the same folder that you put Filer.aspx and FilerEvents.aspx
I'm going to presume that you will not put these files in your root folder. You can if you want to, I can't stop you but unless you can ensure that no one will find and abuse them I would put them into a folder that already contains management utilities (Maybe like root/Admin/...) or create a new folder especially for the purpose and then protect that folder. As I said before, how you handle your security is outside the scope of this article; suffice to say that I set folder level security so that I am asked for a user name and password to access any files in that directory.
My recommendation is that you put the files into a sub folder called 'Admin'. You can either drop the supplied Web.Config in here or modify the root Web.Cofig with these 2 entries.
<?xml version="1.0"?>
<configuration>
<appSettings>
<add key="WFRoot" value=".."/>
</appSettings>
<system.web>
<httpRuntime maxRequestLength="2097151" />
</system.web>
</configuration>
WFRoot: This tells WebFiler where the root of your application is. If you leave it empty the folder where you launch Filer.aspx from will become the root and you will not be able to navigate out past it. If you only want to see, for instance, the Images folder below the root then change it to ../Images. Naturally, you will need to play around with this to give you what you want. However, if you simply create a folder called Admin or Bob or WebFiler and put the files in here then leave as is and you should have access to all of the files and folders in your application.
Note: You will not be able to see files and folders that have an attribute of Hidden and/or System but you can see and manipulate files and folders that are marked as ReadOnly.
maxRequestLength: This is the setting that restricts the size of files you can upload. The default setting is 4096. Fine if all you ever upload are small files of less than 4MB. I have set it here to 2097151 - the maximum. That is insanely high. Don't leave it at that unless you have a really, really good reason to do so and really, really need to upload 2 gig files.
Note: Visual Studio does not include this setting by default when you create a new web application so you may need to add it if you hadn't already done so.
You can also put WFRoot in the root level Web.Config and add/change the maxRequestLength value in the same way which means that you can dispense with placing the Web.config in the same folder as Filer.aspx and FilerEvents.aspx. This cuts the object load down to 3 files.
WebFiler.dll
This is the compiled code that underlies the file manager. This must be placed in the bin folder that should exist below your root folder which is where it is expected to be found. If it is not found here you will get a 'Parser Error', 'Could not load type WebFiler.Filer' - the page cannot find the compiled code behind (WebFiler.dll) and will, therefore, generate an error. It also contains all of the required images and styles thus reducing the footprint of folders and files that have to be deployed. The content is discussed below.
Filer.aspx
This page displays a table of the files and folders and allows the user to navigate between folders and instigate related functionality such as uploading a new file.
FilerEvents.aspx
This page handles a number of events on behalf of Filer.aspx and uses a MultiView control to display each of those functions.
TableEx.cs
This class contains the code to create the table of files and folders that you first see displayed. I have implemented it use plain html tables since they are simple to use and produce excellent tabular displays. I could have passed everything into, for instance, a DataTable but since a DataTable gets rendered as an html table it would have added complexity rather than simplifying the process.
The most important methid in this class is getting a list of files and folders to display. Each time you navigate to a different sub-folder or back to the root level the class is called and a new table constructed.
The listings are obtained as string arrays, one for the folders and one for the files and both are then copied to a combined array, folders first so that they appear before the files.
string[] files = Directory.GetFiles(_root);
string[] folders = Directory.GetDirectories(_root);
_data = new string[files.Length + folders.Length];
folders.CopyTo(_data, 0);
files.CopyTo(_data, folders.Length);
The table is then constructed by first building a basic table and adding a header structure that contains the various columns for display.
A 'root' row is also, if appropriate, created. In other words, if the present view isn't the topmost view (i.e. are we in the root folder?) add a row that simply allows us to navigate back up one folder. Wa also implement a 'Home' button which will return directly to the 'root' folder regardless of current folder position. Of course the 'Home' button is not rendered if we are in the root since there is no need for it. (Shown in the image above for illustrative purposes)
The next task is to create a row for each file or folder and this is accomplished in the DataRows method. Since we now have the data in '_data' (string[]) we can use a foreach to find each row and then act on whatever we find.
foreach (string item in _data)
The big decision here is are we allowed to see what we find? The easiest way to determine this is to create a FileInfo object from 'item' and examine it thusly:
// If the item attributes are Hidden or System, ignore. if ((fi.Attributes & FileAttributes.Hidden) == FileAttributes.Hidden || (fi.Attributes & FileAttributes.System) == FileAttributes.System) { continue; }
This just says if the object's attributes are System or Hidden exit the loop at this point and come back in from the top.
Next, we have to decide if we are looking at a file or a folder. Again, this is straightforward:
if (fi.Attributes == FileAttributes.Directory)
If the item is a folder (directory) then we would treat it a little differently from a file in that we simply want to navigate to the folder whereas we want to open or download a file.
Embedded Resources
This seems like a good time for a quick diversion. We've talked about embedding objects into the assembly and this is one of the places where we retrieve and use a resource for these items. In this case it is an image denoting either a file or a folder.
At the solution level images are stored within the Images folder. However, if you select an image and right-click to view the properties you will see that the 'Build Action' has been set to embedded; in other words embed a copy of this image into the compiled assembly.
At first sight this is simple enough; however, how can you actually use this image with, for instance, another object that requires a physical location for the object so as to display it? In this instance we want to use this image as the source for a System.Web.UI.WebControls.Image which needs an ImageUrl.
The answer is in 2 parts. Firstly you have to tell the application about this object. You do so through the AssemblyInfo.cs file as so:
[assembly: WebResource("WebFiler.Images.delete.png", "img/png")]
This breaks down as Solution.Location.Object, object type. For the style sheet it is very similar:
[assembly: WebResource("WebFiler.CSS.Filer.css", "text/css")]
To use this as a url you need the following:
// Get the folder image. System.Web.UI.WebControls.Image img = new System.Web.UI.WebControls.Image(); img.ImageUrl = Page.ClientScript.GetWebResourceUrl( typeof(WebFiler.Filer), "WebFiler.Images.folder.png");
You need to tell the method the type of the resource and the name of the resource.
In this instance we have passed the assembly as the type and the location of the file as it exists within the solution. The application then translates this for the image and uses WebReource.axd to supply a useable ImageUrl. Result? No need to deploy the phyical images. However, be warned, this is okay for a few images and small objects; probably not so good for large numbers of images, etc. Use your own judgement and test, test, test.
Back at row creation...
Back at row creation we would also use FileInfo to grab some other properties of the file or folder for display. Finally, we have to add a couple of buttons so that objects can be renamed or deleted. The rename simply opens FilerEvents.aspx using a querystring (more on that later) to tell the page what we want to do.
In the case of the delete event we have to be a little more careful since the method will fail when trying to delete readonly files. As we display the readonly status of files it is fairly easy to see that we probbaly shouldn't delete it otherwise why is it set to read only? This is further complicated when attempting to delete a folder and all of its sub-folders. If any object in that list is read only the process will generate an exception. Not the desired behaviour.
The way I have chosen to deal with this is to unset the readonly attribute. For files this is easy:
FileInfo fi = new FileInfo(file);
fi.Attributes = FileAttributes.Normal;
File.Delete(file);
However, it is somewhat more convoluted for folders; the idea is to create a collection of folders (and files) and reset the attributes as each is found using DirectoryInfo. At the same time, unset any files found within that folder. The method can be seen at line 504, ReadOnlyFolderDelete. The core is to reset each folder then reset all the files within that folder. Once all the folders and files have been unset call the Delete method to recursively delete all of the folders.
Request.QueryString
There are a number of different ways to pass data between forms; Session, Hidden Fields, etc; but I decided to use QueryString since this is a secure, administrator only application. I have also encoded (UrlEncode/UrlDecode) the strings that represent files and folders so as to circumvent any potential problems with strings that contain characters that could, potentially, cause problems. All this means is that, for instance, a space (character code 32) is tranlated to a plus sign (+) and an ampersand (&) to %26.
Opening Files
My first cut of this was a little 'fancier' in that I had a method that deduced the mime type and passed this as the content-type. I also had graphics that represented each one. I quickly realized that this was not the way to go. Firstly, at 16x16 it is hard to see the differences between each of the images that represent file types and went against the underlying principle of keeping it simple. It would also have meant embedding large numbers of image files into the assembly: another no-no. Most importantly, it's really not necessary. Using a content type of 'application/octet-stream' covered every case I could find (not exhaustive, by any means) which cuts down on code and resources.
This code passes the file to the Response object with the content type, creates an 'attachment' and uses Response.TransmitFile to display the file directly.
FileInfo fi = new FileInfo(file);
Response.ContentType = "application/octet-stream";
Response.AppendHeader("Content-Disposition", "attachment; filename=" + fi.Name);
Response.TransmitFile(fi.FullName);
Response.End();
FilerEvents
As mentioned above this page takes care of various general events on behalf of Filer.aspx and uses a QueryString to pass instructions and data.
Part of the query string that gets passed contains 'm' for mode. This is parsed in the OnLoad event and the appropriate view is displayed by the simple expedient of matching the value of 'm' to the index of the views.
MultiView.ActiveViewIndex = m; // mode
The events are driven by buttons (see main image above) on Filer.aspx as follows:
Rename
In fact this is a table event (in that it is invoked for any row in the table of files and folders) but I prefer to display a separate page to handle this event as it is easier to maintain than crowding seemingly unrelated functionality onto one page and is more closely related to the other non-table based functions. Partial classes might have worked in this case but I wanted to keep the rename controls away from the main page.
New... File, Folder or Upload
Trivial functionality each segment of which has a small set of controls displayed and each of these is similar in appearance to the rename box displayed above.
References
There are no other references required for this project; I have striven to ensure that only objects that come packaged with VS 2008 are used so that there are no other external dependencies required.
Note: The aplication was built using the .Net 3.5 framework in VS 2008. Viewing and building WebFiler in other version of Visual Studio is beyond the scope of this article.
Deployment
Deployment is simply a case of creating a new folder or using an existing one and dropping Filer.aspx and FilerEvents.aspx into that folder. Place the WebFiler.dll into the root bin folder and make the required changes to Web.config. Open a browser and navigate to Filer.aspx. All of the web site's files and folders should be visible.
Warning
Use entirely at your own risk: you must take the responsibility of use and should exercise extreme caution when deploying applications or utilities or files that have the ability to alter or change other files and folders. You have been warned!
Conclusion
There are, of course, other ways that this could have been approached, any or all of which will have their own merits and drawbacks. I chose to do it this way because it was simple and simple things work pretty well. Even if you like this you should take some time to evaluate other people's approaches as they may suit your particular needs in a better way. If you do like it then please use it. Any reasonable and constructive suggestions to improve this product and/or article are always welcome.