0
votes

I used OpenXML tools to create a byte array of the word and excel file and zip them using ZipArchive and return the filebyte.

httpResponseMessage = Request.CreateResponse(HttpStatusCode.OK);
httpResponseMessage.Content = new ByteArrayContent(zipFileBytes);
httpResponseMessage.Content.Headers.ContentDisposition = new System.Net.Http.Headers.ContentDispositionHeaderValue("attachment");
httpResponseMessage.Content.Headers.ContentType = new System.Net.Http.Headers.MediaTypeHeaderValue("application/zip");
httpResponseMessage.Content.Headers.ContentLength = zipFileBytes.Length;
httpResponseMessage.Content.Headers.Add("xfilename", zipFileName);
httpResponseMessage.StatusCode = HttpStatusCode.OK;
httpResponseMessage.Content.Headers.Add("Access-Control-Expose-Headers", "xfilename");

return httpResponseMessage;

The file can be download and open after unzip the zip file.

However, it cannot be review by window explorer or other unzip software. When trying to open the document in the window explorer, error message

"Windows cannot complete the extraction. The destination file could not be created"

Any ideas about how to solve this issue? Can the documents be review inside the zip which created by OpenXML?

UPDATE: I'm using "Open XML SDK 2.5 Productivity Tool" to generate the code. And the code below is the one generate the document. (For detail, please use the tool to generate the code, since it is toooooooo many line of them)

using DocumentFormat.OpenXml.Packaging;
using Ap = DocumentFormat.OpenXml.ExtendedProperties;
using DocumentFormat.OpenXml.Wordprocessing;
using DocumentFormat.OpenXml;
using M = DocumentFormat.OpenXml.Math;
using Ovml = DocumentFormat.OpenXml.Vml.Office;
using V = DocumentFormat.OpenXml.Vml;
using W15 = DocumentFormat.OpenXml.Office2013.Word;
using A = DocumentFormat.OpenXml.Drawing;
using Thm15 = DocumentFormat.OpenXml.Office2013.Theme;

namespace GeneratedCode
{
    public class GeneratedClass
    {
        // Creates a WordprocessingDocument.
    public void CreatePackage(DataModel dataModel, List<DataModel> dataList, out string filename, out Byte[] fileBytes)
    {
        filename = string.Empty;
        fileBytes = null;
        using (MemoryStream ms = new MemoryStream())
        {
            try
            {
                using (WordprocessingDocument package = WordprocessingDocument.Create(ms, WordprocessingDocumentType.Document))
                {
                    CreateParts(package, dataModel, dataList);
                }

                string extension = ".docx";
                filename = "TestDoc" + extension;
                fileBytes = ms.ToArray();
                ms.Close();

                return;
            }
            catch (System.Exception)
            {
                throw;
            }
        }
    }
}

Then, I generate the zip file using the code below and passing the list of array byte from the CreatePackage function.

public byte[] zipByteDocument(List<Tuple<Byte[], string>> fileBytes)
{
    // the output bytes of the zip
    byte[] zipFileBytes = null;

    // create a working memory stream
    using (System.IO.MemoryStream memoryStream = new System.IO.MemoryStream())
    {
        // create a zip
        using (System.IO.Compression.ZipArchive zip = new System.IO.Compression.ZipArchive(memoryStream, System.IO.Compression.ZipArchiveMode.Create, true))
        {
            // interate through the source files
            foreach (Tuple<Byte[], string> file in fileBytes)
            {
                // add the item name to the zip
                System.IO.Compression.ZipArchiveEntry zipItem = zip.CreateEntry(file.Item2);
                // add the item bytes to the zip entry by opening the original file and copying the bytes 
                using (System.IO.MemoryStream originalFileMemoryStream = new System.IO.MemoryStream(file.Item1))
                {
                    using (System.IO.Stream entryStream = zipItem.Open())
                    {
                        originalFileMemoryStream.CopyTo(entryStream);
                    }
                }
            }
        }
        zipFileBytes = memoryStream.ToArray();
    }
    return zipFileBytes;
}

And finally, I pass the zipFileBytes to the httpResponseMessage and it can be download. But fail to be preview without unzipping the zip file.

1
Can you please create a minimum reproducible example, for example, to help us understand how you create the ZipArchive with the Word and Excel file? What are the Open XML tools used to create a byte array of those Word and Excel files? - Thomas Barnekow
Thanks Thomas, I've update some more code for reproducing the issue. - Andrew Chan
Why do you want to put that WordprocessingDocument into a ZIP file? Are you aware that a WordprocessingDocument is a ZIP file already? Please try this: Take any .docx (or .xlsx or .pptx) and append a ".zip" to the extension, e.g., renaming Document.docx to Document.docx.zip. Then double-click and look at the ZIP archive. - Thomas Barnekow
Thanks Thomas, I just aware about this point (as I just though the WordprocessingDocument is XML at the beginning). And in this case, since I want to zip multiple document in a zip, that I putting them into a ZIP file. - Andrew Chan
I tested your zipByteDocument() method and it produces a ZIP file that can be opened. I can also open the Word file that I included in it (reading it using File.ReadAllBytes()). Thus, did you test other parts of your processing chain? For example, did you test whether your CreatePackage() creates a valid Word document? Did you test whether a byte array that you put into your HTTP response reaches its destination unchanged? - Thomas Barnekow

1 Answers

0
votes

I've created a few unit tests (see below), which demonstrate that there should not be an issue with the code you have shared (noting, however, that I have not simply replicated your code). There is a chance that the generated code or other code that you did not share is the culprit.

using System;
using System.Collections.Generic;
using System.IO;
using System.IO.Compression;
using System.Net;
using System.Net.Http;
using System.Net.Http.Headers;
using System.Threading.Tasks;
using CodeSnippets.IO;
using Xunit;

namespace CodeSnippets.Tests.IO.Compression
{
    public class ZipArchiveTests
    {
        private static byte[] CreateZipArchiveBytes(IEnumerable<(byte[], string)> files)
        {
            using MemoryStream stream = CreateZipArchiveStream(files);
            return stream.ToArray();
        }

