Cómo explorar las API de sistemas de archivos (FileSystem APIs)

HTML5 Rocks

Introducción

Siempre he pensado que sería muy práctico que las aplicaciones web pudieran leer y editar archivos y directorios. Conforme se va produciendo la transición del funcionamiento sin conexión al funcionamiento online, la complejidad de las aplicaciones está aumentando y el avance de la Web se está viendo frenado por la falta de API de sistemas de archivos. El almacenamiento de datos binarios o la interacción con este tipo de datos no debería limitarse al escritorio. Afortunadamente, el API de FileSystem ha acabado con esta limitación. Con esta API, una aplicación web puede crear, leer, explorar y editar una sección de prueba del sistema de archivos local del usuario.

El API se divide en varios temas:

  • lectura y manipulación de archivos (File/Blob, FileList, FileReader),
  • creación y escritura (BlobBuilder, FileWriter),
  • acceso a sistemas de archivos y directorios (DirectoryReader, FileEntry/DirectoryEntry, LocalFileSystem).

Compatibilidad con el navegador y limitaciones de almacenamiento

En el momento de escribir este artículo, Google Chrome cuenta con la única implementación operativa del API de FileSystem. Aún no existe una interfaz de usuario de navegador específica para la administración de espacio de almacenamiento/archivos. Para almacenar datos en el sistema del usuario, es posible que la aplicación tenga que solicitar espacio. Sin embargo, para la realización de pruebas, Chrome se puede ejecutar con el indicador --unlimited-quota-for-files. Por otra parte, si estás creando una aplicación o una extensión para Chrome Web Store, puedes utilizar el permiso unlimitedStorage del archivo de manifiesto en vez de solicitar espacio de almacenamiento. Finalmente, se mostrará a los usuarios un cuadro de diálogo de confirmación de permiso para conceder, denegar o incrementar el espacio de almacenamiento utilizado por una aplicación.

Puede que tengas que utilizar el indicador --allow-file-access-from-files si estás depurando tu aplicación desde file://. Si no se utilizan estos indicadores, se producirá un error de archivo SECURITY_ERR o QUOTA_EXCEEDED_ERR.

Solicitud de un sistema de archivos

Una aplicación web puede solicitar acceso al sistema de archivos de prueba con window.requestFileSystem():

// Note: The file system has been prefixed as of Google Chrome 12:
window.requestFileSystem  = window.requestFileSystem || window.webkitRequestFileSystem;

window.requestFileSystem(type, size, successCallback, opt_errorCallback)
type
Indica si el almacenamiento de archivos debe ser permanente. Los valores que se pueden utilizar son window.TEMPORARY y window.PERSISTENT. Los datos almacenados con el tipo TEMPORARY se pueden eliminar a elección del navegador (por ejemplo, si se necesita más espacio). Los datos almacenados con el tipo PERSISTENT no se pueden borrar a menos que el usuario o la aplicación lo autoricen expresamente y requieren que el usuario otorgue espacio de almacenamiento a la aplicación. Consulta la sección sobre solicitud de espacio de almacenamiento.
size
Indica el espacio (en bytes) de almacenamiento que necesitará la aplicación.
successCallback
Indica la devolución de llamada que se activa cuando se autoriza el acceso a un sistema de archivos. Su argumento es un objeto FileSystem.
opt_errorCallback
Corresponde a una devolución de llamada opcional para la gestión de errores o para los casos en los que se deniega la solicitud de acceso al sistema de archivos. Su argumento es un objeto FileError.

La primera vez que se hace una llamada a requestFileSystem(), se crea nuevo espacio de almacenamiento para la aplicación. Es importante recordar que este sistema de archivos es de prueba, lo que significa que una aplicación web no puede acceder a los archivos de otra aplicación y que no es posible leer ni editar archivos en carpetas del disco duro del usuario (como "Mis imágenes", "Mis documentos", etc.).

Ejemplo de uso:

