Рекомендации по созданию и загрузке огромного 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 многопоточность загрузки файлов

Надеюсь, что это поможет.