This post will focus on a situation I encountered recently, and that is how to create a zip archive on demand to be served via an API endpoint. This is a nice exercise in using tuples and working with compression API for creating a zip file on demand and returning it to the API caller.
In a following post, we will see how to receive a file from an AJAX call since there’s some trickery involved there as well.
The Scenario
Say we either have some local files on the server, or in my case, in private Azure Blob storage, or anywhere we can get the file bytes from (like a third-party service) and we want to bundle these items within a zip archive.
To achieve this, we need to do the following steps:
- Get the bytes from the files.
- Archive them in a zip file.
- Return the response for the zip file.
1. Getting the Bytes From a File
This part should be very simple, for this example, we will use an enumerator to go over local files and get their filenames and bytes. A dictionary in this case would do just fine, but we could use anything we wish to store the data, from custom data structures to just plain tuples.
Dictionary<string, byte[]> filesToArchive = new Dictionary<string, byte[]>();
foreach (string file in Directory.GetFiles("G:\\"))
{
filesToArchive[Path.GetFileName(file)] = await File.ReadAllBytesAsync(file);
}
One thing to take note of about choosing the dictionary approach is that if you do this for nested folder, you might end up with the same key for differently nested files.
2. Archiving the Files Into a Zip File
For us to be able to create an archive, we will be using the namespace System.IO.Compression
.
The following code illustrates how to create an archive and save it to the file system.
using System.IO.Compression;
Dictionary<string, byte[]> filesToArchive = new();
foreach (string file in Directory.GetFiles("G:\\"))
{
filesToArchive[Path.GetFileName(file)] = await File.ReadAllBytesAsync(file);
}
using (MemoryStream archiveStream = new MemoryStream())
{
using (ZipArchive zipArchive =
new ZipArchive(archiveStream, ZipArchiveMode.Create, leaveOpen: false))
{
foreach (var (fileName, fileBytes) in filesToArchive)
{
ZipArchiveEntry zipEntry = zipArchive.CreateEntry(fileName);
using (MemoryStream fileStream = new MemoryStream(fileBytes))
using (Stream zipEntryStream = zipEntry.Open())
{
await fileStream.CopyToAsync(zipEntryStream);
}
}
}
await File.WriteAllBytesAsync("G:\\testArchive.zip", archiveStream.ToArray());
}
As with the previous code snippet, I want to make a note that in a Web API scenario, we don’t want the archive to be saved to the disk since we want it to be returned for a HTTP call. Because of this, we will instead return the archive file name and the byte array to the controller action so that we can serve it to the API caller.
Here’s an example of how that would look like:
public async Task<(string archiveName, byte[] archiveBytes)> GetArchive()
{
Dictionary<string, byte[]> filesToArchive = new();
foreach (string file in Directory.GetFiles("G:\\"))
{
filesToArchive[Path.GetFileName(file)] = await File.ReadAllBytesAsync(file);
}
using (MemoryStream archiveStream = new MemoryStream())
{
using (ZipArchive zipArchive =
new ZipArchive(archiveStream, ZipArchiveMode.Create, leaveOpen: false))
{
foreach (var (fileName, fileBytes) in filesToArchive)
{
ZipArchiveEntry zipEntry = zipArchive.CreateEntry(fileName);
using (MemoryStream fileStream = new MemoryStream(fileBytes))
using (Stream zipEntryStream = zipEntry.Open())
{
await fileStream.CopyToAsync(zipEntryStream);
}
}
}
return ("testArchive.zip", archiveStream.ToArray());
}
}
As you can see, here I opted to return a tuple of the file name and the archive bytes so that they can be used in the next step. Of course, you can opt to create your container data structure like a class, but for a small limited use case, a tuple fits the bill perfectly since it’s not being used anywhere else.
3. Return the Response for the Zip File
So now that we created the archive, let’s have a look at our controller action that returns the file response for our archive.
[HttpGet("getArchive")]
public async Task<IActionResult> GetArchiveAsync(Guid id)
{
try
{
var (fileName, archiveBytes) = await GetArchive();
return File(archiveBytes, "application/zip", fileName);
}
catch (Exception e)
{
}
}
First, we get the archive name and bytes and then we return a File response with the archive bytes, the MIME type of application/zip and the archive name; and if that fails, we would normally return a 400 response to let the user know of what went wrong.
Conclusion
That’s all there is to it, in short, get the bytes, archive them, return the file response. But this has been a nice exercise in using tuples and working with the compression API for creating a zip file on demand and returning it to the API caller.
Of course, based on this, we could do a bit more like:
- Query for specific files to be included or excluded from the archive
- check if the user has the necessary rights to retrieve the archive
We could even encrypt and use a password if we threw in NuGet packages in the mix since C# by default doesn’t have an API for creating encrypted archives; two of them are:
And of course, if you’re using local files, you could even skip the part about reading the file bytes to archive them, but I prefer this approach since it’s more generalized and I don’t have to depend on the underlying file system.
As mentioned at the start of the post, in the next one, we will be looking at how to use this endpoint with AJAX.