        private static MemoryStream CreateZipArchiveStream(IEnumerable<(byte[], string)> files)
        {
            var stream = new MemoryStream();
            using (CreateZipArchive(stream, files))
                return stream;
        }

        private static ZipArchive CreateZipArchive(Stream stream, IEnumerable<(byte[], string)> files)
        {
            if (stream == null) throw new ArgumentNullException(nameof(stream));
            if (files == null) throw new ArgumentNullException(nameof(files));

            var archive = new ZipArchive(stream, ZipArchiveMode.Create, true);

            foreach ((byte[] fileContent, string fileName) in files)
            {
                ZipArchiveEntry archiveEntry = archive.CreateEntry(fileName);
                using Stream entryStream = archiveEntry.Open();
                entryStream.Write(fileContent, 0, fileContent.Length);
            }

            return archive;
        }

        private static ZipArchive ReadZipArchive(byte[] zipArchiveBytes)
        {
            return new ZipArchive(new MemoryStream(zipArchiveBytes), ZipArchiveMode.Read, false);
        }

        private static byte[] ReadEntryBytes(ZipArchive zipArchive, string entryName)
        {
            ZipArchiveEntry entry = zipArchive.GetEntry(entryName) ?? throw new Exception();
            var entryBytes = new byte[entry.Length];

            using Stream entryStream = entry.Open();
            entryStream.Read(entryBytes, 0, (int) entry.Length);
            return entryBytes;
        }

        private static HttpResponseMessage CreateResponseMessage(byte[] content, string fileName, string mediaType)
        {
            var message = new HttpResponseMessage(HttpStatusCode.OK)
            {
                Content = new ByteArrayContent(content)
            };

            message.Content.Headers.ContentDisposition = new ContentDispositionHeaderValue("attachment")
            {
                FileName = fileName
            };
            message.Content.Headers.ContentType = new MediaTypeHeaderValue(mediaType);
            message.Content.Headers.ContentLength = content.Length;

            return message;
        }

        [Fact]
        public async Task CreateResponseMessage_ZipArchiveBytes_Success()
        {
            // Arrange.
            const string path = "Resources\\ZipContents.docx";
            string fileName = Path.GetFileName(path);
            byte[] fileContent = File.ReadAllBytes(path);

            byte[] zipArchiveBytes = CreateZipArchiveBytes(new[]
            {
                (fileContent, fileName)
            });

            // Act.
            using HttpResponseMessage message = CreateResponseMessage(zipArchiveBytes, "ZipArchive.zip", "application/zip");
            HttpContent messageContent = message.Content;
            byte[] messageBytes = await messageContent.ReadAsByteArrayAsync();

            // Assert.
            // Original zipArchiveBytes and recevied messageBytes are equal.
            Assert.Equal(zipArchiveBytes, messageBytes);

            // Original file content and received ZIP archive content are equal.
            using ZipArchive zipArchive = ReadZipArchive(messageBytes);
            byte[] entryContent = ReadEntryBytes(zipArchive, fileName);

            Assert.Equal(fileContent.Length, entryContent.Length);
            Assert.Equal(fileContent, entryContent);
        }

