views:

113

answers:

2

Hi,

I have been investigating how My ASP.NET MVC application can log unsuccessful / incomplete downloads. I have a controller handling the file requests which currently returns a FileResult. What I need to record is the IPAddress, filename and when the download started, then the same data and when it completed. I have looked at intercepting the request start and end with an HttpModule and also IIS7 failed request tracing but am not sure which route would be best and have this feeling that maybe I am missing an obvious answer to the problem.

Does anyone have any suggestions or know of any alternatives as this seems like something many people would want to know from their web server?

Thanks for your help

+2  A: 

You could try writing a custom FilePathResult and override the WriteFileMethod:

public class CustomFileResult : FilePathResult
{
    public CustomFileResult(string fileName, string contentType)
        : base(fileName, contentType)
    { }

    protected override void WriteFile(HttpResponseBase response)
    {
        // TODO: Record file download start
        base.WriteFile(response);
        // TODO: Record file download end
    }
}

and in your controller:

public class HomeController : Controller
{
    public ActionResult Index()
    {
        return new CustomFileResult(@"d:\test.jpg", "image/jpg");
    }
}
Darin Dimitrov
I have to ask would the WriteFile have any memory implications if the file sizes are in the GB range?
Richard
This would depend on the implementation. For example if you are using `FileContentResult` then the whole binary array is loaded in memory and could definitely have implications. If you are using a `FilePathResult` the transmission is delegated to the `HttpResponse.TransmitFile` function which writes it without buffering it in memory.
Darin Dimitrov
I have now tried this but it would seem that the base.WriteFile(response); is an asynchronous call which means the code execution passes straight to the next line and does not wait until the file has completely downloaded.
Richard
I have now created a class to solve this problem and added it as an answer if you are interested
Richard
A: 

OK so having tried the first answer it did not work as the call to base.WriteFile(response); works asynchronously.

I have since written an extension of the FilePathResult class which works by streaming the response. I have also added simple support for file resuming using the range header instruction.

public class LoggedFileDownload : FilePathResult
    {
        private readonly IRepository repository;
        private readonly AssetDownload assetDownload;
        public LoggedFileDownload(string fileName, string contentType, string downloadName, IRepository repository, AssetDownload assetDownload) : base(fileName, contentType)
        {
            FileDownloadName = downloadName;
            this.repository = repository;
            this.assetDownload = assetDownload;
        }

        protected override void WriteFile(HttpResponseBase response)
        {
            long totalSent = 0;
            long bytesRead = 0;
            var fileInfo = new FileInfo(FileName);
            var readStream = fileInfo.OpenRead();
            var buffer = new Byte[4096];

            long responseLength = readStream.Length;
            var rangeHeader = HttpContext.Current.Request.Headers["Range"];

            if (!rangeHeader.IsNullOrEmpty())
            {

                string[] range = rangeHeader.Substring(rangeHeader.IndexOf("=") + 1).Split('-');

                long start = Convert.ToInt64(range[0]);
                long end = 0;

                if (range[1].Length > 0) end = int.Parse(range[1]);

                if (end < 1) end = fileInfo.Length; 

                if (start > 0)
                {
                    responseLength -= start;
                    readStream.Seek(start, 0);
                    totalSent += start;
                    var rangeStr = string.Format("bytes {0}-{1}/{2}", start, end, fileInfo.Length);
                    response.StatusCode = 206;
                    response.AddHeader("Content-Range",rangeStr);
                }
            }

            response.AddHeader("Content-Disposition", string.Format("attachment; filename=\"{0}\"", FileDownloadName));
            response.AddHeader("Content-MD5", GetMD5Hash(fileInfo));
            response.AddHeader("Accept-Ranges", "bytes");
            response.AddHeader("Content-Length", (responseLength).ToString());
            response.AddHeader("Connection", "Keep-Alive");
            response.ContentType = FileTypeHelper.GetContentType(fileInfo.Name);
            response.ContentEncoding = Encoding.UTF8;

            response.Clear();

            while(response.IsClientConnected && (bytesRead = readStream.Read(buffer, 0, buffer.Length)) != 0 )
            {
                totalSent += bytesRead;
                response.BinaryWrite(buffer);
                response.Flush();
            }

            if (totalSent == fileInfo.Length)
            {
                // This means the file has completely downloaded so we update the DB with the completed field set to true
                assetDownload.Completed = true;
                repository.Save(assetDownload);
                repository.Flush();
            }

        }

        private static string GetMD5Hash(FileInfo file)
        {
            var stream = file.OpenRead();
            MD5 md5 = new MD5CryptoServiceProvider();
            byte[] retVal = md5.ComputeHash(stream);
            stream.Close();

            var sb = new StringBuilder();
            for (int i = 0; i < retVal.Length; i++)
            {
                sb.Append(retVal[i].ToString("x2"));
            }
            return sb.ToString();
        }


    }
Richard