Рекомендации по созданию и загрузке огромного ZIP (из нескольких BLOB) в WebApp
Мне нужно будет выполнить массивную загрузку файлов из моего веб-приложения.
Ожидается, что это будет длительное действие (он будет использоваться один раз в год [-per-customer]), поэтому время не является проблемой (если оно не достигло некоторого таймаута, но я могу справляйтесь с этим, создавая некоторую форму сердечного удара). Я знаю, как создать скрытый iframe и использовать его с помощью content-disposition: attachment
, чтобы попытаться загрузить файл, а не открывать его внутри браузера, и как указать связь клиент-сервер для рисования индикатора выполнения;
Фактический размер загрузки (и количества файлов) неизвестен, но для простоты мы можем фактически рассматривать его как 1 ГБ, состоящий из 100 файлов, каждый 10 МБ.
Поскольку это должна быть операция с одним щелчком мыши, моя первая мысль состояла в том, чтобы сгруппировать все файлы, читая их из базы данных, в динамически созданный ZIP, а затем попросите пользователя сохранить ZIP.
Вопрос: каковы наилучшие методы, а также известные недостатки и ловушки при создании огромного архива из нескольких массивов небольших байтов в WebApp?
Это может быть случайным образом разбито на:
- должен ли каждый байт-массив быть преобразован в физический временный файл или может быть добавлен в ZIP-память?
- Если да, я знаю, что мне придется обрабатывать возможное равенство имен (они могут иметь одно и то же имя в разных записях в базе данных, но не внутри одной и той же файловой системы или ZIP): есть ли другие возможные проблемы (при условии, что файловая система всегда имеет достаточное физическое пространство)?
- так как я не могу полагаться на достаточное количество ОЗУ для выполнения всей операции в памяти, я думаю, что ZIP должен быть создан и передан в файловую систему перед отправкой пользователю; есть ли способ сделать это по-другому (например, с помощью websocket), например, спросить пользователя, где сохранить файл, а затем начать постоянный поток данных с сервера на клиент (как я полагаю, Sci-Fi)?
- будут приветствоваться любые другие связанные с вами известные проблемы или передовые методы, которые пересекают ваш разум.
Ответы
Ответ 1
Для большого содержимого, которое сразу не будет вписываться в память, вы можете передать содержимое из базы данных в ответ.
Этот вид на самом деле довольно прост. Вам не нужны AJAX или websockets, можно передавать большие загрузки файлов по простой ссылке, на которую пользователь нажимает. И у современных браузеров есть приличные менеджеры загрузки с их собственными индикаторами прогресса - зачем изобретать колесо?
Если вы пишете сервлет с нуля для этого, получите доступ к BLOB базы данных, получив свой поток ввода и скопируйте контент в поток ответа HTTP-ответа. Если у вас есть библиотека IO Apache Commons, вы можете использовать IOUtils.copy(), иначе вы можете сделать это самостоятельно.
Создание ZIP файла на лету можно сделать с помощью ZipOutputStream. Создайте один из них по выходному потоку ответа (из сервлета или независимо от того, что предоставляет ваша инфраструктура), затем получите каждый BLOB из базы данных, сначала используя putNextEntry()
, а затем потоковой передачи каждого BLOB, как описано ранее.
Потенциальные ошибки/проблемы:
- В зависимости от размера загрузки и скорости сети, запрос может занять много времени. Брандмауэры и т.д. Могут мешать этому и рано разорвать запрос.
- Надеюсь, ваши пользователи будут в приличной корпоративной сети при запросе этих файлов. Это было бы намного хуже, чем удаленные/dodgey/мобильные соединения (если он выпадет после загрузки 1.9G 2.0G, пользователи должны начать заново).
- Он может немного загружать ваш сервер, особенно сжимая огромные ZIP файлы. При создании
ZipOutputStream
может возникнуть проблема с уменьшением/уменьшением сжатия, если это проблема.
- Файлы ZIP более 2 ГБ (или 4 ГБ) могут иметь проблемы с некоторыми программами ZIP. Я думаю, что последняя версия Java 7 использует расширения ZIP64, поэтому эта версия Java будет писать огромный ZIP правильно, но будут ли у клиентов программы, поддерживающие большие ZIP файлы? Я определенно столкнулся с проблемами с ними раньше, особенно на старых серверах Solaris.
Ответ 2
Пример полностью динамического ZIP файла, созданный потоковой передачей каждого BLOB из базы данных непосредственно в файловую систему клиента.
Протестировано огромными архивами со следующими характеристиками:
- Сервер дисковое пространство: 0 MegaBytes
- Сервер Оперативная память:
~ xx Мегабайты. Потребление памяти не проверяется (или, по крайней мере, я не знаю, как это сделать правильно), потому что я получил разные, по-видимому, случайные результаты от выполнения одной и той же процедуры несколько раз (с использованием Runtime.getRuntime().freeMemory()
) до, во время и после цикла). Однако потребление памяти ниже, чем использование байта [], и этого достаточно.
FileStreamDto.java, используя InputStream
вместо byte[]
public class FileStreamDto implements Serializable {
@Getter @Setter private String filename;
@Getter @Setter private InputStream inputStream;
}
Java Servlet (или действие Struts2)
/* Read the amount of data to be streamed from Database to File System,
summing the size of all Oracle BLOB, PostgreSQL ABYTE etc:
SELECT sum(length(my_blob_field)) FROM my_table WHERE my_conditions
*/
Long overallSize = getMyService().precalculateZipSize();
// Tell the browser is a ZIP
response.setContentType("application/zip");
// Tell the browser the filename, and that it needs to be downloaded instead of opened
response.addHeader("Content-Disposition", "attachment; filename=\"myArchive.zip\"");
// Tell the browser the overall size, so it can show a realistic progressbar
response.setHeader("Content-Length", String.valueOf(overallSize));
ServletOutputStream sos = response.getOutputStream();
ZipOutputStream zos = new ZipOutputStream(sos);
// Set-up a list of filenames to prevent duplicate entries
HashSet<String> entries = new HashSet<String>();
/* Read all the ID from the interested records in the database,
to query them later for the streams:
SELECT my_id FROM my_table WHERE my_conditions */
List<Long> allId = getMyService().loadAllId();
for (Long currentId : allId){
/* Load the record relative to the current ID:
SELECT my_filename, my_blob_field FROM my_table WHERE my_id = :currentId
Use resultset.getBinaryStream("my_blob_field") while mapping the BLOB column */
FileStreamDto fileStream = getMyService().loadFileStream(currentId);
// Create a zipEntry with a non-duplicate filename, and add it to the ZipOutputStream
ZipEntry zipEntry = new ZipEntry(getUniqueFileName(entries,fileStream.getFilename()));
zos.putNextEntry(zipEntry);
// Use Apache Commons to transfer the InputStream from the DB to the OutputStream
// on the File System; at this moment, your file is ALREADY being downloaded and growing
IOUtils.copy(fileStream.getInputStream(), zos);
zos.flush();
zos.closeEntry();
fileStream.getInputStream().close();
}
zos.close();
sos.close();
Вспомогательный метод для обработки повторяющихся записей
private String getUniqueFileName(HashSet<String> entries, String completeFileName){
if (entries.contains(completeFileName)){
int extPos = completeFileName.lastIndexOf('.');
String extension = extPos>0 ? completeFileName.substring(extPos) : "";
String partialFileName = extension.length()==0 ? completeFileName : completeFileName.substring(0,extPos);
int x=1;
while (entries.contains(completeFileName = partialFileName + "(" + x + ")" + extension))
x++;
}
entries.add(completeFileName);
return completeFileName;
}
Большое спасибо @prunge за то, что он дал мне идею прямой потоковой передачи.
Ответ 3
Возможно, вы хотите попробовать несколько загрузок одновременно. Я нашел обсуждение, связанное с этим здесь - Java многопоточность загрузки файлов
Надеюсь, что это поможет.