        [Fact]
        public void CreateZipArchiveBytes_WordDocument_ZipFileSuccessfullyCreated()
        {
            // Arrange.
            const string path = "Resources\\ZipContents.docx";
            string fileName = Path.GetFileName(path);
            byte[] fileContent = File.ReadAllBytes(path);

            // Act.
            byte[] zipArchiveBytes = CreateZipArchiveBytes(new[]
            {
                (fileContent, fileName)
            });

            File.WriteAllBytes("ZipArchive_Bytes.zip", zipArchiveBytes);

            // Assert.
            using ZipArchive zipArchive = ReadZipArchive(zipArchiveBytes);
            byte[] entryContent = ReadEntryBytes(zipArchive, fileName);

            Assert.Equal(fileContent.Length, entryContent.Length);
            Assert.Equal(fileContent, entryContent);
        }
    }
}

Update 2019-12-06

I amended the unit tests to also demonstrate that this works with multiple documents. Here's the first one:

        [Fact]
        public void CreateZipArchiveBytes_Directory_ZipFileSuccessfullyCreated()
        {
            // Arrange, creating a ZIP archive with more than one entry.
            List<(byte[] fileContent, string fileName)> files = Directory
                .EnumerateFiles("Resources")
                .Select(path => (File.ReadAllBytes(path), Path.GetFileName(path)))
                .ToList();

            Assert.True(files.Count > 1);

            // Act.
            byte[] zipArchiveBytes = CreateZipArchiveBytes(files);
            File.WriteAllBytes("ZipArchive_Directory.zip", zipArchiveBytes);

            // Assert.
            using ZipArchive zipArchive = ReadZipArchive(zipArchiveBytes);

            Assert.Equal(files.Count, zipArchive.Entries.Count);

            foreach (ZipArchiveEntry entry in zipArchive.Entries)
            {
                byte[] fileContent = files
                    .Where(file => file.fileName == entry.Name)
                    .Select(file => file.fileContent)
                    .Single();

                using Stream entryStream = entry.Open();
                byte[] entryContent = entryStream.ToArray();

                Assert.Equal(fileContent, entryContent);
            }
        }

The next unit test demonstrates the same in conjunction with the HttpResponseMessage.

        [Fact]
        public async Task CreateResponseMessage_ZipArchiveDirectory_Success()
        {
            // Arrange, creating a ZIP archive with more than one entry.
            List<(byte[] fileContent, string fileName)> files = Directory
                .EnumerateFiles("Resources")
                .Select(path => (File.ReadAllBytes(path), Path.GetFileName(path)))
                .ToList();

            Assert.True(files.Count > 1);

            byte[] zipArchiveBytes = CreateZipArchiveBytes(files);

            // Act.
            using HttpResponseMessage message = CreateResponseMessage(zipArchiveBytes, "ZipArchive.zip", "application/zip");
            HttpContent messageContent = message.Content;
            byte[] messageBytes = await messageContent.ReadAsByteArrayAsync();

            // Assert.
            // Original zipArchiveBytes and recevied messageBytes are equal.
            Assert.Equal(zipArchiveBytes, messageBytes);

            // Original directory content and received ZIP archive content are equal.
            using ZipArchive zipArchive = ReadZipArchive(messageBytes);

            Assert.Equal(files.Count, zipArchive.Entries.Count);

            foreach (ZipArchiveEntry entry in zipArchive.Entries)
            {
                byte[] fileContent = files
                    .Where(file => file.fileName == entry.Name)
                    .Select(file => file.fileContent)
                    .Single();

                await using Stream entryStream = entry.Open();
                byte[] entryContent = await entryStream.ToArrayAsync();

                Assert.Equal(fileContent, entryContent);
            }
        }

The core methods have not changed.

The full source code can be found in my CodeSnippets GitHub repository.