使用MemoryStream和ZipArchive在asp.net web api中返回zip文件给客户端

12

我正在尝试使用以下代码从asp.net web api向客户端返回zip文件:

private byte[] CreateZip(string data)
{
    using (var ms = new MemoryStream())
    {
        using (var ar = new ZipArchive(ms, ZipArchiveMode.Create, true))
        {
            var file = archive.CreateEntry("file.html");

            using (var entryStream = file.Open())
            using (var sw = new StreamWriter(entryStream))
            {
                sw .Write(value);
            }
        }
        return memoryStream.ToArray();
    }
}

public HttpResponseMessage Post([FromBody] string data)
{
    HttpResponseMessage result = new HttpResponseMessage(HttpStatusCode.OK);
    result.Content = new ByteArrayContent(CreateZip(data));
    result.Content.Headers.ContentType = new MediaTypeHeaderValue("application/zip, application/octet-stream");
    return result;
}

运行此代码时,我遇到了以下错误:

ExceptionMessage":"值“application/zip,application/octet-stream”的格式无效。"

这是JS代码:

$.ajax({
  type: "POST",
  url: url,
  data: data,
  dataType: application/x-www-form-urlencoded
});
任何解释为什么会发生这种情况?真的非常感激你们的帮助。

展示你的 JavaScript 代码。你尝试过使用 "return ResponseMessage(result)" 吗? - Ricardo Pontual
@RicardoPontual请查看更新。我没有使用ResponseMessage(result)。 - Kob_24
@RicardoPontual 我应该使用它来代替result.Content.Headers.ContentType吗? - Kob_24
你的 API 代码正常工作吗?你尝试过在磁盘上保存文件进行测试吗?我只是想确保问题出在 JavaScript 上,而不是 API。 - user3378165
我尝试了一下从控制台应用程序中保存,而没有使用内存流,它确实是本地保存的。 - Kob_24
5个回答

10
这是我的解决方案,适用于我:
C#部分:
public IActionResult GetZip([FromBody] List<DocumentAndSourceDto> documents)
{
    List<Document> listOfDocuments = new List<Document>();

    foreach (DocumentAndSourceDto doc in documents)
        listOfDocuments.Add(_documentService.GetDocumentWithServerPath(doc.Id));

    using (var ms = new MemoryStream())
    {
        using (var zipArchive = new ZipArchive(ms, ZipArchiveMode.Create, true))
        {
            foreach (var attachment in listOfDocuments)
            {
                var entry = zipArchive.CreateEntry(attachment.FileName);

                using (var fileStream = new FileStream(attachment.FilePath, FileMode.Open))
                using (var entryStream = entry.Open())
                {
                    fileStream.CopyTo(entryStream);
                }
            }

        }
        ms.Position = 0;
        return File(ms.ToArray(), "application/zip");
    }

    throw new ErrorException("Can't zip files");
}

不要错过这里的 ms.Position = 0;

前端 (Angular 4):

downloadZip(datas: any) {
    const headers = new Headers({
        'Content-Type': 'application/json',
        'Accept': 'application/zip'
    });

    const options = new RequestOptions({ headers: headers, withCredentials: true, responseType: ResponseContentType.ArrayBuffer });
    const body = JSON.stringify(datas);
    return this.authHttp.post(`${environment.apiBaseUrl}api/documents/zip`, body, options)
        .map((response: Response) => {
            const blob = new Blob([response.blob()], { type: 'application/zip' });
            FileSaver.saveAs(blob, 'logs.zip');
        })
        .catch(this.handleError);
}

现在我可以将多个文件下载为zip压缩包。

不需要调用ms.Position = 0ToArray()会将字节流写入一个数组,不论其位置在哪里。 - Scott Clark
@ScottClark没有他,我的代码无法按预期工作:/ - Fitch
@Fitch 下载会立即开始吗?还是我们必须等到所有文件都被压缩后才能开始下载? - Shyamal Parikh
@ShyamalParikh,它会等待所有文件压缩后才开始下载。 - Fitch
2
C#部分完成了,我的永恒痛苦(那个复制流的部分)。谢谢。 - Ademar
1
我将流设置为响应内容,但最终在客户端上得到了空的zip文件。最后添加“ms.Position = 0”解决了我的问题。 - Bhavesh

10

$.ajax处理文本响应,并尝试(utf-8)解码内容:如果您的zip文件不是文本,则会得到损坏的内容。jQuery不支持二进制内容,因此您需要使用此链接并在jQuery上添加一个ajax传输或直接使用XmlHttpRequest。使用xhr时,您需要设置xhr.responseType = "blob"并从xhr.response中读取blob。

// with xhr.responseType = "arraybuffer"
var arraybuffer = xhr.response;
var blob = new Blob([arraybuffer], {type:"application/zip"});
saveAs(blob, "example.zip");

