2008-04-04

Binary response in ASP MVC

Today I wanted to give access to certain files on a website only via my DownloadController. This was so that I could ensure the current user had purchased the item in question first, and also sign any license info into the download aswell.



I tried getting a URL like this to work



http://localhost/download/1/SomeFileName



which would remap to the DownloadController



public void Index(int id, string fileName)





This worked fine, and because the URL ended with "SomeFileName" it would get saved as the correct filename too, but this was no use because SomeFileName has no file extension. As soon as I added .zip on the end the request no longer went via the new HttpHandler in the MVC web extensions. Even when I added it in the <httpHandlers> section of web.config it just wouldn’t work.



My problem was in relying on the url for the filename. This is apprarently not the way it should be done. Instead I should have stuck to the standard URL approach



http://localhost/download/1



and added a special HTTP header known as "content-disposition" to the response, this tells the client what the filename should be. Here is a full example of how to write a binary file to the Response when using the new MVC ASP Web Extensions, and how to have it saved on the client with the correct filename.



public void Index(int id)
{
 IProductRepository productRepository = EcoSpace.GetEcoService<IProductRepository>();
 Product product = productRepository.GetByID(id);
 if (product == null)
 {
  ViewData[GlobalViewDataKeys.ErrorMessage] = "Item not found";
  Response.Redirect("/Account/Home", false);
  return;
 }

 Response.ContentType = "Application/" + Path.GetExtension(product.DownloadUrl).Substring(1);
 Response.AppendHeader("content-disposition", "inline; filename=" + product.DownloadUrl);

 string localFileName = "";
 if (product is Edition)
  localFileName = FilePathUrls.Software;
 else
  if (product is Collateral)
   localFileName = FilePathUrls.Collateral;
  else
   throw new NotImplementedException(product.GetType().Name);

 localFileName = Request.MapPath(localFileName);
 localFileName = Path.Combine(localFileName, product.DownloadUrl);

 FileStream fileStream = new FileStream(localFileName, FileMode.Open, FileAccess.Read, FileShare.Read);
 byte[] data = new byte[fileStream.Length];
 using (fileStream)
  fileStream.Read(data, 0, (int)fileStream.Length);
 Response.BinaryWrite(data);
 Response.End();
}



Thanks go to Phil Haak who pointed me in the right direction and was kind enough to promptly help a complete stranger!

3 comments:

Dmitriy Nagirnyak said...

Interesting post Pete.
A quick note: you might think about using Response.WriteFile instead of reading the whole file into memory and pushing it to output stream.

Peter Morris said...

Thanks for that. I am reading into memory because I first need to pass it through a license generator to sign it.

See this post
http://mrpmorris.blogspot.com/2008/02/custom-config-sections.html

Pete

Dan said...

Thanks for the code, it's a very big help!

direct response