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

标签 java io stream tar archive

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

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

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

我尝试计算并返回从 export() 写入的字节数,但是TarArchiveOutputStream期望大小在 TarArchiveEntry写入它之前,所以这显然是行不通的。

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

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

我知道 this SO question asking almost the same thing, but he settled for using zip files 而且没有更多相关信息。

创建包含未知大小条目的 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 的想法.它有效,但我不能保证我总是有足够的内存来一次存储整个条目,因此我的流媒体工作。必须有一种更优雅的方式来传输 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();
    }
}

EDIT 这就是我将条目上传到介质(在本例中为 Google Cloud Storage)以准确查询整个大小的意思。对于看似简单的问题来说,这似乎是大材小用,但这并没有遇到与上述解决方案相同的 ram 问题。只是以带宽和时间为代价。我希望比我聪明的人过来让我很快觉得自己很愚蠢 :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();
}

最佳答案

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

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

根据您的需要和收件人的期望,我可以看到一些选项(并非所有选项都适用于您的情况):

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

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

关于Java 使用未知大小的条目创建 tar 存档,我们在Stack Overflow上找到一个类似的问题: https://stackoverflow.com/questions/53357800/

相关文章:

io - Lisp 格式和强制输出

haskell - 在 Haskell 上,如何让 AI 使用 IO 控制函数(或者如何正确地重新设计它)?

Flutter:StreamProvider catchError 如何

java - 第二次调用时 JSch 不提供输出

java - JSP生成Excel电子表格(XLS)下载

java - 请问java RMI的意义?

java - Android 全屏只有一个 Activity ?

java - JNI 将 long 值传递给本地方法

java - 使用 ObjectInputStream 而不是 BufferedReader 时出现 NullPointerException

c++ - 流的非持久格式化