function onInitFs(fs) {
  console.log('Opened file system: ' + fs.name);
}

window.requestFileSystem(window.TEMPORARY, 5*1024*1024 /*5MB*/, onInitFs, errorHandler);

En la especificación de FileSystem también se define un API síncrona, la interfaz LocalFileSystemSync destinada a Web Workers. Sin embargo, en este tutorial no se trata el tema del API síncrona.

En el resto de este documento, utilizaremos el mismo controlador para el procesamiento de errores en las llamadas asíncronas:

function errorHandler(e) {
  var msg = '';

  switch (e.code) {
    case FileError.QUOTA_EXCEEDED_ERR:
      msg = 'QUOTA_EXCEEDED_ERR';
      break;
    case FileError.NOT_FOUND_ERR:
      msg = 'NOT_FOUND_ERR';
      break;
    case FileError.SECURITY_ERR:
      msg = 'SECURITY_ERR';
      break;
    case FileError.INVALID_MODIFICATION_ERR:
      msg = 'INVALID_MODIFICATION_ERR';
      break;
    case FileError.INVALID_STATE_ERR:
      msg = 'INVALID_STATE_ERR';
      break;
    default:
      msg = 'Unknown Error';
      break;
  };

  console.log('Error: ' + msg);
}

Ciertamente, esta devolución de llamada de error es muy genérica, pero sirve para hacerse una idea. En lugar de eso, se pueden mostrar mensajes legibles para el ser humano a los usuarios.

Solicitud de espacio de almacenamiento

Para utilizar almacenamiento de tipo PERSISTENT, se debe obtener permiso del usuario para almacenar datos permanentes. Esta restricción no se aplica al almacenamiento de tipo TEMPORARY, porque el navegador puede borrar datos almacenados de forma temporal cuando lo desee.

Para utilizar almacenamiento de tipo PERSISTENT con el API de FileSystem, Chrome muestra una nueva API en window.webkitStorageInfo para solicitar almacenamiento:

window.webkitStorageInfo.requestQuota(PERSISTENT, 1024*1024, function(grantedBytes) {
  window.requestFileSystem(PERSISTENT, grantedBytes, onInitFs, errorHandler);
}, function(e) {
  console.log('Error', e);
});

Una vez que el usuario ha concedido su permiso, no es necesario hacer ninguna otra llamada a requestQuota(). Las llamadas posteriores no están operativas.

También hay un API que permite consultar el uso y la asignación de espacio de almacenamiento actual de un elemento de origen: window.webkitStorageInfo.queryUsageAndQuota()

Operaciones con archivos

Los archivos del entorno de pruebas se representan mediante la interfaz FileEntry. Una interfaz FileEntry contiene los tipos de propiedades (name, isFile...) y de métodos (remove, moveTo, copyTo...) que se pueden encontrar en un sistema de archivos estándar.

Propiedades y métodos de FileEntry:

fileEntry.isFile === true
fileEntry.isDirectory === false
fileEntry.name
fileEntry.fullPath
...

fileEntry.getMetadata(successCallback, opt_errorCallback);
fileEntry.remove(successCallback, opt_errorCallback);
fileEntry.moveTo(dirEntry, opt_newName, opt_successCallback, opt_errorCallback);
fileEntry.copyTo(dirEntry, opt_newName, opt_successCallback, opt_errorCallback);
fileEntry.getParent(successCallback, opt_errorCallback);
fileEntry.toURL(opt_mimeType);

fileEntry.file(successCallback, opt_errorCallback);
fileEntry.createWriter(successCallback, opt_errorCallback);
...

En el resto de esta sección, se ofrecen una serie de instrucciones para la realización de tareas comunes que te ayudarán a comprender mejor el funcionamiento de FileEntry.

Cómo crear un archivo

