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 !!!
Leave a Reply