File API: Manejando contenidos de archivo en HTML5

Leer archivo con File API La especificación File API expone una API para representar objetos de archivo en aplicaciones web. Puede gestionar la lista de archivos seleccionados por el usuario y acceder a los datos de esos archivos. Esta especificación resume al principio los componentes de la API. Primero define una interfaz FileList que representa un array de archivos seleccionados. Luego otra interfaz Blob que representa datos binarios sin tratar y que no son modificables, permitiendo el acceso a rangos de bytes dentro de ese objeto. La interfaz File incluye información de atributos del archivo pero de sólo lectura (p.e. nombre de archivo o fecha de modificación). La interfaz FileReader provee métodos para leer un archivo a un objeto File o Blob con eventos para obtener el resultado. Por último define un URI scheme para usar con datos binarios como los archivos y así poder ser referenciados en las aplicaciones web. En la imagen puede ver como podemos acceder a un archivo de texto de nuestro ordenador y leer su contenido.

Esta API surge por la necesidad de otras donde se manejan archivos como AJAX (XMLHttpRequest) donde podemos usar el método send() para enviar un archivo al servidor. También se puede transferir un objeto File o Blob con el paso de mensajes con el método postMessage() (Cross-document messaging). También puede ser necesario con la técnica que permite clonar y transferir ArrayBuffers, en este caso para transferir el contenido de un archivo. Estas últimas pueden usarse en Web Workers donde la utilidad de cargar datos de archivo puede necesitar esta File API. Por último en el elemento y tipo <input type="file"> podemos tener acceso a la lista de archivos seleccionados en el control por el usuario.

Vea que en ningún momento esta API permite escribir archivos. Podemos denominarla como una API para leer archivos. Hay otra para escribir archivos File API: Writer. En incluso una que combina ambas File API: Directories and System. Las tres están en fase WD, la primera bastante soportada por los principales navegadores pero las dos últimas actualmente sólo las soporta Chrome. A la vista del nivel de soporte, he usado en mi aplicación compositor de imágenes y generador Sprites CSS sólo la File API de lectura. El usuario puede cargar archivos de imagen y de texto desde el sistema. Se pueden modificar en la aplicación y guardarlos otra vez, pero no de forma directa:

  • Podemos guardar las imágenes usando el menú contextual del navegador. O bien si éste soporta la API Drag and Drop, arrastrar la imagen desde la ventana del navegador hasta una carpeta del sistema donde se guardará como un archivo de imagen.
  • Para los contenidos de texto no hay más remedio que seleccionar el texto de un <textarea>, copiarlo, abrir un documento de texto en el sistema y pegarlo.

Así estas acciones de escribir archivos en mi aplicación son llevadas a cabo de forma manual por el usuario. Las otras API's que permiten escribir archivos en el sistema suponen un riesgo de seguridad y habría que conocerlas primero en profundidad antes de hacer uso de ellas. En resumen, con la File API de lectura y en lo que respecta a la seguidad del acceso a archivos hemos de decir que éstos han de ser necesariamente seleccionados por el usuario y que no hay otra forma de interaccionar con los archivos del sistema. Hay dos formas para que el usuario seleccione un archivo, una es con el <input type="file">, la otra es arrastrando un archivo hasta una área de la página y soltándolo, en este caso aplicando la API Drag and Drop. En todo caso hay que tener en cuenta algunas cuestiones de seguridad que expone la especificación File API en el apartado Security Considerations.

La especificación sigue un orden diferente al presentar las interfaces, aunque de forma resumida la relación que hay entre ellas es la siguiente:

  • Blob representa datos de archivo no modificables. Podríamos pensar en esto como el contenido de un archivo, una serie de bytes consecutivos sin tratar (raw) y que no se pueden modificar (immutable).
  • File representa un archivo y hereda de Blob. Podemos decir que esta interfaz agrega los atributos de archivo como el nombre y la última fecha de modificación (por ahora sólo estos) a un archivo cuyos datos están cargados en un objeto Blob.
  • FileList representa una lista de objetos File.
  • FileReader nos permite leer objetos File o Blob.
