代码之家  ›  专栏  ›  技术社区  ›  MeetTitan

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

  •  1
  • MeetTitan  · 技术社区  · 7 年前

    我有一个web应用程序,我需要能够为用户提供多个文件的存档。我已经建立了一个通用的 ArchiveExporter ,并使 ZipArchiveExporter . 很好用!我可以将我的数据流到我的服务器上,并将数据存档并流到用户那里,而无需使用大量内存,也无需文件系统(我在Google App Engine上)。

    我退房了 org.apache.commons.compress.archivers.tar.TarArchiveOutputStream 以为我找到了我需要的!可悲的是,当我查看文档时,遇到了一些错误;我很快就发现了你 在传输时传递每个条目的大小。这是一个问题,因为数据是流到我没有办法事先知道大小。

    我试着计算并返回 export() ,但是 TarArchiveOutputStream 预计尺寸为 TarArchiveEntry 之前

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

    我知道 this

    创建包含未知大小条目的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公司 . 它可以工作,但我不能保证我总是有足够的内存来存储整个条目,因此我的流媒体工作。必须有一个更优雅的方式流一个柏油球!也许这是一个更适合代码评审的问题?

    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云存储)以精确查询整个大小。看起来像是一个简单问题的主要过度杀戮,但这并不像上面的解决方案那样遭受相同的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();
    }
    
    0 回复  |  直到 7 年前
        1
  •  1
  •   AMADANON Inc.    7 年前

    我也在考虑类似的问题 tar file format 据我所知。

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

    1. 如您所述,加载整个文件,计算出长度,然后发送它。
    2. 将文件分成预定义长度的块(不需要放入内存),然后用适当的东西将最后一个块填充到该大小。
    3. 使用其他存档格式。
    4. 创建自己的存档格式,但不受此限制。

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