Trabajando con ficheros (JavaScript)

TEORÍA

Trabajar con ficheros en JavaScript consiste principalmente de:

  • Subir ficheros
  • Previsualizar ficheros en el DOM y
  • Descargar ficheros

Otras operaciones (como borrar) son propias del back-end y por ende no tiene mayor importancia que realizar una petición al servidor para que borre un fichero concreto.

Para la parte de la subida de ficheros, es importante entender cómo trabajan los navegadores y los servidores que siguen el estándar RFC-1867 (y sus posteriores revisiones). Puedes leer la sección teórica de subida de ficheros de PHP para entender esto: «Trabajando con ficheros en PHP«.

PRÁCTICA

SUBIDA DEL FICHERO (AJAX)

Ya que trabajamos con JavaScript, realizaremos una petición HTTP POST asíncrona con AJAX (sino sería tan sencillo como crear un formulario en HTML).

Para ello, el formulario con el cual vamos a mandar el fichero, debe indicar que el tipo de contenido es «multipart/form-data«.

Si además se quiere seleccionar más de un fichero a la vez en el formulario, se debe añadir el atributo «multiple» en el input «file» además de poner dos corchetes («[ ]») al final del nombre para que añada múltiples valores al input.

<form id="file_upload_form" enctype="multipart/form-data" action="url.php" method="POST">
    Fichero: <input name="mi_fichero[]" id="fichero" type="file" multiple/>
    <input type="submit" value="Enviar" />
</form>

Ahora debemos recoger los datos para mandarlos por AJAX.

Objeto FormData

Aquí es donde entra en juego el objeto predefinido en HTML5 llamado «FormData«, que serializa los datos de un formulario, incluido los ficheros, para luego mandarlos en la petición HTTP asíncrona.

Hay dos formas de usarlo:

  • Apuntar directamente al formulario (al nodo <form>).
  • Recoger los datos del formulario que deseemos manualmente.

Apuntar directamente al formulario: esto serializará todos los inputs del formulario como «application/x-www-form-urlencoded» por defecto o bien como el tipo indicado en el atributo «enctype» del formulario.

var formData = new FormData(document.getElementById('file_upload_form'));

Recoger los datos manualmente: para esto habrá que crear un objeto FormData vacío e ir metiéndole los datos con el método «append(<nombre_input>, <valor_input>)«. Nótese que para los inputs que no sean ficheros hay que añadirle el atributo «value» de dicho input; sin embargo, para los ficheros, hay que añadirle el atributo «files[i]» del input. El atributo «files» es un array que contiene los metadatos de cada uno de los ficheros que se han seleccionado en el input de tipo file. Podemos mandar todos los ficheros en una sola petición HTTP o bien mandar un fichero por cada petición HTTP (normalmente se mandan todos los ficheros juntos cuando la lógica de la operación requiere todos los ficheros a la vez, sino, lo ideal es mandarlos por separado ya que, en caso de fallo, el rollback de la grabación del fichero es mucho más sencillo).

// Mandar un fichero por cada petición HTTP
var filesInput = document.getElementById('fichero');
for (var i = 0; i < filesInput.files.length; i++) {
	var formData = new FormData(); 
	formData.append('input_text', document.getElementById('texto').value);
	formData.append('input_file', filesInput.files[i]);
	// Mandar formData por Ajax
}

// Mandar todos los ficheros en la petición HTTP
// En este caso sería tan sencillo como crear el
// objeto FormData apuntando al formulario, pero,
// si lo necesitamos introducir manualmente...
var filesInput = document.getElementById('fichero');
var formData = new FormData(); 
formData.append('input_text', document.getElementById('texto').value);
for (var i = 0; i < filesInput.files.length; i++) {
	formData.append('input_file[]', filesInput.files[i]);
}
// Mandar formData por Ajax

Finalmente quedaría mandar el objeto FormData mediante AJAX.

