[platform] introduces compressor API

... as a foundation of future merger/replacement for TarUtil/ZipUtil
This commit is contained in:
Roman Shevchenko
2018-06-18 11:13:59 +03:00
parent 0829af12dc
commit 2de36f53d7
2 changed files with 285 additions and 0 deletions
@@ -0,0 +1,188 @@
// Copyright 2000-2018 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license that can be found in the LICENSE file.
package com.intellij.util.io;
import com.intellij.openapi.util.Condition;
import com.intellij.openapi.util.io.FileUtil;
import com.intellij.openapi.util.text.StringUtil;
import org.apache.commons.compress.archivers.tar.TarArchiveEntry;
import org.apache.commons.compress.archivers.tar.TarArchiveOutputStream;
import org.apache.commons.compress.compressors.gzip.GzipCompressorOutputStream;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import java.io.*;
import java.util.zip.ZipEntry;
import java.util.zip.ZipOutputStream;
public abstract class Compressor implements Closeable {
public static class Tar extends Compressor {
public Tar(@NotNull File file) throws IOException {
myStream = new TarArchiveOutputStream(new GzipCompressorOutputStream(new FileOutputStream(file)));
myStream.setLongFileMode(TarArchiveOutputStream.LONGFILE_POSIX);
}
public Tar(@NotNull TarArchiveOutputStream stream) {
myStream = stream;
}
//<editor-fold desc="Implementation">
private final TarArchiveOutputStream myStream;
@Override
protected void writeDirectoryEntry(String name, long timestamp) throws IOException {
TarArchiveEntry e = new TarArchiveEntry(name + '/');
e.setModTime(timestamp);
myStream.putArchiveEntry(e);
myStream.closeArchiveEntry();
}
@Override
protected void writeFileEntry(String name, InputStream source, long length, long timestamp) throws IOException {
TarArchiveEntry e = new TarArchiveEntry(name);
e.setSize(length);
e.setModTime(timestamp);
myStream.putArchiveEntry(e);
FileUtil.copy(source, myStream);
myStream.closeArchiveEntry();
}
@Override
public void close() throws IOException {
myStream.close();
}
//</editor-fold>
}
public static class Zip extends Compressor {
public Zip(@NotNull File file) throws FileNotFoundException {
myStream = new ZipOutputStream(new FileOutputStream(file));
}
public Zip(@NotNull ZipOutputStream stream) {
myStream = stream;
}
//<editor-fold desc="Implementation">
private final ZipOutputStream myStream;
@Override
protected void writeDirectoryEntry(String name, long timestamp) throws IOException {
ZipEntry e = new ZipEntry(name + '/');
e.setMethod(ZipEntry.STORED);
e.setSize(0);
e.setCrc(0);
e.setTime(timestamp);
myStream.putNextEntry(e);
myStream.closeEntry();
}
@Override
protected void writeFileEntry(String name, InputStream source, long length, long timestamp) throws IOException {
ZipEntry e = new ZipEntry(name);
if (length == 0) {
e.setMethod(ZipEntry.STORED);
e.setSize(0);
e.setCrc(0);
}
e.setTime(timestamp);
myStream.putNextEntry(e);
FileUtil.copy(source, myStream);
myStream.closeEntry();
}
@Override
public void close() throws IOException {
myStream.close();
}
//</editor-fold>
}
private Condition<String> myFilter = null;
public Compressor filter(@Nullable Condition<String> filter) {
myFilter = filter;
return this;
}
public final void addFile(@NotNull String entryName, @NotNull File file) throws IOException {
entryName = entryName(entryName);
if (accepts(entryName)) {
InputStream source = new FileInputStream(file);
try {
writeFileEntry(entryName, source, file.length(), file.lastModified());
}
finally {
source.close();
}
}
}
public final void addFile(@NotNull String entryName, @NotNull byte[] content) throws IOException {
addFile(entryName, content, -1);
}
public final void addFile(@NotNull String entryName, @NotNull byte[] content, long timestamp) throws IOException {
entryName = entryName(entryName);
if (accepts(entryName)) {
writeFileEntry(entryName, new ByteArrayInputStream(content), content.length, timestamp(timestamp));
}
}
public final void addDirectory(@NotNull String entryName) throws IOException {
addDirectory(entryName, -1);
}
public final void addDirectory(@NotNull String entryName, long timestamp) throws IOException {
entryName = entryName(entryName);
if (accepts(entryName)) {
writeDirectoryEntry(entryName, timestamp(timestamp));
}
}
public final void addDirectory(@NotNull File directory) throws IOException {
addRecursively("", directory);
}
public final void addDirectory(@NotNull String prefix, @NotNull File directory) throws IOException {
addRecursively(entryName(prefix), directory);
}
//<editor-fold desc="Internal interface">
protected Compressor() { }
private static String entryName(String name) {
String entryName = StringUtil.trimLeading(StringUtil.trimTrailing(name.replace('\\', '/'), '/'), '/');
if (StringUtil.isEmpty(entryName)) throw new IllegalArgumentException("Invalid entry name: " + name);
return entryName;
}
private static long timestamp(long timestamp) {
return timestamp == -1 ? System.currentTimeMillis() : timestamp;
}
private boolean accepts(String entryName) {
return myFilter == null || myFilter.value(entryName);
}
private void addRecursively(String prefix, File directory) throws IOException {
File[] children = directory.listFiles();
if (children != null) {
if (!prefix.isEmpty()) {
addDirectory(prefix, directory.lastModified());
}
for (File child: children) {
String name = prefix.isEmpty() ? child.getName() : prefix + '/' + child.getName();
if (child.isDirectory()) {
addRecursively(name, child);
}
else {
addFile(name, child);
}
}
}
}
protected abstract void writeDirectoryEntry(String name, long timestamp) throws IOException;
protected abstract void writeFileEntry(String name, InputStream source, long length, long timestamp) throws IOException;
//</editor-fold>
}
@@ -0,0 +1,97 @@
// Copyright 2000-2018 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license that can be found in the LICENSE file.
package com.intellij.util.io
import com.intellij.testFramework.rules.TempDirectory
import org.apache.commons.compress.archivers.tar.TarArchiveInputStream
import org.apache.commons.compress.compressors.gzip.GzipCompressorInputStream
import org.assertj.core.api.Assertions.assertThat
import org.junit.Rule
import org.junit.Test
import java.io.File
import java.io.FileInputStream
import java.util.zip.ZipInputStream
class CompressorTest {
@Rule @JvmField var tempDir = TempDirectory()
@Test fun simpleZip() {
val zip = tempDir.newFile("test.zip")
Compressor.Zip(zip).use { it.addFile("file.txt", "123".toByteArray()) }
assertZip(zip, "file.txt" to "123")
}
@Test fun simpleZipWithFilters() {
val zip = tempDir.newFile("test.zip")
val set = mutableSetOf<String>()
Compressor.Zip(zip).filter(set::add).use {
it.addFile("file1.txt", "123".toByteArray())
it.addFile("file2.txt", "456".toByteArray())
it.addFile("file1.txt", "789".toByteArray())
}
assertZip(zip, "file1.txt" to "123", "file2.txt" to "456")
}
@Test fun recursiveZip() {
val dir = tempDir.newFolder("dir")
tempDir.newFile("dir/f1").writeText("1")
tempDir.newFile("dir/f2").writeText("2")
tempDir.newFile("dir/d1/f11").writeText("11")
tempDir.newFile("dir/d1/f12").writeText("12")
tempDir.newFile("dir/d1/d11/f111").writeText("111")
tempDir.newFile("dir/d1/d11/f112").writeText("112")
tempDir.newFile("dir/d2/f21").writeText("21")
tempDir.newFile("dir/d2/f22").writeText("22")
val zip = tempDir.newFile("test.zip")
Compressor.Zip(zip).use { it.addDirectory(dir) }
assertZip(
zip,
"d1/" to "", "d1/d11/" to "", "d2/" to "",
"f1" to "1", "f2" to "2",
"d1/f11" to "11", "d1/f12" to "12",
"d1/d11/f111" to "111", "d1/d11/f112" to "112",
"d2/f21" to "21", "d2/f22" to "22")
}
@Test fun recursiveTarWithPrefix() {
val dir = tempDir.newFolder("dir")
tempDir.newFile("dir/f1").writeText("1")
tempDir.newFile("dir/f2").writeText("2")
tempDir.newFile("dir/d1/f11").writeText("11")
tempDir.newFile("dir/d1/f12").writeText("12")
tempDir.newFile("dir/d1/d11/f111").writeText("111")
tempDir.newFile("dir/d1/d11/f112").writeText("112")
tempDir.newFile("dir/d2/f21").writeText("21")
tempDir.newFile("dir/d2/f22").writeText("22")
val tar = tempDir.newFile("test.tgz")
Compressor.Tar(tar).use { it.addDirectory("tar/", dir) }
assertTar(
tar,
"tar/" to "", "tar/d1/" to "", "tar/d1/d11/" to "", "tar/d2/" to "",
"tar/f1" to "1", "tar/f2" to "2",
"tar/d1/f11" to "11", "tar/d1/f12" to "12",
"tar/d1/d11/f111" to "111", "tar/d1/d11/f112" to "112",
"tar/d2/f21" to "21", "tar/d2/f22" to "22")
}
@Test fun entryNameTrimming() {
val zip = tempDir.newFile("test.zip")
Compressor.Zip(zip).use { it.addFile("//file.txt//", "123".toByteArray()) }
assertZip(zip, "file.txt" to "123")
}
private fun assertZip(zip: File, vararg expected: Pair<String, String>) {
val actual = ZipInputStream(FileInputStream(zip)).use {
generateSequence(it::getNextEntry).map { entry -> entry.name to String(it.readBytes()) }.toList()
}
assertThat(actual).containsExactlyInAnyOrder(*expected)
}
private fun assertTar(tar: File, vararg expected: Pair<String, String>) {
val actual = TarArchiveInputStream(GzipCompressorInputStream(FileInputStream(tar))).use {
generateSequence(it::getNextTarEntry).map { entry -> entry.name to String(it.readBytes()) }.toList()
}
assertThat(actual).containsExactlyInAnyOrder(*expected)
}
}