Creating a Class 1 WebDAV Server

Class 1 WebDAV Server provides basic file management features available in every WebDAV server. With a Class 1 server, you can create files, copy, move and delete files and folders as well as create, read and delete custom properties on each file or folder. Note that many WebDAV clients, such as Microsoft Office applications, Microsoft Web Folders/mini redirector client, Mac OS X WebDAV client and OpenOffice require Class 2 server. Class 1 server does not protect files from concurrent modifications, so many clients treat Class 1 as read-only, making it unusable for real world applications. We will explain how to extend Class 1 server with Class 2 features in this article.

Creating Basic WebDAV Server Using Wizard.

Here we will use the 'ASP.NET WebDAV Server Application' Wizard to create a Class 1 WebDAV server. Go to File -> New Project -> Visual C# -> Web menu and start the 'ASP.NET WebDAV Server Application' wizard. On the Storage Type step, select 'Store File and metadata in file system' and live the default path in File storage location field.

Select Store File and metadata in file system and live the default path in File storage location field

While the wizard provides two options - file system and MS SQL Database, you can program the engine to store data in virtually any place, for example, in CMS/DMS/CRM or in any non-relational database. You can keep parts of your data in different places, for example, tree structure in database, while file content in file system; or you can integrate several data sources to be displayed under a single root, there are countless number of implementations.

On the Standard Features step clear the Class 2 checkbox, leaving only Class 1, that is always grayed, as soon as Class 1 is the minimum functionality supported by any WebDAV server.

Class 1 checkbox is always grayed and other checkboxes are clear

On the Extended Features step clear all checkboxes except 'Create Custom Handler for GET verb'.

clear all checkboxes except Create Custom Handler for GET verb

Leave the anonymous authentication selected in the Authentication step.

Anonymous authentication is selected

Below is the Solution Explorer with your project (you must check the 'Show All Files' button on the Solution Explorer toolbar to see the App_Data folder):

The Solution Explorer with your project

There is only one aspx file in the project that displays 'Your server is running' page with several options when you run the project. 

The files published via WebDAV are placed to \App_Data\WebDAV\Storage\ folder. The wizard have populated this folder with sample files.

The Wizard have created the folder to keep WebDAV logs: \App_Data\WebDAV\Logs\. When you run the project the engine creates a log file called WebDAVlog.txt in \App_Data\WebDAV\Logs\. If you experience any issues with your server the first thing to do is to examine this log file. You may find exceptions logged in it that may give you the idea about the issue.

Important! Never create logs in your \bin folder in Web Application or MVC project. As soon as the log file is updated your application will restart.

However, you still can create logs in the \bin folder for HttpListener-based projects.

The path to \App_Data\WebDAV\Storage\ and to \App_Data\WebDAV\Logs\ folders are specified in RepositoryPath and LogPath application settings in web.config. If you decide to change the path to these folders make sure your web application has enough permissions to modify files in the new location.

Core Classes in Your Project

As we mentioned earlier, here we can compile and run the application. However, as soon as most WebDAV clients require Class 2 server we will only examine the code generated by the wizard. Let's create a class diagram and look what classes were generated. Here are the core classes of your project:

The diagram of the core classes of your project

There are 2 types of items in a basic WebDAV repository: folders, represented by IFolderAsync interface and files represented by IFileAsync interface. Both IFolderAsync and IFileAsync interfaces are derived from IHierarchyItemAsync interface. Files and folders are generated by the factory method called GetHierarchyItemAsync that you must override in your class derived from DavContextBaseAsync. Below is a simplified GetHierarchyItemAsync implementation provided for demo purposes (the real code generated by the wizard is more complicated and provides more logic for URL decoding):

public class DavContext : DavContextBase
{
    private readonly string repositoryPath;
    ...
 
    public DavContext(HttpContext httpContext, string repositoryPath) 
        : base(httpContext)
    {
        this.repositoryPath = repositoryPath;
        ...
    }
 
    public override IHierarchyItem GetHierarchyItem(string path)
    {
        string fullPath = repositoryPath + path.Replace('/', '\\');
 
        DirectoryInfo directory = new DirectoryInfo(fullPath);
        if (directory.Exists)
            return new DavFolder(directory); // implements IFolder
 
        FileInfo file = new FileInfo(fullPath);
        if (file.Exists)
            return new DavFile(file); // implements IFile
 
        return null; // neither file nor folder was found in the repository        
    }
}