Para buscar o crear archivos, se puede utilizar el método getFile() de la interfaz DirectoryEntry del sistema de archivos. Una vez que se ha solicitado un sistema de archivos, se transmite a la devolución de llamada de operación correcta un objeto FileSystem que contiene una interfaz DirectoryEntry (fs.root) que señala al directorio raíz del sistema de archivos de la aplicación.

El siguiente fragmento de código permite crear un archivo vacío llamado "log.txt" en el directorio raíz del sistema de archivos de la aplicación:

function onInitFs(fs) {

  fs.root.getFile('log.txt', {create: true, exclusive: true}, function(fileEntry) {

    // fileEntry.isFile === true
    // fileEntry.name == 'log.txt'
    // fileEntry.fullPath == '/log.txt'

  }, errorHandler);

}

window.requestFileSystem(window.TEMPORARY, 1024*1024, onInitFs, errorHandler);

Una vez que se ha solicitado el sistema de archivos, se transmite un objeto FileSystem al controlador de operación correcta. Dentro de la devolución de llamada, se puede solicitar fs.root.getFile() indicando el nombre del archivo que se va a crear. Se puede especificar una ruta absoluta o relativa, pero debe ser una ruta válida. Por ejemplo, es un error intentar crear un archivo cuyo elemento inmediatamente anterior no exista. El segundo argumento de getFile() es una cadena literal de objeto que describe el comportamiento que debe tener la función si el archivo no existe. En este ejemplo, create: true hace que se cree el archivo si no existe y que se genere un error en caso contrario (exclusive: true). Por su parte, create: false hace que el archivo simplemente se extraiga y se muestre. En ambos casos, el contenido del archivo no se sobrescribe, ya que solo se está obteniendo una entrada de referencia al archivo en cuestión.

Cómo leer un archivo por nombre

El código que aparece a continuación permite recuperar el archivo "log.txt", leer su contenido con el API FileReader e incorporarlo a una nueva área de texto (<textarea>) en la página. Si el archivo "log.txt" no existe, se genera un error.

function onInitFs(fs) {

  fs.root.getFile('log.txt', {}, function(fileEntry) {

    // Get a File object representing the file,
    // then use FileReader to read its contents.
    fileEntry.file(function(file) {
       var reader = new FileReader();

       reader.onloadend = function(e) {
         var txtArea = document.createElement('textarea');
         txtArea.value = this.result;
         document.body.appendChild(txtArea);
       };

       reader.readAsText(file);
    }, errorHandler);

  }, errorHandler);

}

window.requestFileSystem(window.TEMPORARY, 1024*1024, onInitFs, errorHandler);

Cómo escribir en un archivo

El código que aparece a continuación permite crear un archivo vacío llamado "log.txt" (en caso de que no exista) y escribir en él el texto "Lorem Ipsum".

function onInitFs(fs) {

  fs.root.getFile('log.txt', {create: true}, function(fileEntry) {

    // Create a FileWriter object for our FileEntry (log.txt).
    fileEntry.createWriter(function(fileWriter) {

      fileWriter.onwriteend = function(e) {
        console.log('Write completed.');
      };

      fileWriter.onerror = function(e) {
        console.log('Write failed: ' + e.toString());
      };

      // Create a new Blob and write it to log.txt.
      var bb = new BlobBuilder(); // Note: window.WebKitBlobBuilder in Chrome 12.
      bb.append('Lorem Ipsum');
      fileWriter.write(bb.getBlob('text/plain'));

    }, errorHandler);

  }, errorHandler);

}

window.requestFileSystem(window.TEMPORARY, 1024*1024, onInitFs, errorHandler);

Esta vez, se utiliza el método createWriter() de FileEntry para obtener un objeto FileWriter. Dentro de la devolución de llamada de operación correcta, se definen controladores para los eventos error y writeend. Para escribir los datos del texto en el archivo, se crea un objeto Blob, se le añade texto y se transmite el objeto a FileWriter.write().

Cómo añadir datos a un archivo

