How to programatically record the start and end of an MVC FileResult for logging of incomplete downloads?
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 HttpModul开发者_开发问答e 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
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");
}
}
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();
}
}
It can be done more simple: use Response.TransmitFile() and (IMPORTANT!) set the Response.BufferOutput = false. The default BufferOutput value is true - that means the output is buffered and will be sent after the complete page has finished processing.
Controller:
// record download start event here
...
// send a file to Response, not buffered.
// In my case the downloaded file has name "setup.exe"
Response.ContentType = "application/octet-stream";
Response.AddHeader("Content-Disposition", @"filename=""setup.exe""");
Response.BufferOutput = false;
Response.TransmitFile(fileURL);
// record download finish event here
...
精彩评论