【问题标题】:Java create tar archive with entries of unknown sizeJava 创建包含未知大小条目的 tar 存档
【发布时间】:2019-04-20 19:39:48
【问题描述】:

我有一个网络应用程序,我需要能够为用户提供多个文件的存档。我设置了一个通用的ArchiveExporter,并创建了一个ZipArchiveExporter。工作精美!我可以将我的数据流式传输到我的服务器,并将数据存档并将其流式传输给用户,而无需使用太多内存,也不需要文件系统(我在 Google App Engine 上)。

然后我想起了 4gb zip 文件的整个 zip64 东西。我的档案可能会变得非常大(高分辨率图像),因此我希望有一个选项来避免使用 zip 文件来进行更大的输入。

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

我尝试计算并返回来自export() 的写入字节,但TarArchiveOutputStream 预计TarArchiveEntry 中的大小写入它之前,所以这显然不起作用。

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

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

我知道this SO 问题问的几乎相同,但他决定使用 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();
    }
}

编辑这是我对ByteArrayOutputStream 的想法。它有效,但我不能保证我总是有足够的内存来一次存储整个条目,因此我的流媒体工作。必须有一种更优雅的方式来传输 tarball!也许这是一个更适合 Code Review 的问题?

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();
    }
}

编辑这就是我将条目上传到介质(在本例中为 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();
}

【问题讨论】:

    标签: java io stream tar archive


    【解决方案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 使用 (?) 第一个文件名将所有生成的数据提取到一个文件中。

    【讨论】:

      猜你喜欢
      • 2020-08-07
      • 1970-01-01
      • 1970-01-01
      • 2017-04-20
      • 1970-01-01
      • 2017-01-24
      • 2012-04-30
      • 1970-01-01
      • 2010-12-02
      相关资源
      最近更新 更多