Puede ver el soporte de File API (FileReader) en el sitio html5test.com. O bien en caniuse.com. Lo he probado satisfactoriamente en Chrome 22, Firefox 16 y Opera 12 (con algún comportamiento diferente en el Drag and Drop en éste último). En Internet Explorer parece que está disponible en la versión 10. Safari 5 no soporta FileReader pero si File y FileList. Parece que la versión 6 si soporta FileReader. Estos últimos navegadores no los tengo instalados.
La página Html5Rocks: Reading files in JavaScript using the File APIS de @ebidel [Eric Bidelman] tiene ejemplos muy prácticos sobre este tema y me ha sido de gran ayuda.

File API: El archivo y la lista de archivos (File, FileList)

La interfaz FileList representa una lista de objetos File. La única forma de cargar una lista de archivos es con los métodos que ya dije antes, <input type="file"> o Drag and Drop. Veámos un ejemplo:

Ejemplo:

El código de este ejemplos es el siguiente:

<div class="ejemplo-linea">
    <input type="file" id="boton-file" accept="image/*"
    multiple="multiple" />
    <input type="button" value="listar" onclick="actualizarLista()" />
    <pre id="lista-archivos"></pre>
</div>
<script>
    function actualizarLista(){
        var inpute = document.getElementById("boton-file");
        var lista = inpute.files;
        var cad = "Total archivos:" +lista.length + "<br />";
        for (var i=0; i<lista.length; i++){
            if (cad != "") cad += "------------------<br />";
            cad += "Nombre: " + lista[i].name + "<br />" +
                   "Tipo: " + lista[i].type + "<br />" +
                   "Tamaño: " + lista[i].size + " bytes<br />" +
                   "Fecha: " + lista[i].lastModifiedDate + "<br />";
        }
        document.getElementById("lista-archivos").innerHTML = cad;
    }
</script>

El <input type="file"> tiene el atributo booleano multiple que permite seleccionar varios archivos. El atributo accept informa al navegador para que sólo muestre archivos con ese Mime Type, archivos de imagen de cualquier clase. Aunque no impide seleccionar otros tipos de archivo. Una vez dentro de la lista de selección deberíamos controlar en el JavaScript los tipos con los que vamos a operar. El botón "listar" accede a la lista files de los archivos seleccionados en ese elemento. Realmente esta lista es un objeto FileList. Vea como iteramos por la lista de archivos y con cada objeto File accedemos al nombre y fecha de última modificación del archivo. Dado que el objeto File hereda de Blob, éste tiene las propiedades type y size que actualiza automáticamente el navegador cuando selecciona el archivo. Con ellas accedemos al Mime Type y al tamaño del archivo. La propiedad lenght del FileList nos da el número de archivos seleccionados.

No puede obtenerse la ruta completa del archivo, solo el nombre. A pesar de que Opera o Firefox muestran la ruta completa junto el boton file, sólo podemos acceder al nombre. Esto es por motivos de seguridad y privacidad, pues nadie tiene porqué saber en qué carpeta estaba almacenado ese archivo, especialmente cuando se envía a un servidor.

En los navegadores que lo permitan (Chrome 22, Firefox 16) se puede arrastrar uno o varios archivos y soltarlos encima del botón, acción que hace lo mismo que seleccionarlos en el sistema. La acción no es acumulativa, es decir, una vez seleccionados si ejecutamos una nueva selección la anterior es eliminada del FileList.

File API: Lector de archivos (FileReader)

