Jugando con la API del sistema de archivos HTML5
() translation by (you can also view the original English article)
HTML5 nos brinda una gran variedad de nuevas posibilidades, como dibujar con canvas, implementar multimedia con las API de audio y video, etc. Una de estas herramientas, que todavía es relativamente nueva, es la API del sistema de archivos. Nos da acceso a una sección de espacio aislado del sistema de archivos local del usuario, lo que llena aún más la brecha entre las aplicaciones web y de escritorio. En el tutorial de hoy, repasaremos los conceptos básicos de esta nueva y emocionante API, explorando las tareas más comunes del sistema de archivos. ¡Empecemos!
Introducción
Ya no es necesario descargar e instalar un software determinado para poder utilizarlo. Simplemente un navegador web y una conexión a Internet nos brindan la capacidad de utilizar cualquier aplicación web, en cualquier momento, en cualquier lugar y en cualquier plataforma.
En resumen, las aplicaciones web son geniales; pero, en comparación con las aplicaciones de escritorio, todavía tienen una debilidad significativa: no tienen una forma de interactuar y organizar los datos en una jerarquía estructurada de carpetas: un sistema de archivos real. Afortunadamente, con la nueva API del sistema de archivos, esto se puede cambiar. Esta API brinda a las aplicaciones web acceso controlado a un sistema de archivos local privado "sandbox", en el que pueden escribir y leer archivos, crear y enumerar directorios, etc. Aunque en el momento de escribir este artículo solo el navegador Chrome de Google admite la implementación "completa" de la API del sistema de archivos, todavía merece ser estudiada como una forma poderosa y conveniente de almacenamiento local.
La API del sistema de archivos viene en dos versiones diferentes. La API asíncrona, que es útil para aplicaciones normales, y la API síncrona, reservada para su uso con trabajadores web. Para los propósitos de este tutorial, exploraremos exclusivamente la versión asincrónica de la API.
Paso 1 - Comenzando
Tu primer paso es obtener acceso al sistema de archivos HTML5 solicitando un objeto del sistema llamado LocalFile
, utilizando el método global window.requestFileSystem()
:
1 |
window.requestFileSystem(type, size, successCallback, opt_errorCallback) |
No hay forma de que una aplicación web "salga" más allá del directorio raíz local.
Como los dos primeros parámetros, especifica la vida útil y el tamaño del sistema de archivos que deseas. Un sistema de archivos PERSISTENTE es adecuado para aplicaciones web que desean almacenar datos de usuario de forma permanente. El navegador no lo eliminará, excepto por solicitud explícita del usuario. Un sistema de archivos TEMPORAL es apropiado para aplicaciones web que desean almacenar datos en caché, pero aún pueden funcionar si el navegador web elimina el sistema de archivos. El tamaño del sistema de archivos se especifica en bytes y debe ser un límite superior razonable a la cantidad de datos que necesitas almacenar.
El tercer parámetro es una función de devolución de llamada que se activa cuando el agente de usuario proporciona con éxito un sistema de archivos. Su argumento es un objeto FileSystem
. Y, por último, podemos agregar una función de devolución de llamada opcional, que se llama cuando ocurre un error o se deniega la solicitud de un sistema de archivos. Su argumento es un objeto FileError
. Aunque este parámetro es opcional, siempre es una buena idea detectar errores para los usuarios, ya que hay varios lugares donde las cosas pueden salir mal.
El sistema de archivos obtenido con estas funciones depende del origen del documento que lo contiene. Todos los documentos o aplicaciones web del mismo origen (host, puerto y protocolo) comparten un sistema de archivos. Dos documentos o aplicaciones de diferentes orígenes tienen sistemas de archivos completamente distintos e inconexos. Un sistema de archivos está restringido a una sola aplicación y no puede acceder a los datos almacenados de otra aplicación. También está aislado del resto de archivos en el disco duro del usuario, lo cual es bueno: no hay forma de que una aplicación web "salga" más allá del directorio raíz local o acceda a archivos arbitrarios.
Repasemos un ejemplo:
1 |
window.requestFileSystem = window.requestFileSystem || window.webkitRequestFileSystem; |
2 |
|
3 |
window.requestFileSystem(window.TEMPORARY, 5*1024*1024, initFS, errorHandler); |
4 |
|
5 |
function initFS(fs){ |
6 |
alert("Welcome to Filesystem! It's showtime :)"); // Just to check if everything is OK :) |
7 |
// place the functions you will learn bellow here |
8 |
} |
9 |
|
10 |
function errorHandler(){ |
11 |
console.log('An error occured'); |
12 |
} |
Esto crea un sistema de archivos temporal con 5MB de almacenamiento. Luego proporciona una función de devolución de llamada exitosa, que usaremos para operar nuestro sistema de archivos. Y, por supuesto, también se agrega un controlador de errores, en caso de que algo salga mal. Aquí, la función errorHandler()
es demasiado genérica. Entonces, si lo deseas, puedes crear una versión ligeramente optimizada, que le da al lector un mensaje de error más descriptivo:
1 |
function errorHandler(err){ |
2 |
var msg = 'An error occured: '; |
3 |
|
4 |
switch (err.code) { |
5 |
case FileError.NOT_FOUND_ERR: |
6 |
msg += 'File or directory not found'; |
7 |
break; |
8 |
|
9 |
case FileError.NOT_READABLE_ERR: |
10 |
msg += 'File or directory not readable'; |
11 |
break; |
12 |
|
13 |
case FileError.PATH_EXISTS_ERR: |
14 |
msg += 'File or directory already exists'; |
15 |
break; |
16 |
|
17 |
case FileError.TYPE_MISMATCH_ERR: |
18 |
msg += 'Invalid filetype'; |
19 |
break; |
20 |
|
21 |
default: |
22 |
msg += 'Unknown Error'; |
23 |
break; |
24 |
}; |
25 |
|
26 |
console.log(msg); |
27 |
}; |
El objeto 'filesystem' que obtienes tiene un nombre (name
) (un nombre único para el sistema de archivos, asignado por el navegador) y una propiedad root
que se refiere al directorio raíz del sistema de archivos. Este es un objeto DirectoryEntry
y puede tener directorios anidados que están representados por objetos DirectoryEntry
. Cada directorio del sistema de archivos puede contener archivos, representados por objetos FileEntry
. El objeto DirectoryEntry
define métodos para obtener objetos DirectoryEntry
y FileEntry
por nombre de ruta (opcionalmente crearán nuevos directorios o archivos si especificas un nombre que no existe). DirectoryEntry
también define un método de fábrica createReader()
que devuelve un objeto DirectoryReader
para enumerar el contenido de un directorio. La clase FileEntry
define un método para obtener el objeto File
(un Blob) que representa el contenido de un archivo. Puedes utilizar después un objeto FileReader
para leer el archivo. FileEntry
define otro método para devolver un objeto FileWriter
que puedes usar para escribir contenido en un archivo.
Uf... suena complicado. ¿No? No te preocupes. Todo se aclarará a medida que avancemos en los siguientes ejemplos.
Paso 2: Trabajar con directorios
Obviamente, lo primero que necesitas crear en un sistema de archivos son algunos depósitos o directorios. Aunque el directorio raíz ya existe, no vas a querer colocar todos tus archivos allí. Los directorios son creados por el objeto DirectoryEntry
. En el siguiente ejemplo, creamos un directorio, llamado Documents
, dentro del directorio raíz:
1 |
fs.root.getDirectory('Documents', {create: true}, function(dirEntry) { |
2 |
alert('You have just created the ' + dirEntry.name + ' directory.'); |
3 |
}, errorHandler); |
El método getDirectory()
se usa tanto para leer como para crear directorios. Como primer parámetro, puedes pasar un nombre o una ruta como directorio a buscar o crear. Establecemos el segundo argumento en true
, porque estamos intentando crear un directorio, no leer uno existente. Y al final, agregamos una devolución de llamada de error.
Hasta aquí todo bien. Tenemos un directorio; agreguemos ahora un subdirectorio. La función es exactamente la misma con una diferencia: cambiamos el primer argumento de 'Documents' a 'Documents/Music'. Suficientemente fácil; pero, ¿qué sucede si deseas crear una subcarpeta, por ejemplo Sky
, con dos carpetas principales, Images
y Nature
, dentro de la carpeta Documents
? Si escribes 'Documents/Images/Nature/Sky
' para el argumento de la ruta, recibirás un error, porque no puedes crear un directorio, cuando su padre inmediato no existe. Una solución para esto es crear cada carpeta una por una: Images
dentro de Documents
, Nature
dentro de Images
y luego Sky
dentro de Nature
. Pero este es un proceso muy lento e inconveniente. Hay una solución mejor: crear una función que creará todas las carpetas necesarias automáticamente.
1 |
function createDir(rootDir, folders) { |
2 |
rootDir.getDirectory(folders[0], {create: true}, function(dirEntry) { |
3 |
if (folders.length) { |
4 |
createDir(dirEntry, folders.slice(1)); |
5 |
} |
6 |
}, errorHandler); |
7 |
}; |
8 |
|
9 |
createDir(fs.root, 'Documents/Images/Nature/Sky/'.split('/')); |
Con este pequeño truco, todo lo que tenemos que hacer es proporcionar una ruta completa que represente las carpetas que queremos crear. Ahora, el directorio Sky
se creó con éxito y puedes crear otros archivos o directorios dentro de él.
Ahora es el momento de comprobar lo que tenemos en nuestro sistema de archivos. Crearemos un objeto DirectoryReader
y usaremos el método readEntries()
para leer el contenido del directorio.
1 |
fs.root.getDirectory('Documents', {}, function(dirEntry){<br> |
2 |
var dirReader = dirEntry.createReader(); |
3 |
dirReader.readEntries(function(entries) {<br> |
4 |
for(var i = 0; i < entries.length; i++) { |
5 |
var entry = entries[i]; |
6 |
if (entry.isDirectory){ |
7 |
console.log('Directory: ' + entry.fullPath); |
8 |
} |
9 |
else if (entry.isFile){ |
10 |
console.log('File: ' + entry.fullPath); |
11 |
} |
12 |
} |
13 |
|
14 |
}, errorHandler); |
15 |
}, errorHandler); |
En el código anterior, las propiedades isDirectory
e isFile
se utilizan para obtener una salida diferente para directorios y archivos, respectivamente. Además, usamos la propiedad fullPath
para obtener la ruta completa de la entrada, en lugar de solo su nombre.
Hay dos formas de eliminar un DirectoryEntry
del sistema de archivos: remove()
y removeRecursively()
. El primero elimina un directorio determinado solo si está vacío. De lo contrario, recibirá un error.
1 |
fs.root.getDirectory('Documents/Music', {}, function(dirEntry) { |
2 |
dirEntry.remove(function(){ |
3 |
console.log('Directory successfully removed.'); |
4 |
}, errorHandler); |
5 |
}, errorHandler); |
Si la carpeta Music
tiene archivos, entonces debes usar el segundo método, que borra de forma recursiva el directorio y todo su contenido.
1 |
fs.root.getDirectory('Documents/Music', {}, function(dirEntry) { |
2 |
dirEntry.removeRecursively(function(){ |
3 |
console.log('Directory successufully removed.'); |
4 |
}, errorHandler); |
5 |
}, errorHandler); |
Paso 3: Trabajar con archivos
Ahora que sabemos cómo crear directorios, ¡es hora de poblarlos con archivos!
El siguiente ejemplo crea un archivo test.txt
vacío en el directorio raíz:
1 |
fs.root.getFile('test.txt', {create: true, exclusive: true}, function(fileEntry) { |
2 |
alert('A file ' + fileEntry.name + ' was created successfully.'); |
3 |
}, errorHandler); |
El primer argumento de getFile()
puede ser una ruta absoluta o relativa, pero debe ser válida. Por ejemplo, es un error intentar crear un archivo, cuando su padre inmediato no existe. El segundo argumento es un objeto literal, que describe el comportamiento de la función si el archivo no existe. En este ejemplo, create: true
crea el archivo si no existe y arroja un error si existe (exclusive: true
). De lo contrario, si create: false
, el archivo simplemente se recupera y se devuelve.
Sin embargo, tener un archivo vacío no es muy útil; así que agreguemos algo de contenido adentro. Podemos usar el objeto FileWriter
para esto.
1 |
fs.root.getFile('test.txt', {create: false}, function(fileEntry) { |
2 |
fileEntry.createWriter(function(fileWriter) { |
3 |
window.BlobBuilder = window.BlobBuilder || window.WebKitBlobBuilder; |
4 |
var bb = new BlobBuilder(); |
5 |
bb.append('Filesystem API is awesome!'); |
6 |
fileWriter.write(bb.getBlob('text/plain')); |
7 |
}, errorHandler); |
8 |
}, errorHandler); |
Arriba, recuperamos el archivo test.txt y creamos un objeto FileWriter
para él. Luego le agregamos contenido creando un nuevo objeto BlobBuilder
y usando el método write()
de FileWriter
.
Llamar a getFile()
solo recupera un FileEntry
. No devuelve el contenido del archivo. Entonces, si queremos leer el contenido del archivo, necesitamos usar el objeto File
y el objeto FileReader
.
1 |
fs.root.getFile('test.txt', {}, function(fileEntry) { |
2 |
fileEntry.file(function(file) { |
3 |
var reader = new FileReader(); |
4 |
reader.onloadend = function(e) { |
5 |
alert(this.result); |
6 |
}; |
7 |
reader.readAsText(file); |
8 |
}, errorHandler); |
9 |
}, errorHandler); |
Hemos escrito algo de contenido en nuestro archivo, pero ¿qué pasa si deseas agregar más en una fecha posterior? Para agregar datos a un archivo existente, se usa FileWriter
una vez más. Podemos reposicionar el escritor al final del archivo, usando el método seek()
. seek
acepta un desplazamiento de bytes como argumento y establece la posición del escritor del archivo en ese desplazamiento.
1 |
fs.root.getFile('test.txt', {create: false}, function(fileEntry) { |
2 |
fileEntry.createWriter(function(fileWriter) { |
3 |
fileWriter.seek(fileWriter.length); |
4 |
window.BlobBuilder = window.BlobBuilder || window.WebKitBlobBuilder; |
5 |
var bb = new BlobBuilder(); |
6 |
bb.append('Yes, it is!'); |
7 |
fileWriter.write(bb.getBlob('text/plain')); |
8 |
}, errorHandler); |
9 |
}, errorHandler); |
Para eliminar un archivo del sistema de archivos, simplemente llama a entry.remove()
. El primer argumento de este método es una función de devolución de llamada de parámetro cero, que se llama cuando el archivo se elimina correctamente. El segundo es una devolución de llamada de error opcional si se produce algún error.
1 |
fs.root.getFile('test.txt', {create: false}, function(fileEntry) { |
2 |
fileEntry.remove(function() { |
3 |
console.log('File successufully removed.'); |
4 |
}, errorHandler); |
5 |
}, errorHandler); |
Paso 4: Manipulación de archivos y directorios
FileEntry
y DirectoryEntry
comparten los mismos métodos de API para copiar, mover y cambiar el nombre de las entradas. Hay dos métodos que puedes utilizar para estas operaciones: copyTo()
y moveTo()
. Ambos aceptan exactamente los mismos parámetros:
1 |
copyTo(parentDirEntry, opt_newName, opt_successCallback, opt_errorCallback); |
2 |
|
3 |
moveTo(parentDirEntry, opt_newName, opt_successCallback, opt_errorCallback); |
El primer parámetro es la carpeta principal a la cual mover/copiar la entrada. El segundo es un nuevo nombre opcional para dar a la entrada movida/copiada, que en realidad se requiere cuando copias una entrada en la misma carpeta; de lo contrario, obtendrás un error. El tercer y cuarto parámetros se explicaron anteriormente.
Repasemos algunos ejemplos sencillos. En el siguiente, copiamos el archivo test.txt
de la raíz (root
) al directorio Documents
.
1 |
function copy(currDir, srcEntry, destDir) { |
2 |
currDir.getFile(srcEntry, {}, function(fileEntry) { |
3 |
currDir.getDirectory(destDir, {}, function(dirEntry) { |
4 |
fileEntry.copyTo(dirEntry); |
5 |
}, errorHandler); |
6 |
}, errorHandler); |
7 |
} |
8 |
|
9 |
copy(fs.root, 'test.txt', 'Documents/'); |
El siguiente ejemplo mueve test.txt
a Documents
, en lugar de copiarlo:
1 |
function move(currDir, srcEntry, dirName) { |
2 |
currDir.getFile(srcEntry, {}, function(fileEntry) { |
3 |
currDir.getDirectory(dirName, {}, function(dirEntry) { |
4 |
fileEntry.moveTo(dirEntry); |
5 |
}, errorHandler); |
6 |
}, errorHandler); |
7 |
} |
8 |
|
9 |
move(fs.root, 'test.txt', 'Documents/'); |
El siguiente ejemplo cambia el nombre de test.txt
a text.txt
:
1 |
function rename(currDir, srcEntry, newName) { |
2 |
currDir.getFile(srcEntry, {}, function(fileEntry) { |
3 |
fileEntry.moveTo(currDir, newName); |
4 |
}, errorHandler); |
5 |
} |
6 |
|
7 |
rename(fs.root, 'test.txt', 'text.txt'); |
Aprende más
En este tutorial introductorio, solo hemos arañado la superficie de las diferentes interfaces del sistema de archivos. Si deseas obtener más información y profundizar en la API del sistema de archivos, debes consultar las especificaciones de especificaciones de W3C:
Ahora que tienes una comprensión básica de lo que es la API del sistema de archivos y cómo se puede usar, debería ser considerablemente más fácil comprender la documentación de la API, que puede resultar un poco confusa a primera vista.
Conclusión
La API del sistema de archivos es una tecnología potente y fácil de usar, que proporciona a los desarrolladores web una gran variedad de nuevas posibilidades al crear aplicaciones web. Es cierto que todavía es bastante nueva y no es ampliamente compatible con todos los navegadores principales, pero esto ciertamente cambiará en el futuro. ¡También podrías empezar con ventaja!