// with xhr.responseType = "blob"
var blob = xhr.response;
saveAs(blob, "example.zip");
Edit: examples:

使用jquery.binarytransport.js(任何可让您下载Blob或ArrayBuffer的库均可)


$.ajax({
  url: url,
  type: "POST",
  contentType: "application/json",
  dataType: "binary", // to use the binary transport
  // responseType:'blob', this is the default
  data: data,
  processData: false,
  success: function (blob) {
    // the result is a blob, we can trigger the download directly
    saveAs(blob, "example.zip");
  }
  // [...]
});

使用原始的XMLHttpRequest,你可以查看这个问题,只需要添加xhr.responseType = "blob"就可以获得一个 Blob。

我个人建议你使用 jQuery 上的 AJAX 传输,非常简单,你只需要下载一个库,将其包含在项目中,并写上:dataType: "binary"

这是使用 DotNetZip(Ionic.Zip)的 API 代码:

   [HttpPost]
    public HttpResponseMessage ZipDocs([FromBody] string[] docs)
    {
        using (ZipFile zip = new ZipFile())
        {
            //this code takes an array of documents' paths and Zip them
            zip.AddFiles(docs, false, "");
            return ZipContentResult(zip);
        }
    }

    protected HttpResponseMessage ZipContentResult(ZipFile zipFile)
    {
        var pushStreamContent = new PushStreamContent((stream, content, context) =>
        {
          zipFile.Save(stream);
            stream.Close(); 
        }, "application/zip");

        return new HttpResponseMessage(HttpStatusCode.OK) { Content = pushStreamContent };
    }

我尝试了两种方法,但都不起作用 :/ 我不再收到错误信息,但文件没有被下载。 - Kob_24
我在后端部分没有看到任何问题,你呢?http://postimg.org/image/3m861h77f/ - Kob_24
你有检查开发者工具(F12)上的控制台吗?有任何错误吗?我个人使用了DotNetZip库而不是ZipArchive。 - user3378165

2

这适用于asp.net core版本。

    [HttpGet("api/DownloadZip")]
    public async Task<IActionResult> Download()
    {
        var path = "C:\\test.zip";
        var memory = new MemoryStream();
        using (var stream = new FileStream(path, FileMode.Open))
        {
            await stream.CopyToAsync(memory);
        }

        memory.Position = 0;
        return File(memory, GetContentType(path), Path.GetFileName(path));
    }

然后使用Web客户端调用

      class Program
    {

        static string url = "http://localhost:5000/api/DownloadZip";

        static async Task Main(string[] args)
        {
            var p = @"c:\temp1\test.zip";

            WebClient webClient = new WebClient();

            webClient.DownloadFile(new Uri(url), p);                       

            Console.WriteLine("ENTER to exit...");
            Console.ReadLine();
        }
    }

0
您传递到MediaTypeHeaderValue构造函数中的值格式无效。 您还尝试将多个内容类型添加到标题值中。
内容类型标头接受单个类型/子类型,后跟使用半分号;分隔的可选参数。
例如:
Content-Type: text/html; charset=ISO-8859-4

对于您的结果,您需要决定要使用哪一个。application/zipapplication/octet-stream

result.Content.Headers.ContentType = new MediaTypeHeaderValue("application/zip");

此外,为了避免异常,您可以使用MediaTypeHeaderValue.TryParse方法。
var contentTypeString = "application/zip";
MediaTypeHeaderValue contentType = null;
if(MediaTypeHeaderValue.TryParse(contentTypeString, out contentType)) {
    result.Content.Headers.ContentType = contentType;
}

您不能将 null 赋值给隐式类型变量 var contentType = null,这是错误的。 - Kob_24
@Kob_24,那是我的笔误。我已经修正了它。 - Nkosi

0

如果您希望将其作为可下载文件返回,请尝试这样做。返回类型可以是IActionResult。

 var theJson = JsonConvert.SerializeObject(result);
            using (var zipStream = new MemoryStream())
            {
                using (var zipArchive = new ZipArchive(zipStream, ZipArchiveMode.Create, leaveOpen: true))
                {
                    var entry = zipArchive.CreateEntry("data.json");
                    using (var entryStream = entry.Open())
                    using (var jsonStreamWriter = new StreamWriter(entryStream))
                    {
                        jsonStreamWriter.Write(theJson);
                    }
                }
                // Reset zip stream to point at the beginning of data
                zipStream.Seek(0, SeekOrigin.Begin);
                // Set the response headers
                //Response.Headers.Add("Content-Disposition", "attachment; filename=compressed_response.zip");
                // Return zip file containing JSON data as response
                return File(zipStream.ToArray(), "application/zip");
            }

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接