Mejores prácticas para crear y descargar un enorme ZIP (de varios BLOB) en una aplicación web

Tendré que realizar una descarga masiva de archivos desde mi aplicación web.

Obviamente se espera que sea una acción de larga duración (se usará una vez por año [-per-cliente] ), así que el tiempo no es un problema (a menos que se agote el tiempo de espera, pero puedo manejarlo por creando alguna forma de latido del corazón keepalive). Sé cómo crear un iframe oculto y usarlo con content-disposition: attachment para intentar descargar el archivo en lugar de abrirlo dentro del navegador, y cómo instanciar una comunicación cliente-servidor para dibujar un medidor de progreso;

El tamaño real de la descarga (y el número de archivos) es desconocido, pero para mayor simplicidad, podemos considerarlo virtualmente como 1GB, compuesto de 100 archivos, cada 10MB.

Dado que esto debería ser una operación de un solo clic, lo primero que pensé fue agrupar todos los archivos, mientras los leía desde la base de datos, en un ZIP generado dinámicamente, y luego pedirle al usuario que guarde el ZIP.

La pregunta es: ¿cuáles son las mejores prácticas y cuáles son los inconvenientes y las trampas conocidas al crear un archivo enorme a partir de múltiples matrices de pequeños bytes en una aplicación web?

Eso se puede dividir al azar en:

  • ¿Debería cada conjunto de bytes convertirse en un archivo temporal físico o pueden agregarse al ZIP en la memoria?
  • en caso afirmativo, sé que tendré que manejar la posible igualdad de nombres (pueden tener el mismo nombre en diferentes registros en la base de datos, pero no dentro del mismo sistema de archivos ni ZIP): ¿hay algún otro problema posible que pueda surgir? mente (suponiendo que el sistema de archivos siempre tiene suficiente espacio físico)?
  • dado que no puedo confiar en tener suficiente RAM para realizar toda la operación en la memoria, creo que el ZIP se debe crear y alimentar al sistema de archivos antes de enviarlo al usuario; ¿Hay alguna forma de hacerlo de manera diferente (por ejemplo, con websocket ), como preguntarle al usuario dónde guardar el archivo y luego comenzar un flujo constante de datos desde el servidor al cliente ( ciencia ficción , supongo)?
  • cualquier otro problema relacionado conocido o mejores prácticas que crucen su mente sería muy apreciado.

Para contenido grande que no cabe en la memoria a la vez, transmita el contenido de la base de datos a la respuesta.

Este tipo de cosas es bastante simple. No necesita AJAX ni websockets, es posible transmitir grandes archivos a través de un enlace simple en el que el usuario hace clic. Y los navegadores modernos tienen administradores de descargas decentes con sus propias barras de progreso. ¿Por qué reinventar la rueda?

Si escribe un servlet desde cero para esto, obtenga acceso a la base de datos BLOB, obtenga su flujo de entrada y copie el contenido a la secuencia de salida de respuesta HTTP. Si tiene la biblioteca Apache Commons IO, puede usar IOUtils.copy () ; de lo contrario, puede hacerlo usted mismo.

La creación de un archivo ZIP sobre la marcha se puede hacer con un ZipOutputStream . Cree uno de estos en la secuencia de salida de respuesta (desde el servlet o cualquiera que sea su estructura), luego obtenga cada BLOB de la base de datos, usando putNextEntry() primero y luego transmitiendo cada BLOB como se describió anteriormente.

Posibles trampas / problemas:

  • Según el tamaño de la descarga y la velocidad de la red, la solicitud puede tardar mucho tiempo en completarse. Los firewalls, etc. pueden interferir con esto y terminar la solicitud anticipadamente.
  • Es de esperar que sus usuarios estén en una red corporativa decente cuando soliciten estos archivos. Sería mucho peor que las conexiones remotas / dodgey / móviles (si se desconecta después de descargar 1.9G de 2.0G, los usuarios tienen que comenzar de nuevo).
  • Puede poner un poco de carga en su servidor, especialmente la compresión de grandes archivos ZIP. Podría valer la pena bajar la compresión al crear ZipOutputStream si esto es un problema.
  • Los archivos ZIP de más de 2 GB (o 4 GB) pueden tener problemas con algunos progtwigs ZIP. Creo que el último Java 7 usa extensiones ZIP64, por lo que esta versión de Java escribirá el enorme ZIP correctamente, pero ¿los clientes tendrán progtwigs que admitan los grandes archivos zip? Definitivamente he tenido problemas con esto antes, especialmente en servidores antiguos de Solaris

Ejemplo inicial de un archivo ZIP totalmente dynamic creado al transmitir cada BLOB de la base de datos directamente al sistema de archivos del cliente.

Probado con enormes archivos con las siguientes actuaciones:

  • Costo de espacio en disco del servidor: 0 MegaBytes
  • Costo de RAM del servidor: ~ xx Megabytes. el consumo de memoria no es comprobable (o al menos no sé cómo hacerlo correctamente), porque obtuve resultados diferentes, aparentemente aleatorios, al ejecutar la misma rutina varias veces (usando Runtime.getRuntime().freeMemory() ) antes, durante y después del ciclo). Sin embargo, el consumo de memoria es menor que con byte [], y eso es suficiente.

FileStreamDto.java usando InputStream lugar de byte[]

 public class FileStreamDto implements Serializable { @Getter @Setter private String filename; @Getter @Setter private InputStream inputStream; } 

Java Servlet (o Struts2 Action)

 /* Read the amount of data to be streamed from Database to File System, summing the size of all Oracle's BLOB, PostgreSQL's 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 entries = new HashSet(); /* 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 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(); 

Método de ayuda para manejar entradas duplicadas

 private String getUniqueFileName(HashSet 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; } 

Muchas gracias @prunge por darme la idea de la transmisión directa.

Puede ser que quiera probar varias descargas al mismo tiempo. Encontré una discusión relacionada con esto aquí: rendimiento de descarga de archivos multiproceso de Java

Espero que esto ayude.