Nota: Si quieres indicar explícitamente en la cabecera del objeto XMLHttpRequest que el tipo de Content-Type a mandar es multipart/form-data, debes incluir el atributo «boundary» o se producirá un error. Por defecto, si no lo incluyes, se añade el Content-Type adecuado y su boundary de forma implícita.

var xhttp = new XMLHttpRequest();
xhttp.onreadystatechange = function(){...};
xhttp.open('POST', 'url.php', true);
//Opcional
//xhttp.setRequestHeader('Content-Type', 'multipart/form-data; boundary=--SEPARADOR');
// Mandar la petición HTTP con el objeto FormData
xhttp.send(formData);

IMPORTANTE: El formulario por defecto provocará que se navegue hacia la página donde se procesa la petición HTTP. Para evitar esto y conseguir que solamente se mande asíncronamente (mediante AJAX) hay que prevenir el evento por defecto («event.preventDefault()«).

documnent.getElementById('file_upload_form').onsubmit = uploadFormOnSubmit;
function uploadFormOnSubmit(event){
  // Cancelamos la operación por defecto
  event.preventDefault();
  // Crear el objeto FormData y mandarlo por AJAX
}
Progreso de subida

Cuando subimos ficheros, es habitual querer saber qué porcentaje se ha subido en cada momento.

El objeto XMLHttpRequest tiene un atributo llamado «upload«, que no es más que un objeto que devuelve información del estado de la subida de los datos mandados a través de AJAX.

Entonces, para mantener una barra de progreso de la subida de los datos que queremos mandar al servidor, se deben añadir los siguientes «EventListeners» al objeto «upload«:

  • progress: periódicamente se lanza este evento que indica la cantidad total de bytes que se ha transmitido al servidor.
  • load: cuando termina la subida de los datos pero antes de que se devuelva la respuesta de la petición HTTP.
  • error: cuando la subida ha fallado (motivo desconocido).
  • timeout: cuando la subida ha tardado tanto que se ha devuelto un estado de timeout.
xhttp.upload.addEventListener("progress", function(){...});
xhttp.upload.addEventListener("load", function(){...});
xhttp.upload.addEventListener("error", function(){...});
xhttp.upload.addEventListener("timeout", function(){...});

Para mantener actualizada una barra de progreso, en las funciones de cada evento, se debe manipular un nodo HTML de tipo «progress» de forma adecuada. Se pueden usar otras barras de progreso no estándar creadas mediante CSS, pero, para simplificarlo, vamos a usar la etiqueta «progress«.

Por ejemplo:

xhttp.upload.addEventListener("progress", function(){
  // event.loaded indica cuánto se ha subido al servidor
  // event.total indica cuánto se está intentando enviar
  var pc = parseInt((event.loaded / event.total * 100));
  // progressBar = nodo <progress> de HTML
  progressBar.value = pc;
  // progressPercentaje = nodo de texto donde poner el porcentaje
  progressPercentaje.innerHTML = pc + '%';
});

Cuando se reciba el evento «load«, deberíamos crear un loader infinito, esto es, un indicador de progreso cuyo tiempo y porcentaje de ejecución es indeterminado, ya que en este momento se está procesando la petición en el servidor y no podemos saber cuánto va a tardar en terminar.

Por ejemplo:

// Si usamos font awesome tenemos un spinner loader
// a modo de icono, fácil de meter en una misma línea.
// En la misma línea donde ponía el porcentaje del progreso 
// podríamos poner algo como esto:

// CSS:
// Importar CSS de font awesome
<link rel="stylesheet" href="https://use.fontawesome.com/releases/v5.2.0/css/all.css" integrity="sha384-hWVjflwFxL6sNzntih27bfxkr27PmbbK/iSvJ+a4+0owXq79v+lsFkW54bOGbiDQ" crossorigin="anonymous">

