Showing posts with label folder. Show all posts
Showing posts with label folder. Show all posts

Using MsDeploy publish profile .pubxml to create an empty folder structure on IIS and skip deleting it with MsDeploySkipRules

I’m going to share here a pretty nice deployment automation solution for when you need to have a defined set of folders in place when first deploying an app to IIS. On subsequent re-deployments the folder structure will be kept intact with any files users may have added to them.

Let’s work with a simple example: given an ASP.NET MVC app, we need a folder called Files and inside this folder there will be some pre-defined folders named: Folder 1, Folder 2 and Folder 3 with a child folder called Test. Something like this:

App root
|
---Files
   |
   ---Folder 1
   ---Folder 2
   ---Folder 3
      |
      ---Test

When deploying for the 1st time these folders are empty but the folder structure is mandatory let’s say because I’m using a file manager like elFinder.Net that expects that these folders exist on the server. Why? Because the ASP.NET MVC app has links pointing to these folders in some views. The folders should be ready to store files when the app is released. What also comes to my mind is the case where we need an existing Downloads/Uploads folder.

What’s more? We also want all this to happen while using Publish Web command from within Visual Studio and still keeping the option Remove additional files at destination checked:

Figure 1 - Visual Studio Publish Web with File Publish Options => Remove additional files at destinationFigure 1 - Visual Studio Publish Web with File Publish Options => Remove additional files at destination

This setting is nice because when you update jQuery NuGet package for example (jquery-2.1.1.js) it will send the new files to IIS server and will remove the old version (jquery-2.1.0.js) that exists there. This is really important so that the app keeps working as expected and don’t load the wrong version/duplicate files. If we don’t check that option we have to go to the server and delete the old files manually. What a cumbersome and error prone task!

What to do in this case where we want the deployment task do the work “automagically” for us with no human intervention? It’s seems like a lot of requirements and a task not so simple as “it appears to be”… Yep, it requires a little bit of MsDeploy codez.

Here’s what is working for me at the moment after finding some code pieces from here and there:

Given a  publish profile named Local.pubxml that sits here:
C:\Company\Company.ProjectName\Company.ProjectName.Web\Properties\
PublishProfiles\Local.pubxml

Let’s add the code blocks necessary to make all the requirements come to life:

<?xml version="1.0" encoding="utf-8"?>
<!--
This file is used by the publish/package process of your Web project. You can customize the behavior of this process
by editing this MSBuild file. In order to learn more about this please visit http://go.microsoft.com/fwlink/?LinkID=208121. 
-->
<Project ToolsVersion="4.0" xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
  <PropertyGroup>
<AfterAddIisSettingAndFileContentsToSourceManifest>AddCustomSkipRules</AfterAddIisSettingAndFileContentsToSourceManifest>
    <WebPublishMethod>MSDeploy</WebPublishMethod>
    <LastUsedBuildConfiguration>Local</LastUsedBuildConfiguration>
    <LastUsedPlatform>Any CPU</LastUsedPlatform>
    <SiteUrlToLaunchAfterPublish />
    <ExcludeApp_Data>False</ExcludeApp_Data>
    <MSDeployServiceURL>localhost</MSDeployServiceURL>
    <DeployIisAppPath>SuperCoolAwesomeAppName</DeployIisAppPath>
    <RemoteSitePhysicalPath />
    <SkipExtraFilesOnServer>False</SkipExtraFilesOnServer>
<MSDeployPublishMethod>WMSVC</MSDeployPublishMethod> <AllowUntrustedCertificate>True</AllowUntrustedCertificate>
<EnableMSDeployBackup>False</EnableMSDeployBackup> <UserName /> <_SavePWD>False</_SavePWD> <LaunchSiteAfterPublish>True</LaunchSiteAfterPublish> </PropertyGroup> <PropertyGroup> <UseMsDeployExe>true</UseMsDeployExe> </PropertyGroup> <Target Name="CreateEmptyFolders"> <Message Text="Adding empty folders to Files" /> <MakeDir Directories="$(_MSDeployDirPath_FullPath)\Files\Folder 1" /> <MakeDir Directories="$(_MSDeployDirPath_FullPath)\Files\Folder 2" /> <MakeDir Directories="$(_MSDeployDirPath_FullPath)\Files\Folder 3\Test"/> </Target> <Target Name="AddCustomSkipRules" DependsOnTargets="CreateEmptyFolders"> <Message Text="Adding Custom Skip Rules" /> <ItemGroup>
      <MsDeploySkipRules Include="SkipFilesInFilesFolder">
        <SkipAction>Delete</SkipAction>
        <ObjectName>filePath</ObjectName>
        <AbsolutePath>$(_DestinationContentPath)\\Files\\.*</AbsolutePath>
        <Apply>Destination</Apply>
      </MsDeploySkipRules>

      <MsDeploySkipRules Include="SkipFoldersInFilesFolders">
        <SkipAction></SkipAction>
        <ObjectName>dirPath</ObjectName>
        <AbsolutePath>$(_DestinationContentPath)\\Files\\.*\\*</AbsolutePath>
        <Apply>Destination</Apply>
      </MsDeploySkipRules>

</ItemGroup>
</
Target> </Project>

This is self explanatory. Pay attention to the highlighted parts as they are the glue that make all the requirements happen during the publish action.

What is going on?

The property

<AfterAddIisSettingAndFileContentsToSourceManifest>
AddCustomSkipRules
</AfterAddIisSettingAndFileContentsToSourceManifest>

calls the target

<Target Name="AddCustomSkipRules"
DependsOnTargets="CreateEmptyFolders">
that in turn depends on the other task
<Target Name="CreateEmptyFolders">

CreateEmptyFolders take care of adding/creating the folder structure on the server if it doen’t exist yet.

AddCustomSkipRules contains two  <MsDeploySkipRules...>. One is to prevent deleting Files and the other prevents deleting the child folders.

Check the targets’ logic. They’re pretty easy to understand…

Note: make sure you don’t forget the

<UseMsDeployExe>true</UseMsDeployExe>

otherwise you may see this error during deployment:

Error    4    Web deployment task failed. (Unrecognized skip directive 'skipaction'. Must be one of the following: "objectName," "keyAttribute," "absolutePath," "xPath," "attributes.<name>.")        0    0    Company.ProjectName.Web

Simple as pie after we see it working. Isn’t it? Winking smile

Hope it helps!

As an interesting point, see the command executed by msdeploy.exe that gets written to the output window in Visual Studio:

C:\Program Files (x86)\IIS\Microsoft Web Deploy V3\msdeploy.exe -source:manifest='C:\Company\Company.ProjectName\Company.ProjectName.Web\obj\
Local\Package\Company.ProjectName.Web.SourceManifest.xml' -dest:auto,IncludeAcls='False',AuthType='NTLM' -verb:sync -disableLink:AppPoolExtension -disableLink:ContentExtension -disableLink:CertificateExtension -
skip:skipaction='Delete',objectname='filePath',absolutepath='\\Files\\.*' -skip:objectname='dirPath',absolutepath='\\Files\\.*\\*' -setParamFile:"C:\Company\Company.ProjectName\Company.ProjectName.Web\obj\Local\ Package\Company.ProjectName.Web.Parameters.xml" -retryAttempts=2 -userAgent="VS12.0:PublishDialog:WTE2.3.50425.0"

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.