Click here to Skip to main content
15,885,709 members
Articles / Programming Languages / C#

Creating Zips Server Side Per Request

Rate me:
Please Sign up or sign in to vote.
3.00/5 (1 vote)
21 Jul 2021CPOL3 min read 3.8K   2   4
Creating a zip archive on demand to be served via an API endpoint
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:

  1. Get the bytes from the files.
  2. Archive them in a zip file.
  3. 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.

C#
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.

C#
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:

C#
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.

C#
[HttpGet("getArchive")]
public async Task<IActionResult> GetArchiveAsync(Guid id)
{
	try
	{
		var (fileName, archiveBytes) = await GetArchive();

		return File(archiveBytes, "application/zip", fileName);
	}
	catch (Exception e)
	{
		// log your error and respond in kind to the user to let them know what happened.
	}
}

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.

License

This article, along with any associated source code and files, is licensed under The Code Project Open License (CPOL)


Written By
Software Developer
Romania Romania
When asked, I always see myself as a .Net Developer because of my affinity for the Microsoft platform, though I do pride myself by constantly learning new languages, paradigms, methodologies, and topics. I try to learn as much as I can from a wide breadth of topics from automation to mobile platforms, from gaming technologies to application security.

If there is one thing I wish to impart, that that is this "Always respect your craft, your tests and your QA"

Comments and Discussions

 
QuestionSource code Pin
Salam Y. ELIAS22-Jul-21 7:30
professionalSalam Y. ELIAS22-Jul-21 7:30 
AnswerRe: Source code Pin
Vlad Neculai Vizitiu22-Jul-21 8:13
Vlad Neculai Vizitiu22-Jul-21 8:13 
AnswerRe: Source code Pin
Vlad Neculai Vizitiu22-Jul-21 10:20
Vlad Neculai Vizitiu22-Jul-21 10:20 
GeneralRe: Source code Pin
Salam Y. ELIAS23-Jul-21 2:50
professionalSalam Y. ELIAS23-Jul-21 2:50 

General General    News News    Suggestion Suggestion    Question Question    Bug Bug    Answer Answer    Joke Joke    Praise Praise    Rant Rant    Admin Admin   

Use Ctrl+Left/Right to switch messages, Ctrl+Up/Down to switch threads, Ctrl+Shift+Left/Right to switch pages.