Descarga de Archivos con barra de Progreso

En este artículo veremos como descargar uno o varios archivos, usando el API Fetch de Javascript, y mostrando el progreso de avance de la descarga, en KB y %.


Para descargar archivos de internet mediante Javascript, ya sea de nuestro propio backend o desde el API de un tercero, podemos usar tanto el viejo XMLHttpRequest, como el más reciente API Fetch. En ambos casos es posible tener una actualización «en vivo» de cuantos bytes se han descargado, de forma que podamos informarnos del avance de la descarga.

Veamos como sería el código usando el Fetch API:

const respuesta = await fetch('https://picsum.photos/800/800');
if (respuesta.ok){
    throw new Error(respuesta.textContent); 
}

const lector = respuesta.body.getReader();
const totalBytes = respuesta.headers.get('content-length');

//Aquí pondremos los bytes de la respuesta
let controlador;
const flujoLectura = new ReadableStream({
    start(controller){
        controlador = controller;
    }
});

let avanceBytes = 0;
while (true) {
    const { done, value } = await lector.read()
        
    if (done) {//al terminar de leer: salir del bucle
        controlador.close();
        break
    }

    controlador.enqueue(value)

    avanceBytes += value.byteLength;
    calbackProgreso.apply({}, [avanceBytes, totalBytes]);

}
 
//Al terminar: generar un Blob con el archivo obtenido
const flujoResp = new Response(flujoLectura);
const archivo = await flujoResp.blob();

//Mostramos la vista previa en un control img
imagen.src = URL.createObjectURL(archivo );

Analicemos por partes lo qué hace este fragmento de código. Veamos primero al función Fetch:

  • Usaremos picsum para generar una imágen aleatoria de 800×800 px. Puedes usar otro servicio, o tus propias imágenes para probar.
  • La función Fetch devuelve una promesa, así que usamos await para esperar a que se resuelva y obtener la respuesta. Importante: La promesa se resuelve en cuanto el servidor responde con los encabezados (Headers), pero en ese punto no necesariamente ha enviado el cuerpo, que en nuestro caso es un archivo.
  • Si la respuesta tiene estatus de error (fuera del rango 200-299), la propiedad Response.ok será false, y en ese caso lanzamos una excepción.

Ahora estamos listos para comenzar a leer usando la variable lector:

  • El cuerpo de la respuesta se puede leer de diferentes formas, pero como queremos hacerlo de forma asíncrona usaremos body.getReader, que devuelve un ReadableStream<Uint8Array>.
  • Leemos la longitud total totalBytes enviada por el servidor, que se encuentra en el encabezado content-length de la respuesta.
  • Creamos otro ReadableStream, al que llamaremos flujoLectura: aquí colocaremos los bytes a medida que leamos. Al final, lo usaremos para crear un Blob con su contenido, y lo mostraremos en un elemento img para tener una vista previa.
  • Ahora, llamamos la función lector.read() en un bucle, mientras haya algo qué leer: el valor leído lo encolaremos en el controlador que está dentro de flujoLectura. La variable value también contiene la longitud del bloque de bytes que leemos en cada bloque, así que la guardaremos en el acumulador avanceBytes.
  • Dentro del mismo bucle de lectura, llamaremos a la función mostrarProgreso, que describiremos a continuación.

La siguiente función simplemente actualizará una barra de progreso en pantalla a medida que leamos las partes del archivo que se vayan descargando:

const mostrarProgreso = (avance, totalBytes) =>{

    const porcentaje = Math.round(avance / totalBytes * 100);
    barra.value = porcentaje;
    barra.textContent = porcentaje + '%';
    etiqueta.textContent = porcentaje + '% (' + (avance/1024).toFixed(2) + ' KB)';
}; 

Esta función recibe el acumulado de bytes que hemos recibido hasta ahora, y el total de bytes, según lo indicado en la cabecera. Con esto, calcula el porcentaje de avance, tanto para el valor de la barra como para la etiqueta que pusimos al lado. Como información adicional, también colocamos la cantidad de bytes descargardos hasta el momento.

