Generate Zip File from a String List of Files in Optimizely CMS

Generate Zip File from a String List of Files in Optimizely CMS

In this blog post, we will show you how can you use a string List of Files Ids to generate a unique zip file on the fly so the users will be able to download several files at once. So lets begin!

First, we will show you how it will work in the view. It only requires to set an anchor link with all the files content reference ids concatenated in a string. The files variable is a list of integers containing the ids and the anchor link points to the Api controller.

                         @{
                                var url = "#";
                                if (files.Any())
                                {
                                    url = "/filesZip/downloadfiles?files=" + string.Join(",", files);
                                }
                            }

                            @if (url != "#")
                            {
                                <a href="@url" download title="Download files" style="text-decoration: none;">
                                    Download all
                                </a>
                            }
                            else
                            {
                                Download all
                            }

Second, we will create the Api controller called FilesZipController. In this class we will use the language repository and content loader interfaces to get the content items and generate the zip file. We will begin initializing the needed variables using the constructor.

public class FilesZipController : Controller
    {
        #region Properties

        protected readonly ILanguageBranchRepository LanguageRepository;
        protected readonly IContentLoader ContentLoader;

        #endregion

        #region Constructor

        public FilesZipController(ILanguageBranchRepository languageRepository,
            IContentLoader contentLoader)
        {
            LanguageRepository = languageRepository;
            ContentLoader = contentLoader;
        }

We will now focus on the download files method which will be responsible to generate the zip file using some helper methods along the way. Pay special attention to the comments in the code for more details.

        public ActionResult DownloadFiles(string files)
        {
            if (string.IsNullOrWhiteSpace(files)) // No files, return null
            {
                return null;
            }

            var sourceFiles = new List<SourceFile>();

            foreach (var file in files.Split(','))
            {
                var content = GetFromId(new ContentReference(file)); // Get file using the content reference id
                if (content != null)
                {
                    sourceFiles.Add(content);
                }
            }

            // Check if we found at least one file in the CMS
            if (!sourceFiles.Any())
            {
                return null;
            }

            // Create a bytes variable and start working on the zip file
            byte[] fileBytes;
            using (var memoryStream = new MemoryStream())
            {
                using (var zip = new ZipArchive(memoryStream, ZipArchiveMode.Create, true))
                {
                    // iterate through the source files
                    foreach (var source in sourceFiles)
                    {
                        // Add the item name to the zip
                        var zipItem = zip.CreateEntry(source.Name, CompressionLevel.Fastest);

                        // Add the item bytes to the zip entry by opening the original file and copying the bytes 
                        using (var originalFileMemoryStream = new MemoryStream(source.FileBytes))
                        {
                            using (var entryStream = zipItem.Open())
                            {
                                originalFileMemoryStream.CopyTo(entryStream);
                            }
                        }
                    }
                }

                // Reset the stream to the beginning
                memoryStream.Position = 0;

                // Save the bytes to the fileBytes variable
                fileBytes = memoryStream.ToArray();
            }

            // Add corresponding header to allow the download
            Response.AddHeader("Content-Disposition", "attachment; filename=documents.zip");

            // Download the constructed zip
            return File(fileBytes, "application/zip");
        }

One of the helper methods is the GetFromId method, which uses the content loader and the content reference id of the file to get the document, it also checks if the user has permissions to get the file (always true for the moment), that the file is binary stored and finally it reads all the bytes of the file and returns a SourceFile class with the name, bytes and extension data.

 private SourceFile GetFromId(ContentReference contentReference)
        {
            if (contentReference == null)
            {
                return null;
            }

            var content = ContentLoader.Get<IContent>(contentReference);

            if (content == null)
            {
                return null;
            }

            var document = ContentLoader.Get<MediaData>(contentReference);
            if (document == null)
            {
                return null;
            }

            var hasPermission = true; // Add permission logic if needed
            if (!hasPermission)
            {
                return null;
            }

            if (!(content is IBinaryStorable binaryStored))
            {
                return null;
            }

            byte[] sourceFileBytes;

            try
            {
                using (var input = binaryStored.BinaryData.OpenRead())
                {
                    sourceFileBytes = ReadFully(input);
                }
            }
            catch (Exception)
            {
                return null;
            }

            var sourceFile = GetSourceFileFromBlobData(document.Name, sourceFileBytes, binaryStored);
            return sourceFile;
        }

At the same time the method above uses to additional methods. The ReadFully method which loads the binary stored stream and returns the bytes of the file.

   private static byte[] ReadFully(Stream input)
        {
            var buffer = new byte[16 * 1024];
            using (var ms = new MemoryStream())
            {
                int read;
                while ((read = input.Read(buffer, 0, buffer.Length)) > 0)
                {
                    ms.Write(buffer, 0, read);
                }
                return ms.ToArray();
            }
        }

And the method GetSourceFileFromBlobData which will generate the SourceFile class using the bytes, the name of the file and the binary stored. The binary stored is specially useful to get the file extension which is added to the SourceFile class extension attribute and as part of the name attribute.

 private SourceFile GetSourceFileFromBlobData(string name, byte[] sourceFileBytes, IBinaryStorable binaryStored)
        {
            var sourceFile = new SourceFile
            {
                Name = name,
                FileBytes = sourceFileBytes
            };

            var blobData = binaryStored.BinaryData.ToString();
            var idx = blobData.LastIndexOf("/", StringComparison.Ordinal);

            if (idx != -1)
            {
                var ext = blobData.Substring(idx + 1);
                var extIdx = ext.LastIndexOf(".", StringComparison.Ordinal);
                if (extIdx != -1)
                {
                    sourceFile.Extension = ext.Substring(extIdx + 1);
                    if (!sourceFile.Name.Substring(sourceFile.Name.Length - 5).Contains("."))
                    {
                        sourceFile.Name = sourceFile.Name + "." + sourceFile.Extension;
                    }
                }
            }

            return sourceFile;
        }

The SourceFile class implementation is the following:

 public class SourceFile
    {
        public string Name { get; set; }
        public string Extension { get; set; }
        public Byte[] FileBytes { get; set; }
    }

The final code will looks like this:

    public class FilesZipController : Controller
    {
        #region Properties

        protected readonly ILanguageBranchRepository LanguageRepository;
        protected readonly IContentLoader ContentLoader;

        #endregion

        #region Constructor

        public FilesZipController(ILanguageBranchRepository languageRepository,
            IContentLoader contentLoader)
        {
            LanguageRepository = languageRepository;
            ContentLoader = contentLoader;
        }

        #endregion

        public ActionResult DownloadFiles(string files)
        {
            if (string.IsNullOrWhiteSpace(files)) // No files, return null
            {
                return null;
            }

            var sourceFiles = new List<SourceFile>();

            foreach (var file in files.Split(','))
            {
                var content = GetFromId(new ContentReference(file)); // Get file using the content reference id
                if (content != null)
                {
                    sourceFiles.Add(content);
                }
            }

            // Check if we found at least one file in the CMS
            if (!sourceFiles.Any())
            {
                return null;
            }

            // Create a bytes variable and start working on the zip file
            byte[] fileBytes;
            using (var memoryStream = new MemoryStream())
            {
                using (var zip = new ZipArchive(memoryStream, ZipArchiveMode.Create, true))
                {
                    // iterate through the source files
                    foreach (var source in sourceFiles)
                    {
                        // Add the item name to the zip
                        var zipItem = zip.CreateEntry(source.Name, CompressionLevel.Fastest);

                        // Add the item bytes to the zip entry by opening the original file and copying the bytes 
                        using (var originalFileMemoryStream = new MemoryStream(source.FileBytes))
                        {
                            using (var entryStream = zipItem.Open())
                            {
                                originalFileMemoryStream.CopyTo(entryStream);
                            }
                        }
                    }
                }

                // Reset the stream to the beginning
                memoryStream.Position = 0;

                // Save the bytes to the fileBytes variable
                fileBytes = memoryStream.ToArray();
            }

            // Add corresponding header to allow the download
            Response.AddHeader("Content-Disposition", "attachment; filename=documents.zip");

            // Download the constructed zip
            return File(fileBytes, "application/zip");
        }

        private SourceFile GetFromId(ContentReference contentReference)
        {
            if (contentReference == null)
            {
                return null;
            }

            var content = ContentLoader.Get<IContent>(contentReference);

            if (content == null)
            {
                return null;
            }

            var document = ContentLoader.Get<MediaData>(contentReference);
            if (document == null)
            {
                return null;
            }

            var hasPermission = true; // Add permission logic if needed
            if (!hasPermission)
            {
                return null;
            }

            if (!(content is IBinaryStorable binaryStored))
            {
                return null;
            }

            byte[] sourceFileBytes;

            try
            {
                using (var input = binaryStored.BinaryData.OpenRead())
                {
                    sourceFileBytes = ReadFully(input);
                }
            }
            catch (Exception)
            {
                return null;
            }

            var sourceFile = GetSourceFileFromBlobData(document.Name, sourceFileBytes, binaryStored);
            return sourceFile;
        }

        private SourceFile GetSourceFileFromBlobData(string name, byte[] sourceFileBytes, IBinaryStorable binaryStored)
        {
            var sourceFile = new SourceFile
            {
                Name = name,
                FileBytes = sourceFileBytes
            };

            var blobData = binaryStored.BinaryData.ToString();
            var idx = blobData.LastIndexOf("/", StringComparison.Ordinal);

            if (idx != -1)
            {
                var ext = blobData.Substring(idx + 1);
                var extIdx = ext.LastIndexOf(".", StringComparison.Ordinal);
                if (extIdx != -1)
                {
                    sourceFile.Extension = ext.Substring(extIdx + 1);
                    if (!sourceFile.Name.Substring(sourceFile.Name.Length - 5).Contains("."))
                    {
                        sourceFile.Name = sourceFile.Name + "." + sourceFile.Extension;
                    }
                }
            }

            return sourceFile;
        }

        private static byte[] ReadFully(Stream input)
        {
            var buffer = new byte[16 * 1024];
            using (var ms = new MemoryStream())
            {
                int read;
                while ((read = input.Read(buffer, 0, buffer.Length)) > 0)
                {
                    ms.Write(buffer, 0, read);
                }
                return ms.ToArray();
            }
        }
    }

And that is it. Now, you can download a list of files ids as a zip file generate on the fly. If you have any question let me know. I hope it will help someone and as always keep learning !!!

Written by:

Jorge Cardenas

Developer with several years of experience who is passionate about technology and how to solve problems through it.

View All Posts

Leave a Reply