Reputation: 201
I'm trying to implement file download in MVC. Here's the current code:
Response.Clear();
Response.Charset = "utf8";
Response.ContentType = string.IsNullOrEmpty(dokument.mimeType) ? MimeHelper.GetMimeFromBytes(dokument.Bin) : dokument.mimeType;
Response.AddHeader("Content-Disposition", "attachment; filename*=UTF-8''" + dokument.Nazwa.Replace(" ", "_").Replace(";", "%3B").Replace(",", "%2C"));
Response.BinaryWrite(dokument.Bin);
Response.Flush();
Response.End();
It works nice but some files are downloaded with their database id as a name. This value is passed as a parameter to Action. Other files download properly and I can't find the cause of it. Does anyone have any idea what may be wrong?
If I remove '*=UTF-8''" comma and semicolon codes are not changed.
Upvotes: 0
Views: 1906
Reputation: 201
Actually I've changed header generation to return File()
and it works fine, although algorithm that tpeczek provided works as well.
Upvotes: 0
Reputation: 24125
Most probably your header is faulty in some of those cases.
Generating proper Content-Disposition
header is a little bit more complex than that. It is best to start of by implementation available already in the framework and go for RFC 2231 only when trully needed, something like this:
public class ContentDispositionUtil
{
public static string GetContentDisposition(string fileName)
{
try
{
return (new ContentDisposition() { FileName = fileName }).ToString();
}
catch (FormatException)
{
return GetRfc2231ContentDisposition(fileName);
}
}
}
The implementation of the GetRfc2231ContentDisposition
is the tricky part. In general you need to check every character in the file name if it is valid for the header and if not properly encode it. The checking can be done with following method:
private static bool IsValidRfc2231ContentDispositionCharacter(byte character)
{
if ((byte)'0' <= character && character <= (byte)'9')
return true;
if ((byte)'a' <= character && character <= (byte)'z')
return true;
if ((byte)'A' <= character && character <= (byte)'Z')
return true;
switch (character)
{
case (byte)'-':
case (byte)'.':
case (byte)'_':
case (byte)'~':
case (byte)':':
case (byte)'!':
case (byte)'$':
case (byte)'&':
case (byte)'+':
return true;
}
return false;
}
Simple implementation, but it shows which characters are allowed. Now you can go for encoding like this:
private const string _hexDigits = "0123456789ABCDEF";
private static string EncodeInvalidRfc2231ContentDispositionCharacter(int character)
{
return "%" + _hexDigits[character >> 4] + _hexDigits[character % 16];
}
Now we are ready for the header generation:
private static string GetRfc2231ContentDisposition(string filename)
{
StringBuilder contentDispositionBuilder = new StringBuilder("attachment; filename*=UTF-8''");
byte[] filenameBytes = Encoding.UTF8.GetBytes(filename);
foreach (byte character in filenameBytes)
{
if (IsValidRfc2231ContentDispositionCharacter(character))
contentDispositionBuilder.Append((char)character);
else
{
contentDispositionBuilder.Append(EncodeInvalidRfc2231ContentDispositionCharacter((int)character));
}
}
return contentDispositionBuilder.ToString();
}
Now you should always get the proper header.
P.S. Of course do all this only when trully needed and use built in action results delivered from FileResult
whenever possible ;)
Upvotes: 1