Я использую следующие классы для того, чтобы иметь больше опций с заголовком размещения контента.
Он работает так же, как Ответ Marnix , но вместо полной генерации заголовка с классом ContentDisposition
,что, к сожалению, не соответствует RFC , когда имя файла должно быть в кодировке utf-8, вместо него настраивается заголовок, сгенерированный MVC, что соответствует RFC.
(Изначально я написалчто частично используя этот ответ на другой вопрос и этот еще один .)
using System;
using System.IO;
using System.Web;
using System.Web.Mvc;
namespace Whatever
{
/// <summary>
/// Add to FilePathResult some properties for specifying file name without forcing a download and specifying size.
/// And add a workaround for allowing error cases to still display error page.
/// </summary>
public class FilePathResultEx : FilePathResult
{
/// <summary>
/// In case a file name has been supplied, control whether it should be opened inline or downloaded.
/// </summary>
/// <remarks>If <c>FileDownloadName</c> is <c>null</c> or empty, this property has no effect (due to current implementation).</remarks>
public bool Inline { get; set; }
/// <summary>
/// Whether file size should be indicated or not.
/// </summary>
/// <remarks>If <c>FileDownloadName</c> is <c>null</c> or empty, this property has no effect (due to current implementation).</remarks>
public bool IncludeSize { get; set; }
public FilePathResultEx(string fileName, string contentType) : base(fileName, contentType) { }
public override void ExecuteResult(ControllerContext context)
{
FileResultUtils.ExecuteResultWithHeadersRestoredOnFailure(context, base.ExecuteResult);
}
protected override void WriteFile(HttpResponseBase response)
{
if (Inline)
FileResultUtils.TweakDispositionAsInline(response);
// File.Exists is more robust than testing through FileInfo, especially in case of invalid path: it does yield false rather than an exception.
// We wish not to crash here, in order to let FilePathResult crash in its usual way.
if (IncludeSize && File.Exists(FileName))
{
var fileInfo = new FileInfo(FileName);
FileResultUtils.TweakDispositionSize(response, fileInfo.Length);
}
base.WriteFile(response);
}
}
/// <summary>
/// Add to FileStreamResult some properties for specifying file name without forcing a download and specifying size.
/// And add a workaround for allowing error cases to still display error page.
/// </summary>
public class FileStreamResultEx : FileStreamResult
{
/// <summary>
/// In case a file name has been supplied, control whether it should be opened inline or downloaded.
/// </summary>
/// <remarks>If <c>FileDownloadName</c> is <c>null</c> or empty, this property has no effect (due to current implementation).</remarks>
public bool Inline { get; set; }
/// <summary>
/// If greater than <c>0</c>, the content size to include in content-disposition header.
/// </summary>
/// <remarks>If <c>FileDownloadName</c> is <c>null</c> or empty, this property has no effect (due to current implementation).</remarks>
public long Size { get; set; }
public FileStreamResultEx(Stream fileStream, string contentType) : base(fileStream, contentType) { }
public override void ExecuteResult(ControllerContext context)
{
FileResultUtils.ExecuteResultWithHeadersRestoredOnFailure(context, base.ExecuteResult);
}
protected override void WriteFile(HttpResponseBase response)
{
if (Inline)
FileResultUtils.TweakDispositionAsInline(response);
FileResultUtils.TweakDispositionSize(response, Size);
base.WriteFile(response);
}
}
/// <summary>
/// Add to FileContentResult some properties for specifying file name without forcing a download and specifying size.
/// And add a workaround for allowing error cases to still display error page.
/// </summary>
public class FileContentResultEx : FileContentResult
{
/// <summary>
/// In case a file name has been supplied, control whether it should be opened inline or downloaded.
/// </summary>
/// <remarks>If <c>FileDownloadName</c> is <c>null</c> or empty, this property has no effect (due to current implementation).</remarks>
public bool Inline { get; set; }
/// <summary>
/// Whether file size should be indicated or not.
/// </summary>
/// <remarks>If <c>FileDownloadName</c> is <c>null</c> or empty, this property has no effect (due to current implementation).</remarks>
public bool IncludeSize { get; set; }
public FileContentResultEx(byte[] fileContents, string contentType) : base(fileContents, contentType) { }
public override void ExecuteResult(ControllerContext context)
{
FileResultUtils.ExecuteResultWithHeadersRestoredOnFailure(context, base.ExecuteResult);
}
protected override void WriteFile(HttpResponseBase response)
{
if (Inline)
FileResultUtils.TweakDispositionAsInline(response);
if (IncludeSize)
FileResultUtils.TweakDispositionSize(response, FileContents.LongLength);
base.WriteFile(response);
}
}
public static class FileResultUtils
{
public static void ExecuteResultWithHeadersRestoredOnFailure(ControllerContext context, Action<ControllerContext> executeResult)
{
if (context == null)
throw new ArgumentNullException("context");
if (executeResult == null)
throw new ArgumentNullException("executeResult");
var response = context.HttpContext.Response;
var previousContentType = response.ContentType;
try
{
executeResult(context);
}
catch
{
if (response.HeadersWritten)
throw;
// Error logic will usually output a content corresponding to original content type. Restore it if response can still be rewritten.
// (Error logic should ensure headers positionning itself indeed... But this is not the case at least with HandleErrorAttribute.)
response.ContentType = previousContentType;
// If a content-disposition header have been set (through DownloadFilename), it must be removed too.
response.Headers.Remove(ContentDispositionHeader);
throw;
}
}
private const string ContentDispositionHeader = "Content-Disposition";
// Unfortunately, the content disposition generation logic is hidden in an Mvc.Net internal class, while not trivial (UTF-8 support).
// Hacking it after its generation.
// Beware, do not try using System.Net.Mime.ContentDisposition instead, it does not conform to the RFC. It does some base64 UTF-8
// encoding while it should append '*' to parameter name and use RFC 5987 encoding. http://tools.ietf.org/html/rfc6266#section-4.3
// And https://stackoverflow.com/a/22221217/1178314 comment.
// To ask for a fix: https://github.com/aspnet/Mvc
// Other class : System.Net.Http.Headers.ContentDispositionHeaderValue looks better. But requires to detect if the filename needs encoding
// and if yes, use the 'Star' suffixed property along with setting the sanitized name in non Star property.
// MVC 6 relies on ASP.NET 5 https://github.com/aspnet/HttpAbstractions which provide a forked version of previous class, with a method
// for handling that: https://github.com/aspnet/HttpAbstractions/blob/dev/src/Microsoft.Net.Http.Headers/ContentDispositionHeaderValue.cs
// MVC 6 stil does not give control on FileResult content-disposition header.
public static void TweakDispositionAsInline(HttpResponseBase response)
{
var disposition = response.Headers[ContentDispositionHeader];
const string downloadModeToken = "attachment;";
if (string.IsNullOrEmpty(disposition) || !disposition.StartsWith(downloadModeToken, StringComparison.OrdinalIgnoreCase))
return;
response.Headers.Remove(ContentDispositionHeader);
response.Headers.Add(ContentDispositionHeader, "inline;" + disposition.Substring(downloadModeToken.Length));
}
public static void TweakDispositionSize(HttpResponseBase response, long size)
{
if (size <= 0)
return;
var disposition = response.Headers[ContentDispositionHeader];
const string sizeToken = "size=";
// Due to current ancestor semantics (no file => inline, file name => download), handling lack of ancestor content-disposition
// is non trivial. In this case, the content is by default inline, while the Inline property is <c>false</c> by default.
// This could lead to an unexpected behavior change. So currently not handled.
if (string.IsNullOrEmpty(disposition) || disposition.Contains(sizeToken))
return;
response.Headers.Remove(ContentDispositionHeader);
response.Headers.Add(ContentDispositionHeader, disposition + "; " + sizeToken + size.ToString());
}
}
}
Пример использования:
public FileResult Download(int id)
{
// some code to get filepath and filename for browser
...
return
new FilePathResultEx(filepath, System.Web.MimeMapping.GetMimeMapping(filename))
{
FileDownloadName = filename,
Inline = true
};
}
Обратите внимание, чтоуказание имени файла с помощью Inline
не будет работать с Internet Explorer (включая 11, включая Windows 10 Edge, протестировано с некоторыми файлами PDF), в то время как он работает с Firefox и Chrome.Internet Explorer будет игнорировать имя файла.Для Internet Explorer вам нужно взломать ваш путь к URL, что довольно плохо, IMO.См. этот ответ .