En el apartado anterior vimos como seleccionar un archivo del sistema. Ahora vamos a leerlos con FileReader. Dispone de los siguientes métodos:

  • Leer texto: readAsText(blob, [encoding]). Lee el archivo como texto. El argumento blob es un referencia a un objeto File, es decir, uno de los archivos de la lista que selecciona el usuario. Opcionalmente se puede indicar una codificación como UTF-16. Si no se declara el navegador tratará de determinarla bien por el Mime Type o con las marcas BOM. Si no puede determinar la codificación usará por defecto UTF-8.
  • Leer datos URL: readAsDataURL(blob). Los datos se codifican en base64 y se pasan como texto, independientemente del tipo de datos de que se trate.
  • Leer ArrayBufffer: readAsArrayBuffer(blob). Los datos se leen como una serie consecutiva de bytes. Esto es útil para usar en combinación con los Web Workers.
  • Abortar lectura: abort(). Permite abortar un proceso de lectura.

Veámos un ejemplo. En este caso sólo vamos a permitir un único archivo en la selección para no complicar el ejemplo:

Ejemplo:


Leer como texto datos URL array buffer
Tipo MIME:
Longitud: bytes

Código de este ejemplo:

<div class="ejemplo-linea">
    <input type="file" id="boton-file2" /><br />
    Leer como
    texto<input type="radio" name="read-as" value="text" checked="checked" />
    datos URL<input type="radio" name="read-as" value="data-url" />
    array buffer<input type="radio" name="read-as" value="array-buffer" />
    <input type="button" value="leer" onclick="leerArchivo()" /><br />
    Tipo MIME: <span id="tipo-mime" class="codigo"></span><br />
    Longitud: <span id="longitud" class="codigo"></span> bytes<br />
    <textarea rows="10" style="width: 98%" class="codigo"
    id="contenido-archivo"></textarea>
</div>
<script>
    function leerArchivo(){
        var inpute = document.getElementById("boton-file2");
        if (inpute.files.length > 0){
            var radios = document.getElementsByName("read-as");
            var opcion = "text";
            for (var i=0; i<radios.length; i++){
                if (radios[i].checked) {
                    opcion = radios[i].value;
                    break;
                }
            }
            var archivo = inpute.files[0];
            document.getElementById("tipo-mime").textContent = archivo.type;
            document.getElementById("longitud").textContent = archivo.size;
            var lector = new FileReader();
            lector.addEventListener("load",
                function(evento){
                    var cadena  = evento.target.result;
                    if (cadena.length>50000){
                        cadena = cadena.substr(0,50000) +
                        "\r\n..........Sólo se presentan 50000 bytes para no colapsar el navegador......";
                    }
                    document.getElementById("contenido-archivo").value = cadena;
                }, false);
            if (opcion=="text"){
                lector.readAsText(archivo);
            } else if (opcion=="data-url"){
                lector.readAsDataURL(archivo);
            } else {
                lector.readAsArrayBuffer(archivo);
            }
        }
    }
</script>

Creamos un nuevo FileReader, declaramos un manejador de evento onload y leemos el archivo según la opción seleccionada. Cuando el contenido esté cargado se enciende el evento load y podemos recogerlo desde result del evento y pasarlo al <textarea>. El FileReader dispone de los siguientes eventos que se desencadenan con los métodos señalados antes:

  • loadstart, se activa con el inicio de la lectura.
  • progress, mientras está leyendo y en su caso decodificando, va ofreciendo información acerca del progreso de la carga. Hay una especificación sobre los Eventos de Progreso Progress Events.
  • abort, se activa cuando la lectura ha sido abortada por algún error o bien invocando el método abort().
  • error, se activa en casos de error.
  • load, se activa cuando la lectura finaliza satisfactoriamente.
  • loadend, se activa cuando se finaliza la lectura, bien por terminar satisfactoriamente o en fallo.

Los tres métodos disponibles en el FileReader de lectura readAsText(), readAsDataURL() y readAsArrayBuffer() son asíncronos. Por esta razón hemos de declarar un manejador de evento load pues la lectura no la tenemos disponible de forma inmediata tras ejecutar alguno de esos métodos. La especificación en su apartado 9. Reading on Threads define también los tres métodos anteriores para ser usados de forma síncrona. Se trata del objeto de lectura FileReaderSync especialmente concebido para usar en un Web Worker. Ahí no necesitamos que sea asíncrono, pues cuando la lectura termine satisfactoriamente o por error el Worker nos podría devolver el mensaje adecuado. Es en un Web Worker donde cobra sentido el metodo readAsArrayBuffer(), pues es la estructura óptima para usar con los Workers si hemos de devolver el contenido leído a la página principal.

