Click here to Skip to main content
65,938 articles
CodeProject is changing. Read more.
Articles / web / ASP.NET

Updating IIS Web Server Files Currently Being Downloaded

4.93/5 (7 votes)
5 Apr 2010CPOL8 min read 1   268  
Introducing a method to replace locked binary files on a web server that are currently being downloaded.

Introduction

Currently, we do have two websites where we offer and sell our own applications (a CMS [1] and a Test Case Management tool [2]). These applications are downloaded often and constantly throughout the whole day and night.

When a file is being downloaded, I discovered that it seems to be locked by IIS, making it impossible to update with a newer version.

In this article, I will show you a solution that I came up with, that enables you to seamlessly replace files on the server, even if they are currently being downloaded.

Basic Overview

How Updating Worked in the Past

When we updated the setup for our applications, the usual way until recently was:

  1. Run a batch upload script to upload a new setup executable.
  2. If I had luck, no one is currently downloading the file. Then I was done here.
  3. If the file is being downloaded, however, I had first to call IISRESET on the web server (resulting in the user having a broken download and all other visitors, websites and sessions to drop).
  4. Then, I had to run my batch upload script again, hoping that no one started a download again.

This process has huge drawbacks, since it resets all websites on the webserver, not just the one with the download. All user's sessions are being reset and all current downloads are stopped.

Since we recently switched from IIS6 to IIS7, I thought that IIS7 is more intelligent and does some internal file/lock management to help me on this, but I still faced the same behavior on IIS7.

How Updating Works Now

So I wanted to solve the issue. Doing lots of Google search and reading different articles on Stack Overflow and talking to fellow developers, I still had no solution to this issue.

Therefore I came up with a solution that basically works like this:

  1. Upload files to a central download folder on the web server that is not directly accessible as a valid public URL.
  2. When uploading, store it on the server with a unique name, by adding a GUID suffix. E.g. "mysetup.exe" would be named "mysetup.exe.86fbeac6-6281-4a33-bfd3-cd789f183d08". The unique name is different for each consequent upload.
  3. Add a rewrite rule to the "web.config" file to map the public download URL to an ASP.NET Generic Handler (".ashx"). So e.g. "http://www.myserver.com/mysetup.exe" would be rewritten to "http://www.myserver.com/DownloadSetup.ashx?f=mysetup.exe".
  4. In the provided "DownloadSetup.ashx" handler, enumerate all files in the central download folder that match the "mysetup.exe*" wildcard, and stream the latest of the found files to the client.
  5. From time to time, delete older files that are matching the "mysetup.exe*" wildcard in the central download folder, but silently skip if a deletion fails (because the file is still in use by IIS).

With "algorithm", I am able to allow updating while still other users are downloading a previous version of the file.

Implementation of the New Solution

The new solution I implemented for our websites consists of the following blocks:

  • A web service (".asmx") on the server to receive the new setup files (or any files you want to make downloadable) to be uploaded
  • Some "web.config" appSettings entries on the website
  • A local CS-Script script that takes the local setup files and uploads it through the web service to the server
  • A generic handler (".ashx") on the web server to provide the decision logic and the actual download
  • Some URL rewriting rules to rewrite (not redirect!) the public, stable download URLs to the generic handler

The following chapters quickly introduce these blocks.

ASP.NET Web Service to Receive Uploads

I developed an ASP.NET web service (".asmx") that I use to upload files from my development workstation in our office to the public web server:

C#
[WebService(Namespace = @"http://webservices.zeta-test.com/setupupload")]
[WebServiceBinding(ConformsTo = WsiProfiles.BasicProfile1_1)]
public class SetupUploadService :
       WebService
{
       [WebMethod]
       public void TransferSetupFile(
             string apiKey,
             string fileName,
             byte[] fileContent);
}

As you can see, there is just one method. The method takes an API key (which is a simple GUID) to ensure only allowed scripts may upload to it. You may also restrict the URL to your own IP address. In addition, the original file name (e.g. "mysetup.exe") and the file content as one large byte array are the parameters.

When being called, the service uploads the file to the configured folder (see below) and also tries to delete older files with the same file name base as the provided fileName. If a removal fails (because the previous file is still in use by IIS), it is silently ignored.

The deletion process is done in the upload phase (in contrast to the download phase) to maximize the performance of the download phase and do only the absolutely necessary steps.

The full script is provided in the example download at the top of this article.

"Web.config" Settings on the Web Server

To configure the ASP.NET upload web service, I added the following configuration entries:

XML
<add key="setupUploadService.apiKey" value="17a513cb-603e-4e31-a5e7-e002b4e181c4" />
<add key="setupUploadService.baseFolderPath" value="D:\Data\Downloads\ZetaTest" />

These entries tell the web service the expected API key and where to store the uploaded file, as well as where to try to clean up old uploaded files.

A complete example "web.config" file in included in the download at the top of this article.

Local Upload Script

To actually upload a local file (e.g. "mysetup.exe") to the public web server, I decided to use the great CS-Script engine. I had very good experiences with this tool.

