How to read and write a tar.gz archive
Short answer: Two crates, layered: tar for the archive format, flate2 for the gzip around it. Wrap a GzEncoder around your file and hand it to tar::Builder to write; wrap a GzDecoder in tar::Archive to read. Nothing loads the whole archive into memory — entries stream through as you go.
Writing one
Section titled “Writing one”Start from a fresh project — cargo new tar-demo — and declare both crates:
[dependencies]flate2 = "1.1.10"tar = "0.4.46"This builds a demo directory with two files and packs it into demo.tar.gz. The paths are relative, so the archive lands in the directory you run cargo run from:
use flate2::write::GzEncoder;use flate2::Compression;
fn main() -> Result<(), Box<dyn std::error::Error>> { // some files to pack; drop any leftover dir so a rerun packs exactly this let _ = std::fs::remove_dir_all("demo"); std::fs::create_dir_all("demo")?; std::fs::write("demo/notes.txt", b"hello from the archive")?; std::fs::write("demo/todo.txt", b"1. write the archive\n2. read it back")?;
let tar_gz = std::fs::File::create("demo.tar.gz")?; let encoder = GzEncoder::new(tar_gz, Compression::default()); let mut tar = tar::Builder::new(encoder);
tar.append_dir_all("demo", "demo")?; tar.into_inner()?.finish()?; // close the gzip stream, reporting errors println!("packed demo/ -> demo.tar.gz"); Ok(())}packed demo/ -> demo.tar.gzThe layering is the whole API. GzEncoder wraps the File, so the gzip stream goes straight to disk — no intermediate .tar is ever written. Builder writes tar entries into that encoder: append_dir_all("demo", "demo") walks the directory recursively, the first argument being the name entries get inside the archive, the second what to walk on disk. For a single file there is append_path; Compression::default() is gzip level 6.
The last line is not ceremony. into_inner() takes the encoder back out of the builder, and finish() writes the gzip trailer and returns the file. Dropping the encoder would close the stream too — but it ignores errors on the way out, so a full disk would look like success. finish() is the version that reports them.
Listing what is inside
Section titled “Listing what is inside”use flate2::read::GzDecoder;use std::fs::File;
fn main() -> Result<(), Box<dyn std::error::Error>> { let tar_gz = File::open("demo.tar.gz")?; let mut tar = tar::Archive::new(GzDecoder::new(tar_gz));
for entry in tar.entries()? { let entry = entry?; println!("{}", entry.path()?.display()); } Ok(())}demo/demo/notes.txtdemo/todo.txtArchive::new accepts any reader. Iterating entries() decompresses and parses one entry at a time, in archive order, so a 2 GB tar.gz costs no more memory than this one. entry.path() returns a path borrowed from the entry — a Cow<Path> — which is why display() is called in the same expression; keep the path past the next loop iteration and the borrow checker will point you at to_path_buf().
Reading one entry out
Section titled “Reading one entry out”use flate2::read::GzDecoder;use std::fs::File;use std::io::Read;use std::path::Path;
fn main() -> Result<(), Box<dyn std::error::Error>> { let tar_gz = File::open("demo.tar.gz")?; let mut tar = tar::Archive::new(GzDecoder::new(tar_gz));
for entry in tar.entries()? { let mut entry = entry?; if entry.path()? == Path::new("demo/notes.txt") { let mut contents = String::new(); entry.read_to_string(&mut contents)?; println!("{contents}"); } } Ok(())}hello from the archiveAn entry is itself a Read, so read_to_string streams just that one file out of the archive — no copy of the rest is touched. To write to disk instead, entry.unpack("out/notes.txt")? extracts a single entry, tar.unpack("out")? extracts everything, and for an archive from a source you don’t trust, entry.unpack_in("out") refuses paths that climb out of out with a leading ../.
Related
Section titled “Related”- How to walk a directory tree — what
append_dir_alldoes on the way in - How to copy or move a file — when one file is all you needed
- Reading and writing files — the rest of the chapter