app/services/ZipReader.scala (84 lines of code) (raw):

package services import akka.util.ByteString import io.circe.generic.auto._ import io.circe.yaml.parser import models.BuildInfo import org.slf4j.LoggerFactory import java.io.ByteArrayInputStream import java.util.zip.{ZipEntry, ZipInputStream} import scala.util.{Failure, Success, Try} class ZipReader(bytes:Array[Byte]) { private val logger = LoggerFactory.getLogger(getClass) /** * calls the given callback function with an open ZipStream of the content. * ensures that the streams are closed properly when the callback completes, whether successfully or not * @param func function to call, this must accept a ZipInputStream and return a Try * @tparam A data that is contained in the function's Try * @return the result of the function, or a Failure if the initial stream could not be set up. */ private def withStreams[A](func:(ZipInputStream)=>Try[A]) = { Try { val byteStream = new ByteArrayInputStream(bytes) val zipStream = new ZipInputStream(byteStream) (byteStream, zipStream) } match { case Success((byteStream, zipStream))=> val finalResult = func(zipStream) (Try { zipStream.close() }, Try { byteStream.close() }) match { case (Failure(zipErr), _)=> logger.error(s"Could not close zip stream: ${zipErr.getMessage}", zipErr) finalResult case (_, Failure(byteErr))=> logger.error(s"Could not close byte stream: ${byteErr.getMessage}", byteErr) finalResult case (Success(_), Success(_))=> finalResult } case Failure(err)=> Failure(err) } } /** * gets a list of the entries for the zipfile * @return */ def getEntries() = withStreams { zipStream => Try { /** * recursively traverses the zip directory in-memory * @param accumulator * @return */ def traverseEntries(accumulator: Seq[ZipEntry]): Seq[ZipEntry] = Option(zipStream.getNextEntry) match { case Some(entry) => traverseEntries(accumulator :+ entry) case None => accumulator } traverseEntries(Seq()) } } def readContent(from:ZipInputStream) = Try { from.readAllBytes() } /** * tries to locate the build-info.yaml file in the given bundle. * If found, returns a ByteString with the raw uncompressed data */ def locateBuildInfoContent():Try[Option[ByteString]] = withStreams(zipStream => { import cats.implicits._ def traverseEntries():Option[Try[ByteString]] = Option(zipStream.getNextEntry) match { case Some(entry) => if (entry.getName.endsWith("build-info.yaml")) { logger.debug(s"found build-info.yaml at ${entry.getName}, streaming the content") val content = readContent(zipStream).map(ByteString.apply) zipStream.closeEntry() Some(content) } else { zipStream.closeEntry() traverseEntries() } case None=> logger.debug("reached end of zip file, no build-info found") None } //use cats to turn the Option[Try[ByteString]] into a Try[Option[ByteString]] traverseEntries().sequence }) /** * calls locateBuildInfoContent and marshals the resulting bytestream info a BuildInfo object * @return */ def locateBuildInfo(maybeAwsAccount:Option[String], maybeRegion:Option[String]) = locateBuildInfoContent().map(_.map(bytes=>{ import models.DockerImageDecoder._ parser .parse(bytes.utf8String) .flatMap(_.as[BuildInfo]) match { case err@Left(parseErr)=> logger.error(s"Parsing error: $parseErr. Incoming data was: ${bytes.utf8String}") err case Right(result)=> (maybeAwsAccount, maybeRegion) match { case (Some(awsAcct), Some(region))=>Right(result.fixedUpAwsImage(awsAcct, region)) case (_, _)=>Right(result) } } })) } object ZipReader { def fromByteString(rawData:ByteString) = { new ZipReader(rawData.toArray) } }