Java用未知大小的条目创建tar存档

时间:2018-11-18 04:07:38

标签: java io stream tar archive

我有一个Web应用程序,需要在该应用程序中为用户提供多个文件的存档。我已经建立了一个通用的ArchiveExporter,并制作了一个ZipArchiveExporter。作品精美!我可以将数据流式传输到服务器,然后将数据存档并流式传输给用户,而无需占用大量内存,也不需要文件系统(我使用的是Google App Engine)。

然后,我想起了有关4gb zip文件的整个zip64。我的档案可能会变得非常大(高分辨率图像),因此我想选择一个选项来避免将zip文件用于较大的输入。

我签出了org.apache.commons.compress.archivers.tar.TarArchiveOutputStream,并认为自己已经找到了所需的东西!可悲的是,当我检查文档时,遇到了一些错误;我很快发现您必须在流式传输时通过每个条目的大小。这是一个问题,因为数据正在流式传输给我,而无法事先知道大小。

我尝试计数并从export()返回写入的字节,但是TarArchiveOutputStream期望在TarArchiveEntry 中写入之前有一个大小,因此显然不会”工作。

我可以使用ByteArrayOutputStream并在写入内容之前完全读取每个条目,以便知道其大小,但是我的条目可能会变得非常大;并且对实例上运行的其他进程不太礼貌。

我可以使用某种形式的持久性,上载条目并查询数据大小。但是,那会浪费我的Google存储api调用,带宽,存储和运行时。

我知道this这样的问题询问几乎相同的内容,但他决定使用zip文件,并且没有其他相关信息。

创建条目大小未知的tar存档的理想解决方案是什么?

public abstract class ArchiveExporter<T extends OutputStream> extends Exporter { //base class
    public abstract void export(OutputStream out); //from Exporter interface
    public abstract void archiveItems(T t) throws IOException;
}

public class ZipArchiveExporter extends ArchiveExporter<ZipOutputStream> { //zip class, works as intended
    @Override
    public void export(OutputStream out) throws IOException {
        try(ZipOutputStream zos = new ZipOutputStream(out, Charsets.UTF_8)) {
            zos.setLevel(0);
            archiveItems(zos);
        }
    }
    @Override
    protected void archiveItems(ZipOutputStream zos) throws IOException {
        zos.putNextEntry(new ZipEntry(exporter.getFileName()));
        exporter.export(zos);
        //chained call to export from other exporter like json exporter for instance
        zos.closeEntry();
    }
}

public class TarArchiveExporter extends ArchiveExporter<TarArchiveOutputStream> {
    @Override
    public void export(OutputStream out) throws IOException {
        try(TarArchiveOutputStream taos = new TarArchiveOutputStream(out, "UTF-8")) {
            archiveItems(taos);
        }
    }
    @Override
    protected void archiveItems(TarArchiveOutputStream taos) throws IOException {
        TarArchiveEntry entry = new TarArchiveEntry(exporter.getFileName());
        //entry.setSize(?);
        taos.putArchiveEntry(entry);
        exporter.export(taos);
        taos.closeArchiveEntry();
    }
}

EDIT ,这就是我对ByteArrayOutputStream的看法。它可以工作,但是我不能保证我将永远有足够的内存来一次存储整个条目,因此需要进行流式处理。必须有一种更优雅的流压缩包的方式!也许这是一个更适合代码审查的问题?

protected void byteArrayOutputStreamApproach(TarArchiveOutputStream taos) throws IOException {
    TarArchiveEntry entry = new TarArchiveEntry(exporter.getFileName());
    try(ByteArrayOutputStream baos = new ByteArrayOutputStream()) {
        exporter.export(baos);
        byte[] data = baos.toByteArray();
        //holding ENTIRE entry in memory. What if it's huge? What if it has more than Integer.MAX_VALUE bytes? :[
        int len = data.length;
        entry.setSize(len);
        taos.putArchiveEntry(entry);
        taos.write(data);
        taos.closeArchiveEntry();
    }
}

EDIT 这就是我将条目上传到介质(在这种情况下为Google Cloud Storage)以准确查询整个大小的意思。看起来像是一个简单的问题,看起来似乎有点过大的杀伤力,但这并没有遇到与上述解决方案相同的内存问题。只是以带宽和时间为代价。我希望有人比我聪明,让我很快变得愚蠢:D

protected void googleCloudStorageTempFileApproach(TarArchiveOutputStream taos) throws IOException {
    TarArchiveEntry entry = new TarArchiveEntry(exporter.getFileName());
    String name = NameHelper.getRandomName(); //get random name for temp storage
    BlobInfo blobInfo = BlobInfo.newBuilder(StorageHelper.OUTPUT_BUCKET, name).build(); //prepare upload of temp file
    WritableByteChannel wbc = ApiContainer.storage.writer(blobInfo); //get WriteChannel for temp file
    try(OutputStream out = Channels.newOutputStream(wbc)) {
        exporter.export(out); //stream items to remote temp file
    } finally {
        wbc.close();
    }

    Blob blob = ApiContainer.storage.get(blobInfo.getBlobId());
    long size = blob.getSize(); //accurately query the size after upload
    entry.setSize(size);
    taos.putArchiveEntry(entry);

    ReadableByteChannel rbc = blob.reader(); //get ReadChannel for temp file
    try(InputStream in = Channels.newInputStream(rbc)) {
        IOUtils.copy(in, taos); //stream back to local tar stream from remote temp file 
    } finally {
        rbc.close();
    }
    blob.delete(); //delete remote temp file

    taos.closeArchiveEntry();
}

1 个答案:

答案 0 :(得分:1)

我一直在研究类似的问题,据我所知,这是tar file format的约束。

Tar文件作为流写入,元数据(文件名,权限等)写入文件数据之间(即元数据1,文件数据1,元数据2,文件数据2等)。提取数据的程序将读取元数据1,然后开始提取文件数据1,但是它必须知道何时完成。这可以通过多种方法来完成。 tar通过在元数据中保留长度来做到这一点。

根据您的需求以及收件人的期望,我可以看到一些选项(并非全部适用于您的情况):

  1. 如前所述,加载整个文件,计算长度,然后发送。
  2. 将文件分成预定长度的块(适合内存),然后将其压缩为file1-part1,file1-part2等;最后一块很短。
  3. 将文件划分为预定义长度的块(不需要放入内存),然后使用适当的内容将最后一个块填充到该大小。
  4. 计算出文件的最大可能大小,并将其填充到该大小。
  5. 使用其他存档格式。
  6. 制作自己的存档格式,没有此限制。

有趣的是,gzip没有预定义的限制,可以将多个gzip串联在一起,每个都有自己的“原始文件名”。不幸的是,标准gunzip使用第一个文件名(?)将所有结果数据提取到一个文件中。