Skip to content

Instantly share code, notes, and snippets.

@gekomad
Last active January 5, 2026 10:38
Show Gist options
  • Select an option

  • Save gekomad/209df0d6a532bb87d4a087dcc4ddf948 to your computer and use it in GitHub Desktop.

Select an option

Save gekomad/209df0d6a532bb87d4a087dcc4ddf948 to your computer and use it in GitHub Desktop.
Scala Zip with Cats and FS2
object ZipUtil {
import cats.effect.{IO, Resource}
import cats.implicits.*
import fs2.io.{readInputStream, writeOutputStream}
import java.io.{File, FileInputStream, FileOutputStream}
import java.util.zip.{ZipEntry, ZipOutputStream}
private val bufferSize = 8192
def zipFiles(sources: Set[String], dest: String): IO[String] =
zipOutputStreamResource(dest).use { zipOut =>
sources.map(new File(_)).toList.traverse_(addFileToZip(zipOut, ""))
}.as(dest)
def zipFile(source: String, dest: String): IO[String] =
zipFiles(Set(source), dest).as(dest)
private def zipOutputStreamResource(path: String): Resource[IO, ZipOutputStream] =
Resource
.make(IO(new FileOutputStream(path))) { fos =>
IO(fos.close())
}
.flatMap { fos =>
Resource.make(IO(new ZipOutputStream(fos))) { zos =>
IO(zos.close())
}
}
private def addFileToZip(zipOut: ZipOutputStream, parentPath: String)(file: File): IO[Unit] = {
val entryName = if (parentPath.isEmpty) file.getName else s"$parentPath/${file.getName}"
if (file.isDirectory) {
val dirEntryName = if (entryName.endsWith("/")) entryName else s"$entryName/"
IO(zipOut.putNextEntry(new ZipEntry(dirEntryName))) *>
IO(zipOut.closeEntry()) *>
IO(Option(file.listFiles()).getOrElse(Array.empty[File]).toList)
.flatMap(_.traverse_(addFileToZip(zipOut, dirEntryName)))
} else {
val entry = new ZipEntry(entryName)
for {
_ <- IO(zipOut.putNextEntry(entry))
_ <- copyFileToZip(zipOut)(file)
_ <- IO(zipOut.closeEntry())
} yield ()
}
}
private def copyFileToZip(zipOut: ZipOutputStream)(file: File): IO[Unit] = {
val inputStreamResource: Resource[IO, FileInputStream] =
Resource.make(IO(new FileInputStream(file))) { fis =>
IO(fis.close())
}
inputStreamResource.use { fis =>
readInputStream(IO(fis), chunkSize = bufferSize)
.through(writeOutputStream(IO(zipOut), closeAfterUse = false))
.compile
.drain
}
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment