Skip to content

Commit

Permalink
Implemented ZipFs zip ops
Browse files Browse the repository at this point in the history
  • Loading branch information
lukaszwawrzyk committed Aug 2, 2018
1 parent d24f628 commit c7b5c80
Show file tree
Hide file tree
Showing 5 changed files with 214 additions and 161 deletions.
42 changes: 0 additions & 42 deletions src/main/scala/com/virtuslab/zipops/Main.scala

This file was deleted.

119 changes: 0 additions & 119 deletions src/main/scala/com/virtuslab/zipops/ZipOps.scala

This file was deleted.

148 changes: 148 additions & 0 deletions src/main/scala/org/virtuslab/zipops/Zip4jZipOps.scala
Original file line number Diff line number Diff line change
@@ -0,0 +1,148 @@
package org.virtuslab.zipops

import java.io.{ RandomAccessFile, File }

import net.lingala.zip4j.core.{ HeaderReader, HeaderWriter }
import java.nio.channels.{ FileChannel, Channels }
import java.nio.file.{ Files, Path }
import java.util.ArrayList

import scala.collection.JavaConverters._
import net.lingala.zip4j.model.{ ZipModel, FileHeader }

object Zip4jZipOps extends ZipOps {

override def removeEntries(jarFile: File, classes: Iterable[String]): Unit = {
removeEntries(jarFile.toPath, classes.toSet)
}

override def mergeArchives(into: File, from: File): Unit = {
mergeArchives(into.toPath, from.toPath)
}

private type CentralDirectory = ZipModel

private def removeEntries(path: Path, toRemove: Set[String]): Unit = {
val centralDir = readCentralDir(path)
removeEntriesFromCentralDir(centralDir, toRemove)
val file = openFile(path)
truncateCentralDir(centralDir, file)
val writeOffset = startOfCentralDir(centralDir)
finalizeZip(centralDir, file, writeOffset)
}

private def removeEntriesFromCentralDir(centralDir: CentralDirectory, toRemove: Set[String]): Unit = {
val headers = getHeaders(centralDir)
val sanitizedToRemove = toRemove.map(_.stripPrefix("/"))
val clearedHeaders = headers.filterNot(header => sanitizedToRemove.contains(header.getFileName))
centralDir.getCentralDirectory.setFileHeaders(asArrayList(clearedHeaders))
}

private def mergeArchives(target: Path, source: Path): Unit = {
val targetCentralDir = readCentralDir(target)
val sourceCentralDir = readCentralDir(source)

val targetFile = openFile(target)
val sourceFile = openFile(source)

truncateCentralDir(targetCentralDir, targetFile)

// "source" starts where "target" ends
val sourceStart = targetFile.size()
// "source" is as long as from its beginning till the start of central dir
val sourceLength = startOfCentralDir(sourceCentralDir)

transferAll(from = sourceFile,
to = targetFile,
startPos = sourceStart,
bytesToTransfer = sourceLength)
sourceFile.close()

val mergedHeaders = mergeHeaders(targetCentralDir, sourceCentralDir, sourceStart)
targetCentralDir.getCentralDirectory.setFileHeaders(asArrayList(mergedHeaders))

val centralDirStart = sourceStart + sourceLength
targetCentralDir.getEndCentralDirRecord.setOffsetOfStartOfCentralDir(centralDirStart)

finalizeZip(targetCentralDir, targetFile, centralDirStart)

Files.delete(source)
}

private def mergeHeaders(
targetModel: CentralDirectory,
sourceModel: CentralDirectory,
sourceStart: Long
): Seq[FileHeader] = {
val sourceHeaders = getHeaders(sourceModel)
sourceHeaders.foreach { header =>
// potentially offsets should be updated for each header
// not only in central directory but a valid zip tool
// should not rely on that unless the file is corrupted
val currentOffset = header.getOffsetLocalHeader
val newOffset = currentOffset + sourceStart
header.setOffsetLocalHeader(newOffset)
}

// override files from target with files from source
val sourceNames = sourceHeaders.map(_.getFileName).toSet
val targetHeaders = getHeaders(targetModel).filterNot(h => sourceNames.contains(h.getFileName))

targetHeaders ++ sourceHeaders
}

private def readCentralDir(path: Path): CentralDirectory = {
val file = new RandomAccessFile(path.toFile, "rw")
val headerReader = new HeaderReader(file)
val centralDir = headerReader.readAllHeaders()
file.close()
centralDir
}

private def startOfCentralDir(centralDir: CentralDirectory) = {
centralDir.getEndCentralDirRecord.getOffsetOfStartOfCentralDir
}

private def getHeaders(centralDir: CentralDirectory): Seq[FileHeader] = {
centralDir.getCentralDirectory.getFileHeaders.asInstanceOf[ArrayList[FileHeader]].asScala
}

private def truncateCentralDir(centralDir: CentralDirectory, channel: FileChannel): FileChannel = {
channel.truncate(startOfCentralDir(centralDir))
}

private def finalizeZip(
centralDir: CentralDirectory,
channel: FileChannel,
centralDirStart: Long
): Unit = {
val outputStream = Channels.newOutputStream(channel.position(centralDirStart))
val headerWriter = new HeaderWriter
headerWriter.finalizeZipFile(centralDir, outputStream)
channel.close()
}

private def openFile(path: Path) = {
new RandomAccessFile(path.toFile, "rw").getChannel
}

private def transferAll(
from: FileChannel,
to: FileChannel,
startPos: Long,
bytesToTransfer: Long
): Unit = {
var remaining = bytesToTransfer
var offset = startPos
while (remaining > 0) {
val transferred = to.transferFrom(from, /*position =*/ offset, /*count = */ remaining)
offset += transferred
remaining -= transferred
}
}

private def asArrayList[A](clearedHeaders: Seq[A]): ArrayList[A] = {
new ArrayList[A](clearedHeaders.asJava)
}

}
58 changes: 58 additions & 0 deletions src/main/scala/org/virtuslab/zipops/ZipFsZipOps.scala
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
package org.virtuslab.zipops

