У меня есть веб-приложение, в котором мне нужно предоставить пользователю архив нескольких файлов.Я настроил общий 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();
}