Java создать архив tar с записями неизвестного размера - PullRequest
0 голосов
/ 18 ноября 2018

У меня есть веб-приложение, в котором мне нужно предоставить пользователю архив нескольких файлов.Я настроил общий ArchiveExporter и сделал ZipArchiveExporter.Работает красиво!Я могу передавать свои данные на сервер, архивировать данные и передавать их пользователю, не используя много памяти и не нуждаясь в файловой системе (я работаю в Google App Engine).

Потом я вспомнил овся вещь zip64 с 4gb zip файлами.Мои архивы могут быть очень большими (изображения с высоким разрешением), поэтому я хотел бы иметь возможность избежать zip-файлов для моего большего ввода.

Я извлек org.apache.commons.compress.archivers.tar.TarArchiveOutputStream идумал, что нашел то, что мне нужно!К сожалению, когда я проверил документы и столкнулся с некоторыми ошибками;Я быстро обнаружил, что вы ДОЛЖНЫ передавать размер каждой записи при потоковой передаче.Это проблема, потому что данные передаются мне без возможности заранее узнать размер.

Я пытался считать и возвращать записанные байты из export(), но TarArchiveOutputStream ожидает размер в TarArchiveEntry до записи в него, так что это, очевидно, не работает.

Я могу использовать ByteArrayOutputStream и полностью прочитать каждую запись перед записью ее содержимого, чтобы я знал ее размер, но мойзаписи могут быть очень большими;и это не очень вежливо по отношению к другим процессам, запущенным на экземпляре.

Я мог бы использовать некоторую форму постоянства, загрузить запись и запросить размер данных.Однако это было бы напрасной тратой моих вызовов API хранилища Google, пропускной способности, хранилища и времени выполнения.

Мне известно, этот ТАК вопрос, задающий почти то же самое, но он согласился наиспользуя 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.Это работает, но я не могу гарантировать, что у меня всегда будет достаточно памяти для хранения всей записи сразу, отсюда и мои потоковые усилия.Там должен быть более элегантный способ потоковой передачи тарбол!Может быть, этот вопрос больше подходит для 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 inэтот случай), чтобы точно запросить весь размер.Похоже, что это излишнее излишнее решение проблемы, которая кажется простой, но она не страдает от тех же проблем с оперативной памятью, что и решение, описанное выше.Просто за счет пропускной способности и времени.Я надеюсь, что кто-то умнее меня придет и скоро заставит меня чувствовать себя глупо: 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 голосов
/ 28 мая 2019

Я смотрел на подобную проблему, и это ограничение формат файла tar , насколько я могу судить.

Файлы tar записываются в виде потока, а метаданные (имена файлов, разрешения и т. Д.) Записываются между данными файла (то есть метаданными 1, файловыми данными 1, метаданными 2, файловыми данными 2 и т. Д.). Программа, которая извлекает данные, читает метаданные 1, затем начинает извлекать данные 1, но она должна иметь возможность узнать, когда это будет сделано. Это можно сделать несколькими способами; tar делает это, имея длину в метаданных.

В зависимости от ваших потребностей и ожиданий получателя, я вижу несколько вариантов (не все относятся к вашей ситуации):

  1. Как вы упомянули, загрузите весь файл, определите длину и отправьте его.
  2. Разделите файл на блоки заранее определенной длины (которая умещается в памяти), затем разархивируйте их как file1-part1, file1-part2 и т. Д .; последний блок будет коротким.
  3. Разделите файл на блоки заданной длины (которые не нужно помещать в память), затем добавьте последний блок к этому размеру с помощью чего-то подходящего.
  4. Определите максимально возможный размер файла и добавьте к этому размеру.
  5. Используйте другой формат архива.
  6. Создайте свой собственный формат архива, который не имеет этого ограничения.

Интересно, что у gzip нет предопределенных ограничений, и можно объединить несколько gzips, каждый со своим «исходным именем файла». К сожалению, стандартный gunzip извлекает все полученные данные в один файл, используя (?) Первое имя файла.

...