The DavContext class holds the execution context that is specific to each WebDAV request. The DavContextBaseAsync provides 3 overloaded constructors, two of which are optimized for use in IIS/ASP.NET-based server and in the HttpListener-based server. The third constructor is provided to support any other hosting environment.

You will create a separate context class instance in each request and pass it to the DavEngineAsync.RunAsync. The engine will parse WebDAV XML request and generate the response.

In the case of ASP.NET based WebDAV server, all WebDAV requests are processed by HTTP handler. The wizard has created the DavHandler class for this purpose, that implements IHttpHandler (again, for demo purposes, this code is simplified competing to what wizard generates):

public class DavHandler : IHttpHandler
{
    ...
    public void ProcessRequest(HttpContext context)
    {         context.Response.BufferOutput = false;           DavEngine engine = new DavEngine();         engine.License = @"<?xml version=""1.0""...         var davContext = new DavContext(context, "C:\\MyStorage");         engine.Run(davContext);     } }

The DavEngineAsync.RunAsync method is thread safe. While the above example creates a separate engine instance for each request, you are free to create a single engine instance, save it in application state and reuse in each request. However, note that you must create a separate instance of your DAV context class in each request. 

In your web.config file, you will find 2 entries for DavHandler class. The first one is used if your server runs in IIS Classic mode and the second one is used in IIS Integrated mode:

<configuration>  
  
      <system.web>        
        <httpHandlers>       
          <clear />      
          <add verb="*" path="*" type="WebDavApplication1.DavHandler, WebDavApplication1" />     
        </httpHandlers>   
       </system.web>      

       <system.webServer>     
         <handlers>      
           <clear />      
           <add name="My WebDAV Handler" path="*" verb="*" type="WebDavApplication1.DavHandler, WebDavApplication1" preCondition="integratedMode" />    
          </handlers>  
        </system.webServer>
 </configuration>

Below we will summarize how your application works. Here is the typical workflow:

1. Create an instance DavEngineAsync class.
2. Set the license string using DavEngineAsync.License property.
3. In each HTTP request:
     a. Create an instance of your DAV context class derived from DavContextBaseAsync.
     b. Pass your DAV context class to DavEngineAsync.RunAsync method.
     c. Engine calls DavContextBaseAsync.GetHierarchyItemAsync.
     d. Engine calls members of interfaces IFileAsyncIFolderAsync, etc.

IT Hit WebDAV Storage Interfaces

Now let's see how the storage is represented in IT Hit WebDAV Server library. The storage is the place where you keep files tree structure, files content, custom properties, etc.

The IFileAsync interface, in addition to IHierarchyItemAsync, implements IContentAsync interface that provides methods for managing file content. The class diagram below shows these classes:

The class diagram for .Net Interfaces

As you can see the IHierarchyItemAsync is inherited by IFolderAsync indirectly, IFolderAsync inherits IItemCollectionAsync that adds GetChildernAsync method for enumerating folder content. In your code, you must implement IFolderAsync and IFileAsync interfaces and create your class derived from DavContextBaseAsync, that implements DavContextBaseAsync.GetHierarchyItemAsync method.

IHierarchyItemAsync

IHierarchyItemAsync interface contains properties and methods that are common for all WebDAV items. While all properties are self-explanatory here, let's look at specifics of their implementation.

Each item has its creation and modification date/time that your implementation must return in UTC. In the case of a file, the modification date must change only when the content of the file changes. It must not change when the item is locked or unlocked or properties modified. In particular Mac OS Finder relies on such behavior.

The path that you return from your Path property implementation should be relative to WebDAV root. For example, if your WebDAV server root is located at ‘http://webdavserver.com/myserver/’ and the item URL is ‘http://webdavserver.com/myserver/myfolder/myitem.doc’ the Path property implementation must return ‘myfolder/myitem.doc’. To calculate the entire item URL, the framework will call DavRequest.ApplicationPath property and concatenate it with URL returned by Path property.

The Name property is ignored by Microsoft Web Folders client. To display files and folders names, it is using the last segment of the item URL, returned by the IHierarchyItemAsync.Path property. When you are renaming an item in a WebDAV client, the IHierarchyItemAsync.MoveToAsync method is called, there is no any separate method for renaming the item or changing the name. To avoid any inconsistency and achieve the same behavior within all WebDAV clients we recommend the Name property to be identical to the last segment of the URL. If you store this property separately from the URL, when the item is renamed using IHierarchyItemAsync.MoveToAsync all, you must update this property.

Your implementation of CopyToAsync, MoveToAsync and DeleteAsync methods can update more than a single item in your repository. This usually happens when processing folder items. If an operation with some items failed, but you want to continue the operation, you can report the error to the client using multistate parameter passed to each of this methods. You can add error to the list calling MultistatusException.AddInnerException method. Note that while the engine will process all inner exceptions in MultistatusException and generate a WebDAV-compliant response with an error description for each failed item, many WebDAV clients, including Microsoft Mini-redirector client just ignores the response.

IContentAsync

IContentAsync interface provides methods and properties for reading and saving file content in your repository. We will start with ContentType property, as is vital for web browser-based clients. Most WebDAV clients, including Web Folders/mini redirector, do not submit Content-Type header when uploading a file, so the ContentType parameter of the IContentAsync.WriteAsync method will be null in most cases. Nevertheless, you still must provide the correct Content-Type / mime-type when it is requested by the WebDAV client. Browsers, such as Firefox, rely on the mime-type returned in Content-Type header to determine what action to take when the user clicks on file hyperlink. Depending on the mime-type provided by the server, the browser can either load file content in a browser window or can display File Open / File Save dialog. To get the file mime-type, the IT Hit WebDAV Server Engine for .Net provides MimeType class that returns mime-type by extension:

public virtual string ContentType
{
    get { return MimeType.GetMimeType(fileSystemInfo.Extension) ?? "application/octet-stream"; }
}

When WebDAV client is uploading a file to the server, the IFileAsync.WriteAsync method is called. Most WebDAV clients including Microsoft Web Folders/mini-redirector, Mac OS X Finder, Microsoft Office and OpenOffice submit an entire file to the server in one piece. However, some WebDAV clients may want to update only a part of a file, submitting a file segment. To submit a file segment, the client application must attach the Content-Range: bytes XXX-XXX/XXX header to PUT request. If no Content-Range header is found the IT Hit WebDAV Server Engine assumes an entire file is submitted. The IFileAsync.WriteAsync has parameters that provide information to implementers about position of the submitted segment inside content and total file size:

public bool Write(Stream content, string contentType, long startIndex, long totalFileSize)
{
    //Set timeout to maximum value to be able to upload large files.
    HttpContext.Current.Server.ScriptTimeout = int.MaxValue;
 

    return context.FileOperation(
        this,
         () => writeInternal(content, startIndex, totalFileSize),
        Privilege.Write);
}
        
private bool writeInternal(Stream content, long startIndex, long totalLength)
{
    if (startIndex == 0 && fileInfo.Length > 0)
    {
        SafeNativeMethods.TruncateFile(fileInfo);
    }
 
    RewriteStream("SerialNumber", GetStreamAndDeserialize<int>("SerialNumber", context.Logger) + 1); // Update ETag
 
    using (var fileStream = fileInfo.Open(FileMode.OpenOrCreate, FileAccess.Write, FileShare.Read))
    {
        if (fileStream.Length < startIndex)
        {
            throw new DavException("Previous piece of file was not uploaded.", DavStatus.PRECONDITION_FAILED);
        }
 
        fileStream.Seek(startIndex, SeekOrigin.Begin);
        var buffer = new byte[bufSize];
              
        int lastBytesRead;
        while ((lastBytesRead = content.Read(buffer, 0, bufSize)) > 0)
        {
            fileStream.Write(buffer, 0, lastBytesRead);
            fileStream.Flush();
        }
    }
 
    return true;
}

The engine also supports file upload via multipart-encoded form using POST verb. In the case of POST upload, the IFileAsync.WriteAsync also called, there is no any separate method required to implement for its processing.

Some WebDAV clients such as Mac OS X Finder upload content using chunked upload. There is no way to detect total content length in this case, and totalFileSize will be -1, in this case. As a general rule, we do not recommend relying on the totalFileSize parameter. For example, if a file over 2Gb is uploaded to IIS hosted server this parameter will be -1 as well.

For getting file content, the IFileAsync interface provides ReadAsync method. The WebDAV client application can request either entire file or only a file segment. Unlike segmented upload, that is used rarely, the segmented download is often used by download managers. The client must attach Range header to indicate what file segment to download. Here is the example of IFileAsync.ReadAsync implementation:

public void Read(Stream output, long startIndex, long count)
{
    //Set timeout to maximum value to be able to download large files.
    HttpContext.Current.Server.ScriptTimeout = int.MaxValue;
 
    context.FileOperation(
        this,
        () => readInternal(output, startIndex, count),
        Privilege.Read);
}
 
private void readInternal(Stream output, long startIndex, long count)
{
    var buffer = new byte[bufSize];
    using (var fileStream = fileInfo.Open(FileMode.Open, FileAccess.Read, FileShare.Read))
    {
        context.Response.ContentLength = fileStream.Length;
        fileStream.Seek(startIndex, SeekOrigin.Begin);
        int bytesRead;
        var toRead = (int)(count > bufSize ? bufSize : count);
        while ((bytesRead = fileStream.Read(buffer, 0, toRead)) > 0)
        {
            output.Write(buffer, 0, bytesRead);
            count -= bytesRead;       
        }
    }
 }

If your server is hosted in IIS/ASP.NET, to avoid content buffering on the server side make sure you set

HttpContext.Current.Response.BufferOutput = false;

To prevent script timeout increase it before file content writing or reading: 

HttpContext.Current.Server.ScriptTimeout = 2400; // timeout in seconds.

The IFileAsync.ContentLength must provide a length of the file. The value returned by this method is used in Content-Length header sent to a client as well as may be used by your hosting environment and engine. Sometimes you may need to encrypt content in your storage or somehow else modify it so that the actual content size on your storage changes. Make sure you correctly report the content, it must exactly specify amount of bytes sent to the client by your IFileAsync.ReadAsync method implementation. If it is larger than the actual bytes sent, the request may hang, waiting for more bytes to be written by your code into the stream.

ETag property is the unique value that changes each time when you update file content. It is sent with GET request in the header and used by the client applications to determine if a file was changed since last read. Usually in your IFileAsync.WriteAsync method implementation you must update ETag and file modification date.

IFileAsync

The IFileAsync interface represents a file in a repository. This is a marker interface derived from IContentAsync and IHierarchyItemAsync, it does not add any additional properties or methods.

IItemCollectionAsync

The IItemCollectionAsync interface extends IHierarchyItemAsync interface and is implemented on container items, such as folders. It provides a single GetChildernAsync method, that is in the case of a folder, returns direct children of the folder:

public virtual IEnumerable<IHierarchyItem> GetChildren(IList<PropertyName> propNames)
{
    //Enumerate all child files and folders.
    FileSystemInfo[] fileInfos =
        context.FileOperation(this,
            () => dirInfo.GetFileSystemInfos(),
            Privilege.Read);
 
    // return DavFile or DavFolder for every child item.
    foreach (FileSystemInfo item in fileInfos)
    {
        var childPath = Path + EncodeUtil.EncodeUrlPart(item.Name);
        if (item is DirectoryInfo)
        {
            yield return new DavFolder((DirectoryInfo) item, context, childPath + "/");
        }
        else
        {
            yield return new DavFile((FileInfo) item, context, childPath);
        }
    }
}

When a user browses folders with WebDAV client the IItemCollectionAsync.GetChildernAsync on folder items is always called. In this method, you will often filter items returned, depending on user permissions.

The propNames input parameter is provided solely for performance optimization purposes. It gives you a hint what properties were requested by the client application, so you can request all required properties for child items in a single call to your storage. Later engine will call IFolderAsync and IFileAsync methods and properties depending on the properties that were requested by the client.

IFolderAsync

The IFolderAsync interface is derived from IItemCollectionAsync. It adds 2 methods: CreateFileAsync and CreateFolderAsync, that are called when a file or a folder should be created. From CreateFileAsync call, you must return the object implementing IFileAsync interface. Depending on the operation that WebDAV client requested, the engine may call IContentAsync.Write to save file content, or call other file item methods.