Skip to Content
Course content

198: Working with ZIP Archives

Click on the "Edit" button in the top corner of the screen to edit your slide content.

A few years ago, I worked with a developer who was building a telemetry exporter for a high-traffic microservice. He was trying to upload thousands of small JSON log files to an S3 bucket every hour. At first, he just uploaded them individually, but he quickly hit two walls: the API request costs were skyrocketing, and the upload process was agonizingly slow due to the overhead of thousands of individual HTTP requests. He came to me frustrated because the network latency was killing his performance. The fix was simple—bundle those files into a ZIP archive locally before shipping them. It turned a ten-minute upload process into a ten-second one.

Bundling Files with zip.Writer

In Go, the archive/zip package is your primary tool here. To create an archive, you don't just "save" a file; you create a zip.Writer that wraps an existing io.Writer (usually an os.File). Think of the zip.Writer as a stream that formats your data into the ZIP specification as you feed it files.

The trickiest part for most people is the workflow of adding a file. You have to explicitly create a "header" for each file you want to add using writer.Create("filename.txt"). This method returns a new io.Writer. Anything you write to that returned writer gets compressed and tucked into the archive under that filename. I've seen plenty of bugs where developers forget to call Close() on the zip.Writer—if you do that, the ZIP's central directory won't be written, and the resulting file will be corrupted and unreadable.

package main

import (
	"archive/zip"
	"io"
	"os"
)

func createArchive(outFile string, files []string) error {
	// Create the physical file on disk
	f, err := os.Create(outFile)
	if err != nil {
		return err
	}
	defer f.Close()

	// Wrap the file in a zip writer
	zw := zip.NewWriter(f)
	defer zw.Close() // Crucial! This writes the ZIP central directory.

	for _, file := range files {
		// Create a entry in the zip
		w, err := zw.Create(file)
		if err != nil {
			return err
		}

		// Open the actual file to be compressed
		content, err := os.Open(file)
		if err != nil {
			return err
		}
		defer content.Close()

		// Stream the file content into the zip entry
		if _, err := io.Copy(w, content); err != nil {
			return err
		}
	}
	return nil
}

Unpacking and the Zip Slip Danger

Reading a ZIP file is a bit different. You use zip.OpenReader, which gives you a *zip.ReadCloser. You can then iterate through the files in the archive using a simple range loop. However, there is a serious security concern you need to be aware of called "Zip Slip."

If you blindly trust the filename inside a ZIP archive, a malicious actor could name a file ../../etc/passwd. If your code simply joins that filename to your destination directory, you might accidentally overwrite critical system files. I always recommend cleaning the destination path using filepath.Join and then checking if the resulting absolute path still resides within your intended target directory. Don't skip this check if you're handling files uploaded by users.

package main

import (
	"archive/zip"
	"fmt"
	"io"
	"os"
	"path/filepath"
	"strings"
)

func unzip(src string, dest string) error {
	r, err := zip.OpenReader(src)
	if err != nil {
		return err
	}
	defer r.Close()

	for _, f := range r.File {
		// Prevent Zip Slip: Ensure the file stays within the dest folder
		fpath := filepath.Join(dest, f.Name)
		if !strings.HasPrefix(fpath, filepath.Clean(dest)+string(os.PathSeparator)) {
			return fmt.Errorf("illegal file path: %s", fpath)
		}

		if f.FileInfo().IsDir() {
			os.MkdirAll(fpath, os.ModePerm)
			continue
		}

		if err := os.MkdirAll(filepath.Dir(fpath), os.ModePerm); err != nil {
			return err
		}

		rc, err := f.Open()
		if err != nil {
			return err
		}

		outFile, err := os.OpenFile(fpath, os.O_WRONLY|os.O_CREATE|os.O_TRUNC, f.Mode())
		if err != nil {
			rc.Close()
			return err
		}

		_, err = io.Copy(outFile, rc)
		outFile.Close()
		rc.Close()

		if err != nil {
			return err
		}
	}
	return nil
}



📋 Practical Task

Build a Configuration Bundle Tool

You need to create a utility that helps your team package environment-specific configurations for deployment. Build a program that does the following:

  • Scans a directory named ./configs for any files ending in .yaml or .json.
  • Bundles all matching files into a single archive named deployment_bundle.zip.
  • Includes a generated file inside the ZIP called manifest.txt that lists the names and sizes of all files included in the bundle.
  • Ensures that the zip.Writer is properly closed to avoid archive corruption.

Test your tool by creating a few dummy YAML files in a ./configs folder and verifying that the resulting ZIP contains both your config files and the generated manifest.

Rating
0 0

There are no comments for now.

to be the first to leave a comment.