El código que aparece a continuación permite añadir el texto "Hello World" al final del archivo de registro. Si el archivo no existe, se genera un error.

function onInitFs(fs) {

  fs.root.getFile('log.txt', {create: false}, function(fileEntry) {

    // Create a FileWriter object for our FileEntry (log.txt).
    fileEntry.createWriter(function(fileWriter) {

      fileWriter.seek(fileWriter.length); // Start write position at EOF.

      // Create a new Blob and write it to log.txt.
      var bb = new BlobBuilder(); // Note: window.WebKitBlobBuilder in Chrome 12.
      bb.append('Hello World');
      fileWriter.write(bb.getBlob('text/plain'));

    }, errorHandler);

  }, errorHandler);

}

window.requestFileSystem(window.TEMPORARY, 1024*1024, onInitFs, errorHandler);

Cómo duplicar archivos seleccionados por el usuario

El código que aparece a continuación permite que un usuario seleccione varios archivos utilizando <input type="file" multiple /> y que se creen copias de esos archivos en el sistema de archivos de prueba de la aplicación.

<input type="file" id="myfile" multiple />
document.querySelector('#myfile').onchange = function(e) {
  var files = this.files;

  window.requestFileSystem(window.TEMPORARY, 1024*1024, function(fs) {
    // Duplicate each file the user selected to the app's fs.
    for (var i = 0, file; file = files[i]; ++i) {

      // Capture current iteration's file in local scope for the getFile() callback.
      (function(f) {
        fs.root.getFile(file.name, {create: true, exclusive: true}, function(fileEntry) {
          fileEntry.createWriter(function(fileWriter) {
            fileWriter.write(f); // Note: write() can take a File or Blob object.
          }, errorHandler);
        }, errorHandler);
      })(file);

    }
  }, errorHandler);

};

Aunque en este ejemplo se ha utilizado un comando de entrada para la importación del archivo, se podría obtener fácilmente el mismo resultado utilizando la función de arrastrar y soltar de HTML5.

Como se ha indicado en el comentario, FileWriter.write() puede aceptar objetos Blob o File. Esto se debe a que File hereda Blob, por lo que los objetos File son "blobs".

Cómo eliminar un archivo

El código que aparece a continuación permite eliminar el archivo "log.txt".

window.requestFileSystem(window.TEMPORARY, 1024*1024, function(fs) {
  fs.root.getFile('log.txt', {create: false}, function(fileEntry) {

    fileEntry.remove(function() {
      console.log('File removed.');
    }, errorHandler);

  }, errorHandler);
}, errorHandler);

Operaciones con directorios

Los directorios de la zona de pruebas se representan mediante la interfaz DirectoryEntry, que comparte la mayoría de sus propiedades con FileEntry (ambas interfaces heredan de una interfaz Entry común). Sin embargo, la interfaz DirectoryEntry tiene métodos adicionales para la manipulación de directorios.

Propiedades y métodos de DirectoryEntry:

dirEntry.isDirectory === true
// See the section on FileEntry for other inherited properties/methods.
...

var dirReader = dirEntry.createReader();
dirEntry.getFile(path, opt_flags, opt_successCallback, opt_errorCallback);
dirEntry.getDirectory(path, opt_flags, opt_successCallback, opt_errorCallback);
dirEntry.removeRecursively(successCallback, opt_errorCallback);
...

Cómo crear directorios

El método getDirectory() de la interfaz DirectoryEntry permite leer y crear directorios. Se puede especificar un nombre o una ruta como identificación del directorio que se quiere consultar o crear.

Por ejemplo, el código que aparece a continuación permite crear un directorio llamado "MyPictures" en el directorio raíz:

window.requestFileSystem(window.TEMPORARY, 1024*1024, function(fs) {
  fs.root.getDirectory('MyPictures', {create: true}, function(dirEntry) {
    ...
  }, errorHandler);
}, errorHandler);
  

Subdirectorios

