I have an application that is used by people inside and outside my organization. This application exports both Excel (.xlsx) and PDF files. I'm having trouble with the file exports. It works fine for people that are on my network, but people outside my network are getting a "File read error. File type is unsupported or the file is corrupted", and the file will only be 127 bytes instead of it's correct size (normally about 2 megabytes). I need people outside my network to be able to successfully download and open the files.
I've also tried running handler classes tailored to each specific file type, I've tried opening up the directory with the file to let "Everyone" have read access, I'm really not sure on how to fix this. The web server is running IIS 10.
public class fileExportHandler : IHttpHandler
{
public void ProcessRequest(HttpContext context)
{
string fileToExport = "";
string fileName = "exportedFile";
string fileType = "";
System.Web.HttpRequest request = System.Web.HttpContext.Current.Request;
if ((request.QueryString["fileToExport"] != null))
{
fileToExport = request.QueryString["fileToExport"].ToString();
string[] fileParts = fileToExport.Split('.');
fileType = fileParts[1];
if ((request.QueryString["fileName"] != null))
{
fileName = request.QueryString["fileName"].ToString();
}
}
fileToExport = #"E:\Website\Cascade\" + fileToExport;
//send the file to the browser
System.Web.HttpResponse Response = System.Web.HttpContext.Current.Response;
Response.ClearHeaders();
Response.Clear();
Response.Buffer = true;
string contentType = "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet";
if(fileType == "pdf")
contentType = "application/pdf";
Response.ContentType = contentType;
Response.AddHeader("content-disposition", "attachment; filename=" + fileName + "." + fileType);
Response.TransmitFile(fileToExport);
Response.Flush();
HttpContext.Current.ApplicationInstance.CompleteRequest();
}
public bool IsReusable
{
get
{
return false;
}
}
}
Got it figured out. It was a network issue, my network people had moved me to a new server, and people outside my organization were seeing the old server, but people inside were seeing the new version. I got them to get the people outside my organization to see the new server, and that resolved my issue.
I have download code functionality in my ASP.NET project and the download code look like below.
public class Download : IHttpHandler
{
private void DownloadPsListingProduct(Guid which)
{
string path = GetFilePathFromGuid(which);
FileInfo file = new FileInfo(path);
HttpResponse response = HttpContext.Current.Response;
response.ClearContent();
response.Clear();
response.ContentType = "application/octet-stream";
response.AddHeader("Content-Disposition",
"attachment;filename=\"" + file.Name.NeutralizationCrlfSequences() + "\";");
response.TransmitFile(file.FullName);
response.Flush();
response.End();
}
public bool IsReusable
{
get
{
return false;
}
}
}
This code work like a charm when I download single file at a time.
But when one file is under process of downloading and I request to download other file then it first wait to completion of first file downloading and then second file download start.
Note: I am sending new request to download each file.
I want to avoid this single file download behavior and user should able to download files without waiting previous one to complete.
ASP.NET Web API 2 should be able to handle this with very little ceremony. There's an example here, but I'll re-iterate the important parts:
public class FilesController : ApiController
{
public IHttpActionResult(Guid fileId)
{
var filePath = GetFilePathFromGuid(fileId);
var fileName = Path.GetFileName(filePath);
var mimeType = MimeMapping.GetMimeMappting(fileName);
return OkFileDownloadResult(filePath, mimeType, fileName, this);
}
}
Of course, hooking up routing etc in ASP.NET Web API 2 is quite different from hooking up an IHttpHandler, but there's also a plethora of examples on the internet (including here on SO) on how to get started with that.
I have the code below which works well for small files but for large files it generates the zip as required but doesn't download it. I get all sorts of errors including Timeout (which I have managed to resolve). The other problem is that it runs in Sync. The largest file I have generated myself is a 330MB zip file with about 30 HD images attached to it. But this can even go to GBs as the user can choose to download about 100 or even more HD images at once.
To resolve both issues, I thought downloading in async may help in both cases. I want to alert the user that their download has started, and that they will be notified when it is ready.
I am thinking of sending the stream down if the client IsConnected (then delete the file) or sending an email to ask them to download the file if they have decided to logout (then delete the file using the offline download link). I just don't know where or how to write async code, or if what I want to do can actually be done if the user decides to logout.
Here's my current code:
private void DownloadFile(string filePath)
{
FileInfo myfile = new FileInfo(filePath);
// Checking if file exists
if (myfile.Exists)
{
// Clear the content of the response
Response.ClearContent();
// Add the file name and attachment, which will force the open/cancel/save dialog box to show, to the header
Response.AddHeader("Content-Disposition", "attachment; filename=" + myfile.Name);
// Add the file size into the response header
Response.AddHeader("Content-Length", myfile.Length.ToString());
// Set the ContentType
Response.ContentType = "application/octet-stream";
Response.TransmitFile(filePath);
Response.Flush();
try
{
myfile.Delete();
}
catch { }
}
}
I don't know about Async downloads from asp.net applications so I can't address that question. But I have run into enough download issues to always start from the same place.
First, download from a generic handle (ASHX) and not a web form. The webform wants to do extra processing at the end of the request that can cause problems. You question didn't state if you are using a web form or generic handler.
Second, always end the request with the ApplicationInstance.CompleteRequest() method call. Don't use Request.Close() or Request.End()
Those two changes have often cleaned up download issues for me. Try these change and see if you get the same results. Even if you do get the same results this is a better way of coding downloads.
Finally, as an aside, only catch appropriate exceptions in the try-catch bock.
Your code would be like this:
public class Handler1 : IHttpHandler
{
public void ProcessRequest(HttpContext context)
{
// set from QueryString
string filePath = "...";
FileInfo myfile = new FileInfo(filePath);
// Checking if file exists
if (myfile.Exists)
{
// Clear the content of the response
context.Response.ClearContent();
// Add the file name and attachment, which will force the open/cancel/save dialog box to show, to the header
context.Response.AddHeader("Content-Disposition", "attachment; filename=" + myfile.Name);
// Add the file size into the response header
context.Response.AddHeader("Content-Length", myfile.Length.ToString());
// Set the ContentType
context.Response.ContentType = "application/octet-stream";
context.Response.TransmitFile(filePath);
context.Response.Flush();
HttpContext.Current.ApplicationInstance.CompleteRequest();
try
{
myfile.Delete();
}
catch (IOException)
{ }
}
}
public bool IsReusable
{
get
{
return false;
}
}
}
I have a handler which works as it should to serve a download. This is the important code:
// Get size of file
FileInfo f = new FileInfo(Settings.ReleaseFileLocation + ActualFileName);
long FileSize = f.Length;
// Init (returns ID of tblDownloadLog record created with blank end date)
int DownloadRecordID = Constructor.VersionReleaseDownload.newReleaseDownload(ActualFileName);
context.Response.Clear();
context.Response.Buffer = false;
context.Response.ContentType = "application/octet-stream";
context.Response.AddHeader("Content-Disposition", "attachment; filename=" + OriginalFileName);
context.Response.AddHeader("Content-Length", FileSize.ToString());
context.Response.TransmitFile(Settings.ReleaseFileLocation + ActualFileName);
context.Response.Close();
// Complete download log, fills out the end date
Constructor.VersionReleaseDownload.completeReleaseDownload(DownloadRecordID);
The context.Response.Close(); ensures the completeReleaseDownload() only runs when the download is complete which is very useful (re Only count a download once it's served)
Problem is, we're getting a lot of logs that come from the same IP address in about the same time spacing. After digging a bit deeper it appears they are users using Download Resumer software.
When I try to use a download resumer it seems to fail. My question is:
How do I detect this is a partial request
How can I serve the partial request
How can I make it work with the above code so it a) Calls https://www.scirra.com/downloads/releases/construct2-r68-setup_4.exe on the first partial get and b) Calls completeReleaseDownload on the last partial get?
This is achieved in Mime with an E-Tag, check out: http://www.devx.com/dotnet/Article/22533/1954
When you capture some packets sent using DownloadResumer, you will probably find the Range tag being specified.
Range: bytes=500-1000
This allows you to check if this is a partial request and if so, take action like:
bool isFirstRequest = RangeStart == 0;
bool isLastRequest = RangeEnd == file.TotalBytes - 1;//(Ranges use Zero-Based Indices)
I have a number of pages which need to support exporting data to an Excel spreadsheet. I can generate the Excel files just fine, but I'm trying to work out how to abstract this behavior so it's easily reusable from all of the pages where I need it. My current idea is to use a static utility method, as follows:
public static void SendExcelFile(System.Web.UI.Page callingPage, string downloadFileName, List<List<string>> data, string worksheetTitle)
{
string tempFileName = Path.GetTempFileName();
try
{
// Generate file using ExcelPackage
GenerateExcelDoc(tempFileName, data, worksheetTitle);
callingPage.Response.AddHeader("Content-Disposition", "attachment;filename=" + downloadFileName);
callingPage.Response.ContentType = "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet";
callingPage.Response.AddHeader("Content-Length", new FileInfo(tempFileName).Length.ToString());
callingPage.Response.TransmitFile(tempFileName);
}
finally
{
//When this is removed, the method works as expected.
if (File.Exists(tempFileName))
File.Delete(tempFileName);
}
}
The click handler where I'm calling SendExcelFile looks like this:
protected void lnkExport_Click(object sender, EventArgs e)
{
List<List<string>> dataList = GatherDataForSpreadsheet();
Utility.SendExcelFile(this, "fileNameForDownload.xlsx", dataList, "MyReports");
}
This code works just fine as an instance method of the calling page. As a static method, though, it doesn't work at all. When I click the button that invokes this, the browser shows the loading animations indefinitely, but never prompts for a file download.
I'm very new to ASP.NET (and web programming in general), so I'm sure I'm missing something here. Could someone please explain the behavior I'm seeing, and suggest a reasonable alternative to this approach?
EDIT: If I remove the call to File.Delete() at the end, the method works as expected. Does Response.TransmitFile() do the transfer asynchronously?
EDIT 2: I just needed to call Response.Flush() before I deleted the file. See my answer below.
Thanks!
The problem was that the temp file was being deleted before the data was sent down. I just needed to call Response.Flush() like so:
public static void SendExcelFile(System.Web.UI.Page callingPage, string downloadFileName, List<List<string>> data, string worksheetTitle)
{
string tempFileName = Path.GetTempFileName();
try
{
// Generate file using ExcelPackage
GenerateExcelDoc(tempFileName, data, worksheetTitle);
callingPage.Response.AddHeader("Content-Disposition", "attachment;filename=" + downloadFileName);
callingPage.Response.ContentType = "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet";
callingPage.Response.AddHeader("Content-Length", new FileInfo(tempFileName).Length.ToString());
callingPage.Response.TransmitFile(tempFileName);
callingPage.Response.Flush(); //This is what I needed
}
finally
{
if (File.Exists(tempFileName))
File.Delete(tempFileName);
}
}
Try this, you can get the Request and Response directly off HttpContext.Current:
public static void SendExcelFile(string downloadFileName, List<List<string>> data, string worksheetTitle)
{
var context = HttpContext.Current;
string tempFolder = context.Request.PhysicalApplicationPath + "temp";
string tempFileName = tempFolder + "tempFileName.xlsx"
if (!Directory.Exists(tempFolder))
Directory.CreateDirectory(tempFolder);
// Generate file using ExcelPackage
GenerateExcelDoc(tempFileName, data, worksheetTitle);
context.Response.AddHeader("Content-Disposition", "attachment;filename=" + downloadFileName);
context.Response.ContentType = "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet";
context.Response.AddHeader("Content-Length", new FileInfo(tempFileName).Length.ToString());
context.Response.TransmitFile(tempFileName);
File.Delete(tempFileName);
}
Another alternative is a base class for your pages that contains this method, that may be a much easier route. You pages don't have to inherit from System.Web.UI.Page, they can inherit from something else, like this:
public class BasePage : System.Web.UI.Page
{
public void SendExcelFile(string downloadFileName, List<List<string>> data, string worksheetTitle)
{
string tempFolder =Request.PhysicalApplicationPath + "temp";
string tempFileName = tempFolder + "tempFileName.xlsx"
if (!Directory.Exists(tempFolder))
Directory.CreateDirectory(tempFolder);
// Generate file using ExcelPackage
GenerateExcelDoc(tempFileName, data, worksheetTitle);
Response.AddHeader("Content-Disposition", "attachment;filename=" + downloadFileName);
Response.ContentType = "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet";
Response.AddHeader("Content-Length", new FileInfo(tempFileName).Length.ToString());
Response.TransmitFile(tempFileName);
File.Delete(tempFileName);
}
}
Then in your page the class looks like:
public partial class MyPage : BasePage
{
//Stuff!
}
We need more information - what you're doing should work.
I created a stripped-down version that just sends a copy of the calling page to the client and it works as expected:
public class Utility {
// This just sends the client a copy of the calling page itself
public static void SendExcelFile(Page callingPage) {
string path = callingPage.Request.PhysicalPath;
callingPage.Response.AddHeader("Content-Disposition", "attachment;filename=test.xls");
callingPage.Response.ContentType = "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet";
callingPage.Response.AddHeader("Content-Length", new FileInfo(path).Length.ToString());
callingPage.Response.TransmitFile(path);
}
}
Here's my calling page:
public partial class main : System.Web.UI.Page {
protected void SendButton_Click(object sender, EventArgs e) {
Utility.SendExcelFile(this);
}
}
Do you see any differences from your implementation?
At this point I'd use an HTTP debugging proxy like Fiddler to compare the HTTP sessions generated by both the working (page codebehind) and nonworking (static) versions of your code.
As an aside, you should be aware that your code as written won't work well if more than one user clicks the button at the same time -- the first user's temp file may get overwritten by the second user's file, and the second user's file may get deleted in the middle of being transmitted! Consider using Path.GetTempFileName() or a guid in the filename to ensure that each user's file is uniquely named.
I would use this instead. The current HTTP context will be available on every page.
HttpContext.Current.Response.AddHeader("Content-Disposition", "attachment;filename=" + downloadFileName);
HttpContext.Current.Response.ContentType = "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet";
HttpContext.Current.Response.AddHeader("Content-Length", new FileInfo(tempFileName).Length.ToString());
HttpContext.Current.Response.TransmitFile(tempFileName);