Manage folders & files in your ASP.NET MVC app with elFinder.Net



Recently I had to evaluate what were my options when it comes to managing folders and files in an ASP.NET MVC project – a files manager somewhat like what a Content Management System does but I needed something way simpler and intuitive and principally of easy integration.

The Story
As part of a project requirement, a super user wants to able to offer other users some files like PDFs, Images, Videos, etc. This super user should be capable of creating a folder structure as he sees fit and then upload files to a central point (the server). All this looks like a difficult task to accomplish through a client server architecture since we’re dealing with clients and their files that need to go to the server. At first I thought about using a SharePoint module/web part but then it seemed overkill for what I wanted.

As always I just put some simple words in a Google search and to my surprise I found an outstanding open source project called elFinder by Studio 42. Here’s their ASCII art for your amusement:

      _ ______ _           _           
     | |  ____(_)         | |          
  ___| | |__   _ _ __   __| | ___ _ __ 
 / _ \ |  __| | | '_ \ / _` |/ _ \ '__|
|  __/ | |    | | | | | (_| |  __/ |   
 \___|_|_|    |_|_| |_|\__,_|\___|_|   

elFinder is an open-source file manager for web, written in JavaScript using jQuery UI. Creation is inspired by simplicity and convenience of Finder program used in Mac OS X operating system.

After going through the readme file for a moment, I thought: Oh my god, this is just what I need. The only drawback in my case was that the connector (server part) of elFinder is written in PHP and I’m developing an ASP.NET MVC app. Then let me go to Google once again and type elFinder ASP NET. To my surprise there are some ports to the .NET arena but they seem to lack integration simplicity, that is, the dependencies these ports need just interfere with my current code as was the case of the elFinder ASP.NET Connector that uses Autofac as a dependency. I use Castle Windsor for the IoC part and then this was a no go. A more careful read at the original elFinder wiki let me know about different ports of this amazing project: 3rd party connectors, plugins, modules. This page pointed me to ElFinder.NET connector for elFinder 2.x hosted at CodePlex.

I downloaded ElFinder.NET’s source code that comes with a sample ASP.NET MVC app and played with it. When I saw the amount of features available [ Client options + Connector options ] I almost cried in excitement Crying face. ElFinder.NET is a very well written and organized port put together by Evgeny Noskov. Congrats to him and to Studio 42 from Russia for sharing the code with the community. It works great and so I started integrating it with my ASP.NET MVC 4 app.

Along the way I hit some missing features that I saw are part of the original elFinder by Studio 42. They are startPath and uploadMaxSize. I contacted Evgeny through the CodePlex contact form and asked him about the option to set a start path in the root directory. Thinking he wouldn’t even answer me, I decided to implement it myself looking at the original elFinder codebase. Today I got an e-mail from Evgeny telling me that he just added the start path option to elFinder.Net. What a joy! Then I wanted to set a max upload size and I just saw that this option was also not present in elFinder.Net. Asked about it in this discussion and Evgeny promptly added it to the library. I even had no time to try to implement this one… he went even further and added a must have feature that I didn’t notice was missing: file download.

So after all this amazing story of “is giving that you receive” I decided to spread the word and write a blog post about elFinder.net…

It’s enough of background info. Let me show you one simple use case and how you can take advantage of such outstanding open source project.

The Use Case
Let’s say you want to let one super user with read/write permissions create a folder structure inside a given root directory in the server and upload files there. Other users accessing the app will then be presented with a page that has links that point to the 1st level folders of that root directory. These users will be able to only read those folders/files uploaded by the super user. Note: this app has some kind of membership implemented with roles and permissions granted to users. I’ll omit this part in this sample code for simplicity sake.

The super user has access to a screen (Files menu option) like the following one that contains elFinder files manager:

Figure 1 - elFinder.Net file manager UI styled with jQuery UI supporting any folder depth, file/folder upload/download, delete, rename, copy/cut/paste, preview, properties, drag and drop, etc.Figure 1 - elFinder.Net file manager UI styled with jQuery UI supporting any folder depth, file/folder upload/download, delete, rename, copy/cut/paste, preview, properties, drag and drop, etc.

Other users when logging in for example will have access to a page (Home menu option) listing the 1st level folders like this:

Figure 2 - 1st level folders links allow users to click on them and have the selected folder opened in elFinder’s file manager automagicallyFigure 2 - 1st level folders links allow users to click on them and have the selected folder opened in elFinder’s file manager automagically

When clicking the folder name link, the user will be sent to elFinder’s file manager and the folder will be selected automatically showing its content to the user. The user without super powers then is allowed only to read the folder content and if they want they can even download the files to their machines. The possibilities are endless…

The Code
The full code is available at this GitHub repo: https://github.com/leniel/elFinder.Net

You’ll find comments throughout the code. Make sure you read them carefully.

Let’s start defining two action methods in the HomeController.

public partial class HomeController : Controller
{
    [GET("")]
    public virtual ActionResult Index()
    {
        DirectoryInfo di = new DirectoryInfo(Server.MapPath("~/Files/MyFolder"));
        // Enumerating all 1st level directories of a given root folder (MyFolder in this case) and retrieving the folders names.
        var folders = di.GetDirectories().ToList().Select(d => d.Name);

        return View(folders);
    }

    [GET("FileManager/{subFolder?}")]
    public virtual ActionResult Files(string subFolder)
    {
        // FileViewModel contains the root MyFolder and the selected subfolder if any
        FileViewModel model = new FileViewModel() { Folder = "MyFolder", SubFolder = subFolder };

        return View(model);
    }
}

The Index action method corresponding View has this code:

@model IEnumerable<string>

@{
    ViewBag.Title = "Index";
}

Available 1st level Folders - clicking will navigate you to the File Manager setting the selected folder as elFinder's start path.

<ul>
    @foreach (string folder in Model)
    {
        <li><a href="@Url.Action(MVC.Home.ActionNames.Files, MVC.Home.Name, new { subFolder = folder })">@folder</a> </li>
    }
</ul>

The Files action method corresponding View has this code:

@model FileViewModel

@{
    ViewBag.Title = "Files";
}

@Html.Partial(MVC.Shared.Views.FilesForm, Model)

The FilesForm partial view has elFinder’s client side interesting pieces of code:

@model FileViewModel

@{
    ViewBag.Title = "Files";
}

@* Bundles with elFinder's CSS and JavaScript files configured in App_Start\BundleConfig.cs*@
@Styles.Render("~/Content/elfinder")
@Scripts.Render("~/Scripts/elfinder")

<script type="text/javascript">
    $(function ()
    {
        var myCommands = elFinder.prototype._options.commands;

        var disabled = ['extract', 'archive', 'resize', 'help', 'select']; // Not yet implemented commands in ElFinder.Net

        $.each(disabled, function (i, cmd)
        {
            (idx = $.inArray(cmd, myCommands)) !== -1 && myCommands.splice(idx, 1);
        });

        var selectedFile = null;

        var options = {
            url: '/connector', // connector route defined in the project folder App_Start\RouteConfig.cs
            customData : { folder : '@Model.Folder', subFolder: '@Model.SubFolder' }, // customData passed in every request to the connector as query strings. These values are used in FileController's Index method.
            rememberLastDir: false, // Prevent elFinder saving in the Browser LocalStorage the last visited directory
            commands: myCommands,
            //lang: 'pt_BR', // elFinder supports UI and messages localization. Check the folder Content\elfinder\js\i18n for all available languages. Be sure to include the corresponding .js file(s) in the JavaScript bundle.
            uiOptions: { // UI buttons available to the user
                toolbar: [
                    ['back', 'forward'],
                    ['reload'],
                    ['home', 'up'],
                    ['mkdir', 'mkfile', 'upload'],
                    ['open', 'download'],
                    ['info'],
                    ['quicklook'],
                    ['copy', 'cut', 'paste'],
                    ['rm'],
                    ['duplicate', 'rename', 'edit'],
                    ['view', 'sort']
                ]
            },

            handlers: {
                select: function (event, elfinderInstance) {

                    if (event.data.selected.length == 1) {
                        var item = $('#' + event.data.selected[0]);
                        if (!item.hasClass('directory')) {
                            selectedFile = event.data.selected[0];
                            $('#elfinder-selectFile').show();
                            return;
                        }
                    }
                    $('#elfinder-selectFile').hide();
                    selectedFile = null;
                }
            }
        };
        $('#elfinder').elfinder(options).elfinder('instance');

        $('.elfinder-toolbar:first').append('<div class="ui-widget-content ui-corner-all elfinder-buttonset" id="elfinder-selectFile" style="display:none; float:right;">'+
        '<div class="ui-state-default elfinder-button" title="Select" style="width: 100px;"></div>');
        $('#elfinder-selectFile').click(function () {
            if (selectedFile != null)
                $.post('file/selectFile', { target: selectedFile }, function (response) {
                    alert(response);
                });
               
        });
    });
</script>

<div id="elfinder"></div>

The last part is the FileController that gets called in every connector request:

public partial class FileController : Controller
{
    public virtual ActionResult Index(string folder, string subFolder)
    {
        FileSystemDriver driver = new FileSystemDriver();

        var root = new Root(
                new DirectoryInfo(Server.MapPath("~/Files/" + folder)),
                "http://" + Request.Url.Authority + "/Files/" + folder)
        {
// Sample using ASP.NET built in Membership functionality... // Only the super user can READ (download files) & WRITE (create folders/files/upload files). // Other users can only READ (download files) // IsReadOnly = !User.IsInRole(AccountController.SuperUser)
            IsReadOnly = false,
Alias = "Files", // Beautiful name given to the root/home folder MaxUploadSizeInKb = 500 // Limit imposed to user uploaded file <= 500 KB }; // Was a subfolder selected in Home Index page? if (!string.IsNullOrEmpty(subFolder)) { root.StartPath = new DirectoryInfo(Server.MapPath("~/Files/" + folder + "/" + subFolder)); } driver.AddRoot(root); var connector = new Connector(driver); return connector.Process(this.HttpContext.Request); } public virtual ActionResult SelectFile(string target) { FileSystemDriver driver = new FileSystemDriver(); driver.AddRoot( new Root( new DirectoryInfo(Server.MapPath("~/Files")), "http://" + Request.Url.Authority + "/Files") { IsReadOnly = false }); var connector = new Connector(driver); return Json(connector.GetFileByHash(target).FullName); } }

Hope it helps.