import java.net.URI
import java.io.File
import java.nio.file._
import java.util.function.Consumer

object ZipFsZipOps extends ZipOps {

def removeEntries(jarFile: File, classes: Iterable[String]): Unit = {
withZipFs(jarFile) { fs =>
classes.foreach { cls =>
Files.deleteIfExists(fs.getPath(cls))
}
}
}

def mergeArchives(into: File, from: File): Unit = {
withZipFs(into) { intoFs =>
withZipFs(from) { fromFs =>
Files
.walk(fromFs.getPath("/"))
.forEachOrdered(new Consumer[Path] {
override def accept(t: Path): Unit = {
if (Files.isDirectory(t)) {
Files.createDirectories(intoFs.getPath(t.toString))
} else {
Files.copy(t,
intoFs.getPath(t.toString),
StandardCopyOption.COPY_ATTRIBUTES,
StandardCopyOption.REPLACE_EXISTING)
}
}
})
}
}
from.delete()
}

private def withZipFs[A](file: File, create: Boolean = false)(action: FileSystem => A): A = {
withZipFs(fileToJarUri(file), create)(action)
}

private def fileToJarUri(jarFile: File): URI = {
new URI("jar:" + jarFile.toURI.toString)
}

private def withZipFs[A](uri: URI, create: Boolean)(action: FileSystem => A): A = {
val env = new java.util.HashMap[String, String]
if (create) env.put("create", "true")
val fs = FileSystems.newFileSystem(uri, env)
try action(fs)
finally {
fs.close()
}
}

}
8 changes: 8 additions & 0 deletions src/main/scala/org/virtuslab/zipops/ZipOps.scala
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
package org.virtuslab.zipops

import java.io.File

trait ZipOps {
def removeEntries(jarFile: File, classes: Iterable[String]): Unit
def mergeArchives(into: File, from: File): Unit
}

0 comments on commit c7b5c80

Please sign in to comment.