So I developed a script ("publish-setups.cs") that dynamically generates a web service proxy (this is a feature of the CS-Script engine) and then upload the local setup files to the web server through the ASP.NET web service (".asmx"), described above.

To dynamically include the web service reference, I used:

C#
//css_pre wsdl(https://www.zeta-test.com/MyUploadService.asmx, MyUploadService); 
//css_imp MyUploadService; 

This generates a proxy class automatically as a new file.

To transfer the files to the server, I wrote a function that can be called like this:

C#
transferFiles(
        @"${ScriptFolderPath}\..\Master\zetatest-setup-core.exe",
        @"${ScriptFolderPath}\..\Master\zetatest-deployment-setup.exe",
        @"${ScriptFolderPath}\..\Master\zetatest-setup.msi",
        @"${ScriptFolderPath}\..\Master\zetatest-setup-de.exe",
        @"${ScriptFolderPath}\..\Master\zetatest-setup-en.exe"); 

The "${ScriptFolderPath}" is a placeholder that ensures that I only do have relative paths in the script and therefore is usable wherever I SVN check out the project.

Again, the full source to the script is available in this article's download.

To actually call the CS-Script upload script, I use a simple Windows command line batch file "publish-setups.cmd":

@REM http://blogs.msdn.com/oldnewthing/archive/2005/01/28/362565.aspx
PUSHD 
CD /d %~dp0 
CLS
"\\mylocal-fileserver\tools\cs-script\cscs.exe" "%CD%\publish-setups.cs"
POPD
@REM PAUSE

This batch file is available in the download at the top of this article, too.

Generic Download Handler on the Web Server

Since the uploaded files have that random file name suffix and are stored inside a folder that is (by intention) not accessible through a public URL, an active component is required to "stream" a download to the browser.

Therefore I developed a generic ASP.NET handler (".ashx") that does just that:

C#
public class DownloadSetup : 
       IHttpHandler
{
       public void ProcessRequest(
             HttpContext context);
 
       private static void streamFileToBrowser(
             HttpResponse response,
             FileInfo file,
             string fileName);
}

The handler is called with the requested URL in a query string parameter ("f"). It enumerates a list of all files in the central download folder that matches the requested file name and then streams the newest found file to the clients web browser.

Please see the download at the top of this article for the full source code of the download handler.

URL Rewriting of the Public Stable Download URLs

The last step is to add some URL rewriting to your "web.config" file to have public, stable download URLs like e.g. "http://www.zeta-test.com/media/files/zetatest-setup-core.exe" that actually fetch the file through the generic ASP.NET handler (".ashx") for downloading.

Since we used the great, free, open source URL Rewriter on IIS6, we decided to stick with it for now, on IIS7, too. I am aware that IIS7 provides its own URL Rewrite module, but for ease of use, I just used the URL Rewriter which still works well on IIS7 for me.

An example excerpt from the URL rewrite rules I used to rewrite are:

XML
<rewrite url="/media/files/zetatest-setup-de.exe" 
	to="/downloadsetup.ashx?f=zetatest-setup-de.exe" processing="stop" />
<rewrite url="/media/files/zetatest-setup-en.exe" 
	to="/downloadsetup.ashx?f=zetatest-setup-en.exe" processing="stop" />
<rewrite url="/media/files/zetatest-setup-core.exe" 
	to="/downloadsetup.ashx?f=zetatest-setup-core.exe" processing="stop" />
<rewrite url="/media/files/zetatest-deployment-setup.exe" 
	to="/downloadsetup.ashx?f=zetatest-deployment-setup.exe" processing="stop" />
<rewrite url="/media/files/zetatest-setup.msi" 
	to="/downloadsetup.ashx?f=zetatest-setup.msi" processing="stop" />

Please note that I used "rewrite" instead of "redirect". This ensures that the browser never sees the URL of the generic ".ashx" handler but only the stable, public URL.

Drawbacks

There are several drawbacks to my solution, I can think of:

  • The idea is rather new, I developed and implemented it last week; so it is likely that I still forgot some possible issue scenarios that may arise. If you found one, please let me know, I will provide a fix then!
  • There is a performance overhead compared to the original version: The enumeration of files in the central download folder, the ASP.NET code to stream the file to download, the URL rewriting.
  • The upload web service takes one large byte array instead of allowing a chunked upload. If you have an unstable internet connection, this may be not error-tolerant enough.
  • It is more complicated in terms of handling: You require several files on your server and an upload script.
  • You need more hard disk space on your web server, since usually multiple versions of a downloadable file are being stored.

Summary

This article introduced one way to update currently locked files on a Windows IIS web server. The article is more of a discussion of a concept than a complete drop-in-replacement to your current solution. Please keep in mind that you should have a basic understanding of what you are trying to achieve.

Since I am no rocket scientist, my solution isn't rocket science, either. Expect it to be much more improvable or even being erroneous. Therefore I strongly encourage you to send me feedback if you see issues or have improvements. To ask questions, suggest features or provide other comments, please use the comments section at the bottom of this article.

Thank you!

History

  • 2010-05-04 - First update to CodeProject.com

License

This article, along with any associated source code and files, is licensed under The Code Project Open License (CPOL)