package com.faforever.client.io; import com.faforever.client.util.Validator; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import java.io.File; import java.io.IOException; import java.io.InputStream; import java.io.OutputStream; import java.lang.invoke.MethodHandles; import java.nio.file.FileVisitResult; import java.nio.file.Files; import java.nio.file.Path; import java.nio.file.SimpleFileVisitor; import java.nio.file.attribute.BasicFileAttributes; import java.util.zip.ZipEntry; import java.util.zip.ZipOutputStream; public final class Zipper { private static final Logger logger = LoggerFactory.getLogger(MethodHandles.lookup().lookupClass()); private static final char PATH_SEPARATOR = File.separatorChar; private final Path directoryToZip; private boolean zipContent; private ByteCountListener byteCountListener; private int byteCountInterval; private int bufferSize; private long bytesTotal; private long bytesDone; private ZipOutputStream zipOutputStream; private long lastCountUpdate; private byte[] buffer; /** * @param zipContent {@code true} if the contents of the directory should be zipped, {@code false} if the specified * file (directory) should be zipped directly. */ private Zipper(Path directoryToZip, boolean zipContent) { this.directoryToZip = directoryToZip; this.zipContent = zipContent; // 4K bufferSize = 0x1000; byteCountInterval = 40; } public static Zipper of(Path path) { return new Zipper(path, false); } public static Zipper contentOf(Path path) { return new Zipper(path, true); } public Zipper to(ZipOutputStream zipOutputStream) { this.zipOutputStream = zipOutputStream; return this; } public Zipper byteCountInterval(int byteCountInterval) { this.byteCountInterval = byteCountInterval; return this; } public Zipper listener(ByteCountListener byteCountListener) { this.byteCountListener = byteCountListener; return this; } public void zip() throws IOException { Validator.notNull(zipOutputStream, "zipOutputStream must not be null"); Validator.notNull(directoryToZip, "directoryToZip must not be null"); bytesTotal = calculateTotalBytes(); bytesDone = 0; buffer = new byte[bufferSize]; Files.walkFileTree(directoryToZip, new SimpleFileVisitor<Path>() { public FileVisitResult preVisitDirectory(Path dir, BasicFileAttributes attrs) throws IOException { if (zipContent) { zipOutputStream.putNextEntry(new ZipEntry(directoryToZip.relativize(dir).toString() + "/")); } else { zipOutputStream.putNextEntry(new ZipEntry(directoryToZip.getParent().relativize(dir).toString() + "/")); } zipOutputStream.closeEntry(); return FileVisitResult.CONTINUE; } public FileVisitResult visitFile(Path file, BasicFileAttributes attrs) throws IOException { logger.trace("Zipping file {}", file.toAbsolutePath()); if (zipContent) { zipOutputStream.putNextEntry(new ZipEntry(directoryToZip.relativize(file).toString().replace(PATH_SEPARATOR, '/'))); } else { zipOutputStream.putNextEntry(new ZipEntry(directoryToZip.getParent().relativize(file).toString().replace(PATH_SEPARATOR, '/'))); } try (InputStream inputStream = Files.newInputStream(file)) { copy(inputStream, zipOutputStream); } zipOutputStream.closeEntry(); return FileVisitResult.CONTINUE; } }); } private long calculateTotalBytes() throws IOException { final long[] bytesTotal = {0}; Files.walkFileTree(directoryToZip, new SimpleFileVisitor<Path>() { public FileVisitResult visitFile(Path file, BasicFileAttributes attrs) throws IOException { bytesTotal[0] += Files.size(file); return FileVisitResult.CONTINUE; } }); return bytesTotal[0]; } private void copy(InputStream inputStream, OutputStream outputStream) throws IOException { int length; while ((length = inputStream.read(buffer)) > 0) { outputStream.write(buffer, 0, length); bytesDone += length; long now = System.currentTimeMillis(); if (byteCountListener != null && lastCountUpdate < now - byteCountInterval) { byteCountListener.updateBytesWritten(bytesDone, bytesTotal); lastCountUpdate = now; } } } }