Por último señalar que aunque el archivo se lee completo sólo se presentan en pantalla los primeros 50000 bytes para no bloquear el navegador (cargar muchos MB en el DOM puede colapsarlo).

File API con Drag and Drop: Arrastrar y soltar archivos

Ya hemos dicho que cuando seleccionamos un archivo, éste estará disponible en el objeto FileList del control <input type="file">. Aunque otra forma de seleccionar archivos es usando Drag and Drop. En navegadores como Firefox y Chrome podemos arrastrar y soltar un archivo encima de un botón de tipo file. No funciona en Opera ejecutándose la acción por defecto que es abrir el contenido del archivo en la ventana. Estos efectos los puede comprobar en los ejemplos anteriores, arrastrando un archivo sobre el botón tipo file. Si el navegador permite Drag and Drop podemos usar un elemento de la página para también soltar el archivo. El tipo de elemento a usar es importante, porque creo que por ejemplo Opera 12 no permite soltar archivos en un <textarea>. En este ejemplo usaremos un <div> para recibir el soltado del archivo:

Ejemplo:

Leer como texto datos URL array buffer
Tipo MIME:
Longitud: bytes
Arrastre un archivo aquí.

El código del ejemplo es similar al anterior, sólo cambia que no hay ningún botón <input type="file"> y que ahora tenemos un elemento <div> donde recibir el soltado del archivo y exponer ahí el contenido de texto.

<div class="ejemplo-linea">
    Leer como
    texto<input type="radio" name="read-as3" value="text" checked="checked" />
    datos URL<input type="radio" name="read-as3" value="data-url" />
    array buffer<input type="radio" name="read-as3" value="array-buffer" /><br />
    Tipo MIME: <span id="tipo-mime3" class="codigo"></span><br />
    Longitud: <span id="longitud3" class="codigo"></span> bytes<br />
    <div class="codigo" id="contenido-archivo3" wrap="off"
    style="white-space: pre; height: 10em; overflow: auto; border: gray solid 1px;">
        Arrastre un archivo aquí.
    </div>
</div>
<script>
    //Leemos archivos. En el arrastre viene la colección de archivos pues se pueden
    //seleccionar y arrastrar varios. Aquí sólo cogeremos el primero.
    function leerArchivo3(files){
        if (files.length > 0){
            var radios = document.getElementsByName("read-as3");
            var opcion = "text";
            for (var i=0; i<radios.length; i++){
                if (radios[i].checked) {
                    opcion = radios[i].value;
                    break;
                }
            }
            var archivo = files[0];
            document.getElementById("tipo-mime3").textContent = archivo.type;
            document.getElementById("longitud3").textContent = archivo.size;
            var lector = new FileReader();
            lector.addEventListener("load",
                function(evento){
                    var cadena  = evento.target.result;
                    if (cadena.length>50000){
                        cadena = cadena.substr(0,50000) +
                        "\r\n..........Sólo se presentan 50000 bytes para no colapsar el navegador......";
                    }
                    document.getElementById("contenido-archivo3").value = cadena;
                }, false);
            if (opcion=="text"){
                lector.readAsText(archivo);
            } else if (opcion=="data-url"){
                lector.readAsDataURL(archivo);
            } else {
                lector.readAsArrayBuffer(archivo);
            }
        }
    }
</script>

Para completar el funcionamiento del ejemplo hemos de declarar el manejador del evento drop sobre el área que va a recibir el archivo. Lo hacemos con este codigo en el window.onload donde hemos dejado sólo lo que interesa:

<script>
window.onload = function(){
    ...............
    var ta3 = document.getElementById("contenido-archivo3");
    //Anulamos el drag en toda la página (CH no necesita esto pero FF, OP sí)
    document.body.addEventListener("dragover",
        function (evento){
            //Pero no en botones file (CH y FF)
            if (evento.target.type!="file"){
                evento.preventDefault();
                return false;
            }
        }, true);
    //Anulamos el  drop en toda la página
    document.body.addEventListener("drop",
        function (evento){
            //Pero no en botones file (CH y FF)
            if (evento.target.type!="file"){
                evento.preventDefault();
                return false;
            }
        }, true);
    //Manejamos drop del DIV
    ta3.addEventListener("drop",
        function (evento){
            evento.stopPropagation();
            evento.preventDefault();
            leerArchivo3(evento.dataTransfer.files);
        } , false);
    ...............
};
</script>

Adjudicamos un evento drop al <div> que va a recibir el contenido del archivo (variable ta3). Detenemos posibles propagaciones del evento así como el comportamiento por defecto del navegador y pasamos a leer el archivo con leerArchivo3(), pues el evento drop nos trae la lista de ficheros en dataTransfer.files. Esto lo hacemos en el window.onload con la carga de la página pues también aprovechamos para desactivar arrastre y soltado en otras partes de la página. El comportamiento por defecto en los navegadores al soltar un archivo es abrirlo en la ventana. De esta forma conseguimos que no se lleve a cabo esto en toda la página a excepción de los botones tipo file de los ejemplos anteriores, donde aún nos interesa recibir el drop. Esto sólo para Chrome y Firefox, pues en Opera al soltar sobre un botón tipo file o incluso sobre cualquier textarea se abre la ventana y realmente no sé el motivo.

Se observa que el FileReader hace todas las lecturas como texto. Incluso los datos binarios los intenta leer como UTF-8. No sé hasta qué punto puede suponer un riesgo esto si luego ese archivo se envía a un servidor en un proceso siguiente aunque sólo sea por error del usuario. De todas formas podríamos controlar el mime type con el tipo de archivo que se recibe a partir de la propiedad type del archivo. Si no es el esperado no seguiríamos el proceso.

La lectura readAsDataURL (datos URL) codifica en base64. Es ideal para leer una imagen y luego adjudicar la cadena de texto al atributo src de un elemento <img>. De esta forma es como funciona mi aplicación del compositor de imágenes y generador Sprites CSS. El proceso es igual que leer texto: leer el archivo y cuando el contenido esté disponible en el onload del lector, pasarlo al atributo src del elemento imagen.

File API: La barra de progreso (Event Progress)

El evento progress permite hacer un seguimiento del porcentaje de archivo leído. Pero en las pruebas realizadas he visto que se necesita un archivo muy grande para observar ese avance (del orden de varios megabytes dependiendo de los recursos). Como dije más arriba, la lectura se realiza de forma asíncrona y por tanto no bloquea la interfaz de usuario, con lo que podemos ir actualizando la barra de progreso. E incluso abortar la lectura, como en el siguiente ejemplo:

Ejemplo:




Tipo MIME:
Longitud: bytes
Progreso: 0%

Para no alargar mucho esta página he preferido no poner el código completo aquí. De todas formas lo puedes consultar con el bóton Código en la barra de la cabecera de este documento. Sólo me voy a detener en el manejador del evento progress:

lector4.addEventListener("progress",
    function(evento){
        if (evento.lengthComputable) {
            actualizarProgreso(evento.loaded / evento.total);
        }
    }, false); 

El evento porta loaded y total que nos va dando el progreso de la lectura. Como la lectura es asíncrona podemos actualizar la barra de progreso que es un elemento <div>, simplemente cambiando su estilo width en la misma proporción que lo hace evento.loaded / evento.total.


Y hasta aquí esta breve introducción a la File API. Aún quedan algunas cosas como analizar con mayor detalle el objeto Blob o ver qué es eso de URI scheme.