La interface: HTML

Ahora veamos el HTML:

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>Descarga con Progreso</title>
    <link href="style.css" rel="stylesheet"></link>
</head>
<body>
    <main>
        <div>
            <button id="btnCargar">Cargar Imagen</button>
        </div>
            
        <div id="progreso" >
            <progress id="progreso__barra" max="100">0%</progress>
            <label id="progreso__etiqueta" for="progreso__barra">conectando...</label>
        </div>
        <img id="imagen" src="">
    </main>
  
    </div>
    <script> <!-- Javascript: aquí o en un archivo externo -->
</script>
 </body>
</html>

La interface es bastante simple: un botón para iniciar la descarga de la imagen, Una barra de progreso, con su respectiva etiqueta para mostrar el % y total de bytes durante la descarga, y un control img para mostrar la vista previa.

El CSS no es relevante para este ejemplo, pero si quieres puedes ver el código completo en este repositorio: https://github.com/roimergarcia/20230822—descarga-con-progreso

Cuando hacemos clic en el botón cargar, se inicia la petición de la imagen. Hasta el momento que obtengamos las cabeceras HTTP, el usuario verá el mensaje «cargando» y la barra e progreso tendrá un valor indeterminado (en Chrome, se verá una barra azul moviendose de lado a lado).

Cuando comienza la descrga del contenido de la imagen, podremos ver el porcentaje de avance de la descarga, y la cantidad de bytes descargados hasta el momento:

Finalmene, cuando la descarga termina, la barra muestra un 100% de descarga, y se muestra una vista previa de la imagen final:

Multiples Archivos

En el ejemplo anterior hicimos una petición básica con Fetch, un simple GET para obtener un archivo de imagen. Este mismo código funciona para cualquier otro tipo de archivo, aunque tal vez haya que modificar la parte de la vista previa en el img.

Para descargar varios archivos será necesario modificar solo la última parte, pero la modificación depende de cómo el servidor envíe los archivos. Por ejemplo, si el servidor envía los archivos como un Array, donde cada ítem es un Array de bytes, podemos hacer lo siquiente:

//Para un archivo: 
const archivo = await flujoResp.blob();

//Para varios archivos en un Array JSON
//Supongamos que todos son imágenes JPEG
const archivos = await flujoResp.json();
for each (var bloque in archivos) {
  const archivo = new Blob(bloque, {type: 'image/jpeg' })
  //Aquí: hacer algo con cada archivo
}

Referencia

  • Repositorio con código completo: https://github.com/roimergarcia/20230822—descarga-con-progreso
  • Fetch API: https://developer.mozilla.org/es/docs/Web/API/Fetch_API/Using_Fetch

Derecho de uso

Los contenidos generados por el autor de este artículo (explicaciones, código fuente, y archivos adjuntos creados por el autor) están disponibles bajo licencia CC BY-SA 3.0, y pueden ser usados, derivados y compartidos bajo los términos indicados en la misma. Los contenidos no generados por el autor de este artículo son propiedad de sus respectivos dueños y están regidos por las licencias que estos hayan dispuesto.


Deja una respuesta

Tu dirección de correo electrónico no será publicada. Los campos obligatorios están marcados con *

Soluciones claras y simples



Ing. Industrial, dedicado a la programación en ASP.NET+VB, SQL y Javascript+AJAX; y un poco de Android, Kotling, y Unity 🙂
Valencia, Venezuela



¿QUIERES APOYARME?

¿Te ha sido de ayuda alguno de mis artículos? Generar contenido técnico requiere de tiempo y esfuerzo. Con tu colaboración me puedes ayudar a mantener mi blog activo y actualizado. Si quieres y puedes apoyarme has clic aquí:

https://paypal.me/roimergarcia


ENTRADAS RECIENTES