我试图在我的ASP.NET mVC应用程序中创建一个ZipArchive,虽然一切似乎都正常(基于调试时的变量内容),最终结果不会下载到用户浏览器。
我有一个javascript函数,当用户点击链接下载照片时会调用它,该照片会将所有选定照片的ID发送到服务器端功能:
$(document).on('click', '#download-photos', function (e) {
var PhotoIds = $(".chkDownloadPhoto:checked").map(function () {
return $(this).val();
}).get();
$.ajax({
url: '/Photo/Download/',
type: 'POST',
contentType: 'application/json',
dataType: 'json',
data: JSON.stringify({
photoIds: PhotoIds
}),
});
});
这是服务器端功能:
public ActionResult Download(List<int> photoIds)
{
var attachments = new List<DownloadItem>();
foreach (int photoId in photoIds)
{
var Photo = db.Photos.Find(photoId);
var image = new WebImage(Server.MapPath(Photo.imageUrl));
attachments.Add(new DownloadItem
{
Data = image.GetBytes(),
FileName = Photo.Filename
});
}
using (var memoryStream = new MemoryStream())
{
using (var archive = new ZipArchive(memoryStream, ZipArchiveMode.Create, true))
{
foreach (var attachment in attachments)
{
ZipArchiveEntry entry = archive.CreateEntry(attachment.FileName);
using (Stream ZipFile = entry.Open())
{
byte[] data = attachment.Data;
ZipFile.Write(data, 0, data.Length);
}
}
}
Response.Clear();
Response.ClearContent();
Response.ClearHeaders();
Response.ContentType = "application/x-compressed";
Response.AddHeader("Content-Disposition", string.Format("attachment;filename=Photos-{0}.zip; size={1}", DateTime.Now.ToString("yyyyMMdd-HHmm"), memoryStream.Length));
Response.BinaryWrite(memoryStream.ToArray());
Response.Flush();
Response.End();
return null;
}
}
当我点击链接时,代码会运行,但没有任何内容被发送到浏览器,我认为会在Response.BinaryWrite行上发生。我做错了什么?
答案 0 :(得分:1)
使用MVC,您必须返回ActionResult
进行回复。直接调整http响应不会利用MVC框架。
使用FileResult
。在控制器内部,您可以使用File
方法来处理此问题。
memoryStream.Position = 0;
return File(memoryStream,
System.Net.Mime.MediaTypeNames.Application.Zip,
string.Format("Photos-{0:yyyyMMdd-HHmm}.zip", DateTime.Now));
但我怀疑这个错误存在于用于生成zip的代码中。尝试将ZipArchive
保存到磁盘以进行检查。
否则,在项目中添加ZipFileResult
类,让它为您处理压缩。
这是我使用的类(使用MVC 5):
using System.IO.Compression;
using System.Web;
using System.Web.Mvc;
using System;
using System.Linq;
using System.IO;
using System.Net.Mime;
namespace Whatever
{
public class ZipFileResult : FileResult
{
/// <summary>
/// Folder inside zip which will contains the files.
/// (<c>FileDownloadName</c> without its extension will be used
/// by default if there is more than one file in the zip.
/// </summary>
/// <value>
/// <c>string.Empty</c> for not having a folder inside the zip.
/// <c>null</c> for using <c>FileDownloadName</c> without its extension
/// if there is more than one file in the zip.
/// </value>
/// <remarks>If <c>FileDownloadName</c> is <c>null</c> or empty, the
/// name "files" will be used instead.</remarks>
public string ZipFolder { get; set; }
private readonly ZipFileResultEntry[] _files;
public ZipFileResult(params ZipFileResultEntry[] files)
: base(MediaTypeNames.Application.Zip)
{
_files = files;
}
public ZipFileResult(params string[] filesPaths)
: this(filesPaths == null ? null :
filesPaths.Select(fp => ZipFileResultEntry
.Create(Path.GetFileName(fp), fp)).ToArray())
{
}
protected override void WriteFile(HttpResponseBase response)
{
// By default, response is fully buffered in memory and sent
// once completed. On big zipped content, this would cause troubles.
// If un-buffering response is required (<c>response.BufferOutput =
// false;</c>), beware, it may emit very small packets,
// causing download time to dramatically raise. To avoid this,
// it would then required to use a BufferedStream with a
// reasonnable buffer size (256kb for instance).
// http://stackoverflow.com/q/26010915/1178314
// The BufferedStream should encapsulate response.OutputStream.
// PositionWrapperStream must then Dispose it (current
// implementation will not), so long for this causing OutputStream
// to get closed (BufferedStream do not have any option for
// telling it not to close its underlying stream, and it is
// sealed...).
using (var outputStream =
new PositionWrapperStream(response.OutputStream))
using (var zip = new ZipArchive(outputStream,
ZipArchiveMode.Create, true))
{
if (_files != null)
{
var archiveDir = ZipFolder ??
(_files.Length <= 1 ? string.Empty :
string.IsNullOrEmpty(FileDownloadName) ?
"files" :
Path.ChangeExtension(FileDownloadName, null));
foreach (var file in _files)
{
if (file == null)
continue;
file.WriteEntry(zip, archiveDir);
}
}
}
}
// Workaround bug ZipArchive requiring Position while creating.
// Taken from http://stackoverflow.com/a/21513194/1178314
class PositionWrapperStream : Stream
{
private Stream _wrapped;
private int _pos = 0;
public PositionWrapperStream(Stream wrapped)
{
_wrapped = wrapped;
}
public override bool CanSeek { get { return false; } }
public override bool CanWrite { get { return true; } }
public override bool CanRead { get { return false; } }
public override long Position
{
get { return _pos; }
set { throw new NotSupportedException(); }
}
public override long Length { get { return _pos; } }
public override void Write(byte[] buffer, int offset, int count)
{
_pos += count;
_wrapped.Write(buffer, offset, count);
}
public override void Flush()
{
_wrapped.Flush();
}
protected override void Dispose(bool disposing)
{
// Fcd : not closing _wrapped ourselves, MVC handle that.
_wrapped = null;
base.Dispose(disposing);
}
// all the other required methods can throw NotSupportedException
public override int Read(byte[] buffer, int offset, int count)
{
throw new NotSupportedException();
}
public override void SetLength(long value)
{
throw new NotSupportedException();
}
public override long Seek(long offset, SeekOrigin origin)
{
throw new NotSupportedException();
}
}
}
public abstract class ZipFileResultEntry
{
/// <summary>
/// Filename to use inside the zip.
/// </summary>
public string Filename { get; private set; }
internal ZipFileResultEntry(string filename)
{
Filename = filename;
}
internal abstract void WriteEntry(ZipArchive zip, string directory);
/// <summary>
/// Create a file to zip in response from an uncompressed file.
/// </summary>
/// <param name="filename">Filename to use inside the zip.</param>
/// <param name="path">Full path to uncompressed file on
/// server.</param>
public static ZipFileResultEntry Create(string filename, string path)
{
return new FileSystemEntry(filename, path);
}
/// <summary>
/// Create a text file to zip in response using a callback.
/// </summary>
/// <param name="filename">Filename to use inside the zip.</param>
/// <param name="writer">Callback responsible of writing
/// uncompressed file content in zip stream.</param>
public static ZipFileResultEntry CreateText(string filename,
Action<StreamWriter> writer)
{
return new TextCallbackEntry(filename, writer);
}
private class FileSystemEntry : ZipFileResultEntry
{
private readonly string SystemPath;
public FileSystemEntry(string filename, string path)
: base(filename)
{
SystemPath = path;
}
internal override void WriteEntry(ZipArchive zip, string directory)
{
zip.CreateEntryFromFile(SystemPath,
Path.Combine(directory, Filename));
}
}
private class TextCallbackEntry : ZipFileResultEntry
{
private readonly Action<StreamWriter> Writer;
public TextCallbackEntry(string filename,
Action<StreamWriter> writer)
: base(filename)
{
if (writer == null)
throw new ArgumentNullException("writer");
Writer = writer;
}
internal override void WriteEntry(ZipArchive zip, string directory)
{
var entry = zip.CreateEntry(Path.Combine(directory, Filename));
using (var sw = new StreamWriter(entry.Open()))
{
Writer(sw);
}
}
}
}
}
然后你可以重写你的行动:
public ActionResult Download(List<int> photoIds)
{
var attachments = new List<string>();
foreach (int photoId in photoIds)
{
var Photo = db.Photos.Find(photoId);
attachments.Add(Server.MapPath(Photo.imageUrl));
}
return new ZipFileResult(attachments.ToArray())
{
FileDownloadName = string.Format("Photos-{0:yyyyMMdd-HHmm}.zip",
DateTime.Now)
};
}
附注:出于性能原因,应避免在循环中调用db。如果这是一个EF上下文,最好将其更改为(假设Photo具有Id
属性作为主键:
public ActionResult Download(int[] photoIds)
{
var attachments = new List<string>();
foreach (var photo in db.Photos.Where(p => photoIds.Contains(p.Id)))
{
attachments.Add(Server.MapPath(photo.imageUrl));
}
return new ZipFileResult(attachments.ToArray())
{
FileDownloadName = string.Format("Photos-{0:yyyyMMdd-HHmm}.zip",
DateTime.Now)
};
}