// JS:
// Los datos están en el servidor pero aún no se ha recibido 
// respuesta HTTP
xhttp.upload.addEventListener("load", function(){
    progressPercentaje.innerHTML = '<i class="fa fa-spinner fa-pulse"></i> Guardando...';
});

Y finalmente, cuando la petición HTTP nos devuelva una respuesta, deberíamos indicar que el fichero (o ficheros, si los mandamos todos en una sola petición HTTP) ya se ha subido.

PREVISUALIZAR FICHEROS

Para ver los ficheros en el DOM del documento HTML, se deben crear los nodos adecuados apuntando a la URL del archivo (o del script del servidor que nos dé el fichero).

Es importante saber el tipo de fichero que se va a emplear para saber cómo tratarlo, pero esencialmente hay 3 tipos de ficheros: vídeo, audio e imagen:

<!-- vídeo -->
<video controls>
  <source src="URL_AL_FICHERO" type="MIME">
</video>

<!-- audio -->
<audio controls>
  <source src="URL_AL_FICHERO" type="MIME">
</audio>

<!-- imagen -->
<img src="URL_AL_FICHERO">

Donde pone URL_AL_FICHERO puede ser un acceso directo al fichero (por ejemplo: www.example.com/media/nombre_fichero.jpg) o bien puede ser un script del servidor que nos devolverá el fichero si estamos autorizados a visualizarlo (por ejemplo:

www.example.com / ajax / files / download.php ? file_name = nombre_fichero.jpg).

Donde pone MIME debemos indicar el tipo de objeto MIME que tiene el fichero (debería proporcionarlo el servidor en lugar de tener que calcularlo cada vez que se quiera visualizar el fichero).

Entonces, en JavaScript, crear la vista es tan sencillo como:

function getFile(mime_type, file_name){
	function newNode(node){return document.createElement(node);}
	switch(mime_type){
		case 'video/mp4':
		case 'video/mkv':
			var video = this.newNode('video');
			video.setAttribute('controls', '');
			var source = this.newNode('source');
			source.setAttribute('src', file_name);
			source.setAttribute('type', mime_type);
			video.appendChild(source);
			return video;
			break;

		case 'audio/mpeg':
		case 'audio/wav':
			var audio = this.newNode('audio');
			audio.setAttribute('controls', '');
			var source = this.newNode('source');
			source.setAttribute('src', file_name);
			source.setAttribute('type', mime_type);
			audio.appendChild(source);
			return audio;
			break;

		case 'image/jpeg':
		case 'image/gif':
		case 'image/bmp':
		case 'image/png':
			var img = this.newNode('img');
			img.setAttribute('src', file_name);
			return img;
			break;
		default:
			console.warn('Tipo de fichero no controlado: '+mime_type);
			return null;
			break;
	}
}

DESCARGAR FICHERO

Un fichero se puede descargar mediante el menú contextual del puntero del ratón (click derecho, «Guardar imagen/vídeo como…»), sin embargo, si por algún motivo queremos ofrecerle un botón al usuario para que realice esta tarea directamente, sólo hay que abrir la URL del archivo con el método «window.open()» o modificando el atributo «window.location» (este último impedirá la creación y destrucción de una pestaña en el navegador antes de descargar el fichero).

// Si usas window.open() provocará que se abra una nueva pestaña y luego
// preguntará si quieres descargar el fichero. Con este método no abre pestaña

// Si el archivo está detrás de un script
window.location = 'www.example.com/ajax/files/download.php?file_name='+fileName;

// Si el archivo es directamente accesible
window.location = 'www.example.com/media/'+fileName;

NOTA: La respuesta HTTP con el fichero debe tener en su cabecera «Content-Disposition: attachment;» para que el navegador entienda que es un fichero «adjunto» y que no queremos abrir una pestaña que muestre el fichero, sino descargarlo.

DESCARGAS

Puedes descargar un ejemplo para el manejo de ficheros en JavaScript aquí (incluye una implementación básica del manejo de ficheros en el back-end).

 

 

Deja una respuesta