Los subdirectorios se crean exactamente de la misma forma que los directorios. Sin embargo, el API genera un error si se intenta crear un directorio cuyo elemento inmediatamente anterior no existe. La solución es crear cada directorio de forma secuencial, algo bastante difícil de hacer con un API asíncrona.

El código que aparece a continuación permite crear una nueva jerarquía (music/genres/jazz) en el directorio raíz del sistema de archivos de la aplicación añadiendo cada subdirectorio de forma recurrente después de que se cree su carpeta principal.

var path = 'music/genres/jazz/';

function createDir(rootDirEntry, folders) {
  // Throw out './' or '/' and move on to prevent something like '/foo/.//bar'.
  if (folders[0] == '.' || folders[0] == '') {
    folders = folders.slice(1);
  }
  rootDirEntry.getDirectory(folders[0], {create: true}, function(dirEntry) {
    // Recursively add the new subfolder (if we still have another to create).
    if (folders.length) {
      createDir(dirEntry, folders.slice(1));
    }
  }, errorHandler);
};

function onInitFs(fs) {
  createDir(fs.root, path.split('/')); // fs.root is a DirectoryEntry.
}

window.requestFileSystem(window.TEMPORARY, 1024*1024, onInitFs, errorHandler);

Una vez creado el subdirectorio "music/genres/jazz", podemos transmitir su ruta completa a getDirectory() y crear nuevas subcarpetas o archivos dentro de él. Por ejemplo:

window.requestFileSystem(window.TEMPORARY, 1024*1024, function(fs) {
  fs.root.getFile('/music/genres/jazz/song.mp3', {create: true}, function(fileEntry) {
    ...
  }, errorHandler);
}, errorHandler);

Cómo leer el contenido de un directorio

Para leer el contenido de un directorio, se debe crear un objeto DirectoryReader y hacer una llamada a su método readEntries(). No se puede garantizar que se vayan a obtener todas las entradas de un directorio con una única llamada a readEntries(). Por tanto, hay que seguir haciendo llamadas a DirectoryReader.readEntries() hasta que se hayan recibido todos los resultados posibles. A continuación se muestra un fragmento de código de ejemplo de esta operación:

<ul id="filelist"></ul>
function toArray(list) {
  return Array.prototype.slice.call(list || [], 0);
}

function listResults(entries) {
  // Document fragments can improve performance since they're only appended
  // to the DOM once. Only one browser reflow occurs.
  var fragment = document.createDocumentFragment();

  entries.forEach(function(entry, i) {
    var img = entry.isDirectory ? '<img src="folder-icon.gif">' :
                                  '<img src="file-icon.gif">';
    var li = document.createElement('li');
    li.innerHTML = [img, '<span>', entry.name, '</span>'].join('');
    fragment.appendChild(li);
  });

  document.querySelector('#filelist').appendChild(fragment);
}

function onInitFs(fs) {

  var dirReader = fs.root.createReader();
  var entries = [];

  // Call the reader.readEntries() until no more results are returned.
  var readEntries = function() {
     dirReader.readEntries (function(results) {
      if (!results.length) {
        listResults(entries.sort());
      } else {
        entries = entries.concat(toArray(results));
        readEntries();
      }
    }, errorHandler);
  };

  readEntries(); // Start reading dirs.

}

window.requestFileSystem(window.TEMPORARY, 1024*1024, onInitFs, errorHandler);

Cómo eliminar un directorio

El método DirectoryEntry.remove() tiene el mismo comportamiento que FileEntry. La única diferencia es que, si se intenta eliminar un directorio que no esté vacío, se genera un error.

El código que aparece a continuación permite eliminar el directorio vacío "jazz" de "/music/genres/":

window.requestFileSystem(window.TEMPORARY, 1024*1024, function(fs) {
  fs.root.getDirectory('music/genres/jazz', {}, function(dirEntry) {

    dirEntry.remove(function() {
      console.log('Directory removed.');
    }, errorHandler);

  }, errorHandler);
}, errorHandler);

Cómo eliminar un directorio de forma recurrente

Si quieres deshacerte de un directorio que contenga entradas, removeRecursively() es la solución. Este método permite eliminar el directorio y su contenido de forma recurrente.

El código que aparece a continuación permite eliminar de forma recurrente el directorio "music" y todos los archivos y directorios incluidos en él:

window.requestFileSystem(window.TEMPORARY, 1024*1024, function(fs) {
  fs.root.getDirectory('/misc/../music', {}, function(dirEntry) {

    dirEntry.removeRecursively(function() {
      console.log('Directory removed.');
    }, errorHandler);

  }, errorHandler);
}, errorHandler);

Cómo copiar entradas, cambiarles el nombre y moverlas

Las interfaces FileEntry y DirectoryEntry comparten algunas operaciones.

Cómo copiar una entrada

Tanto FileEntry como DirectoryEntry cuentan con el método copyTo() para la duplicación de entradas. Este método permite hacer una copia recurrente en diferentes carpetas.

El código del ejemplo que aparece a continuación permite copiar el archivo "me.png" de un directorio en otro:

function copy(cwd, src, dest) {
  cwd.getFile(src, {}, function(fileEntry) {

    cwd.getDirectory(dest, {}, function(dirEntry) {
      fileEntry.copyTo(dirEntry);
    }, errorHandler);

  }, errorHandler);
}

window.requestFileSystem(window.TEMPORARY, 1024*1024, function(fs) {
  copy(fs.root, '/folder1/me.png', 'folder2/mypics/');
}, errorHandler);

Cómo mover una entrada o cambiarle el nombre

El método moveTo() de FileEntry y DirectoryEntry permite mover archivos y directorios y cambiarles el nombre. El primer argumento especifica el directorio principal al que se debe mover el archivo y el segundo argumento indica un nuevo nombre opcional para el archivo. Si no se especifica un nombre nuevo, se utilizará el nombre original del archivo.

El código de ejemplo que aparece a continuación permite sustituir el nombre del archivo "me.png" por "you.png" sin mover el archivo:

function rename(cwd, src, newName) {
  cwd.getFile(src, {}, function(fileEntry) {
    fileEntry.moveTo(cwd, newName);
  }, errorHandler);
}

window.requestFileSystem(window.TEMPORARY, 1024*1024, function(fs) {
  rename(fs.root, 'me.png', 'you.png');
}, errorHandler);

El código de ejemplo que aparece a continuación permite mover el archivo "me.png" (ubicado en el directorio raíz) a una carpeta llamada "newfolder".

function move(src, dirName) {
  fs.root.getFile(src, {}, function(fileEntry) {

    fs.root.getDirectory(dirName, {}, function(dirEntry) {
      fileEntry.moveTo(dirEntry);
    }, errorHandler);

  }, errorHandler);
}

window.requestFileSystem(window.TEMPORARY, 1024*1024, function(fs) {
  move('/me.png', 'newfolder/');
}, errorHandler);

filesystem: URL

El API de FileSystem muestra un nuevo esquema de URL, filesystem:, que se puede utilizar para la definición del contenido de los atributos src o href. Por ejemplo, para que se muestre una imagen y obtener su interfaz FileEntry, se puede llamar a toURL() para obtener la URL de filesystem: del archivo:

var img = document.createElement('img');
img.src = fileEntry.toURL(); // filesystem:http://example.com/temporary/myfile.png
document.body.appendChild(img);

Por el contrario, si ya tenemos una URL de filesystem:, podemos utilizar resolveLocalFileSystemURL() para obtener la interfaz FileEntry:

window.resolveLocalFileSystemURL = window.resolveLocalFileSystemURL ||
                                   window.webkitResolveLocalFileSystemURL;

var url = 'filesystem:http://example.com/temporary/myfile.png';
window.resolveLocalFileSystemURL(url, function(fileEntry) {
  ...
});

Unión de todos los elementos

Ejemplo básico

Esta demo muestra archivos/carpetas en el sistema de archivos.

Terminal de HTML5

Esta shell reproduce algunas de las operaciones habituales de un sistema de archivos UNIX (como cd, mkdir, rm, open y cat) creando una abstracción del API de FileSystem. Para añadir archivos, arrástralos desde el escritorio y suéltalos en el terminal que aparece abajo.

Casos prácticos

Hay varias opciones de almacenamiento disponibles en HTML5, pero FileSystem presenta la diferencia de que ofrece soluciones para casos prácticos de almacenamiento de cliente que no se pueden resolver adecuadamente mediante bases de datos. Por lo general, son casos de aplicaciones que deben manejar grandes objetos Blob binarios o compartir datos con otras aplicaciones fuera del contexto del navegador.

En la especificación se indican varios casos prácticos:

  1. Herramienta de subida persistente
    • Cuando se selecciona un archivo o un directorio para subirlo, copia los archivos en una zona de pruebas local y los va subiendo de forma fragmentada.
    • Las subidas se pueden reiniciar después de fallos del navegador, interrupciones de conexión a la red, etc.
  2. Videojuego, aplicación de música u otra aplicación con muchos elementos multimedia
    • Descarga uno o varios paquetes tar de gran tamaño y los expande localmente en una estructura de directorio.
    • La misma descarga funciona en cualquier sistema operativo.
    • Puede hacer que se precarguen en segundo plano únicamente los siguientes elementos necesarios, de forma que no haya que esperar descargas para pasar al siguiente nivel de un juego o para activar una nueva función.
    • Utiliza esos elementos directamente desde su caché local, mediante lectura directa de archivos o mediante la transferencia de URI locales a etiquetas de vídeo o imágenes, cargadores de elementos WebGL, etc.
    • Los archivos pueden tener un formato binario arbitrario.
    • En el servidor, un paquete tar comprimido normalmente será mucho más pequeño que un conjunto de archivos comprimidos por separado. Además, el uso de un paquete tar en lugar de 1.000 pequeños archivos permite reducir las búsquedas. Todo lo demás es igual.
  3. Programa de edición fotográfica o de audio con acceso sin conexión o caché local para aumentar la velocidad
    • Los blobs de datos son bastante grandes en potencia y son objetos de lectura y escritura.
    • Podría tener que escribir parcialmente en los archivos (sobrescribiendo únicamente las etiquetas ID3/EXIF, por ejemplo).
    • La posibilidad de organizar archivos de proyectos creando directorios sería una opción bastante útil.
    • Las aplicaciones cliente (como iTunes o Picasa) deberían poder acceder a los archivos editados.
  4. Visualizador de vídeos sin conexión
    • Descarga archivos de gran tamaño (>1 GB) para su posterior visualización.
    • Necesita eficacia de búsqueda y emisión.
    • Debe poder transmitir una URI a la etiqueta de vídeo.
    • Debería permitir el acceso a archivos parcialmente descargados (por ejemplo, para poder ver el primer episodio de un DVD aunque no se haya completado aún la descarga antes de subir a un avión).
    • Debería poder extraer un episodio en mitad de una descarga y transmitir únicamente ese contenido a la etiqueta de vídeo.
  5. Cliente de correo web sin conexión
    • Descarga archivos adjuntos y los almacena localmente.
    • Almacena en caché los archivos adjuntos seleccionados por el usuario para subirlos más tarde.
    • Debe poder hacer referencia a miniaturas de imágenes y archivos adjuntos almacenados en caché que se deban visualizar y subir.
    • Debe poder activar el administrador de descargas de UA como si estuviera comunicándose con un servidor.
    • Debe poder subir un correo electrónico con archivos adjuntos como una operación POST multipart en lugar de enviar los archivos de uno en uno mediante una solicitud XHR.

Especificaciones de referencia

Comments

0