Leer y escribir archivos y directorios con la biblioteca de browser-fs-access
Los navegadores han podido manejar archivos y directorios durante mucho tiempo. La API de File (archivos) proporciona funciones para representar objetos de archivo en aplicaciones web, así como para seleccionarlos mediante programación y acceder a tus datos. Sin embargo, en el momento en que miras más de cerca, todo lo que brilla no es oro.
La forma tradicional de tratar con los archivos
Si sabes cómo solía funcionar la antigua forma, puedes saltarte directamente a la nueva.
Abrir archivos
Como desarrollador, puedes abrir y leer archivos a través del elemento <input type="file">
. En su forma más simple, abrir un archivo puede parecerse al ejemplo de código a continuación. El objeto input
te proporciona una lista de FileList
, que en el caso siguiente consta de un solo File
. Un File
es un tipo específico de Blob
y se puede usar en cualquier contexto en el que un Blob pueda hacerlo.
const openFile = async () => {
return new Promise((resolve) => {
const input = document.createElement('input');
input.type = 'file';
input.addEventListener('change', () => {
resolve(input.files[0]);
});
input.click();
});
};
Abrir directorios
Para abrir carpetas (o directorios), puedes establecer el atributo de <input webkitdirectory>
. Aparte de eso, todo lo demás funciona igual que el ejemplo anterior. A pesar de su nombre predefinido por el proveedor, webkitdirectory
no solo se puede usar en los navegadores Chromium y WebKit, sino también en legacy Edge basado en EdgeHTML y en Firefox.
Guardar (más bien, descargar) archivos
Para guardar un archivo, tradicionalmente, se limita a descargar un archivo, que funciona gracias al atributo <a download>
. Dado un Blob, puedes establecer el atributo href
del ancla en un blob:
URL que puedes obtener del método URL.createObjectURL()
.
Para evitar pérdidas de memoria, siempre revoca la URL después de la descarga.
const saveFile = async (blob) => {
const a = document.createElement('a');
a.download = 'my-file.txt';
a.href = URL.createObjectURL(blob);
a.addEventListener('click', (e) => {
setTimeout(() => URL.revokeObjectURL(a.href), 30 * 1000);
});
a.click();
};
El problema
Una desventaja masiva del enfoque de descarga es que no hay forma de hacer que suceda un flujo clásico de abrir → editar → guardar, es decir, no hay forma de sobrescribir el archivo original. En su lugar, terminarás con una nueva copia del archivo original en la carpeta de Descargas predeterminada del sistema operativo en cada ocasión que "guardes".
La API de File System Access
La API de File System Access hace que ambas operaciones, abrir y guardar, sean mucho más simples. También permite un verdadero guardado, es decir, no solo puedes elegir dónde guardar un archivo, sino también sobrescribir un archivo existente.
Para obtener una introducción más detallada a la API de File System Access, consulta el artículo La API de File System Access: simplificación del acceso a archivos locales.
Abrir archivos
Con la API de File System Access, abrir un archivo es cuestión de una llamada al método window.showOpenFilePicker()
. Esta llamada devuelve un identificador de archivo, desde el cual puede obtener el File
real a través del método getFile()
.
const openFile = async () => {
try {
// Siempre regresa una matriz.
const [handle] = await window.showOpenFilePicker();
return handle.getFile();
} catch (err) {
console.error(err.name, err.message);
}
};
Abrir directorios
Abres un directorio llamando a window.showDirectoryPicker()
el cual hace que los directorios se puedan seleccionar en el cuadro de diálogo del archivo.
Guardar archivos
Guardar archivos es igualmente sencillo. Desde un identificador de archivo, crea un stream de escritura a través de createWritable()
, luego escribe los datos de Blob llamando al metodo write()
del stream y finalmente cierra el stream llamando a su método close()
.
const saveFile = async (blob) => {
try {
const handle = await window.showSaveFilePicker({
types: [{
accept: {
// Omitted
},
}],
});
const writable = await handle.createWritable();
await writable.write(blob);
await writable.close();
return handle;
} catch (err) {
console.error(err.name, err.message);
}
};
Presentando a browser-fs-access
Tan perfectamente bien como lo es la API de File System Access, esta función aún no está ampliamente disponible.
Es por eso que veo la API de File System Access como una mejora progresiva. Como tal, quiero usarla cuando el navegador lo permita y usar el enfoque tradicional y si no es posible; mientras el usuario no sea castigado con descargas innecesarias de código JavaScript no compatible. La biblioteca browser-fs-access es mi respuesta para este desafío.
Filosofía de diseño
Dado que es probable que la API de File System Access cambie en el futuro, la API de browser-fs-access no se basa en ella. Es decir, la biblioteca no es un polyfill, sino un ponyfill. Puedes (estática o dinámicamente) importar exclusivamente cualquier funcionalidad que necesites para mantener tu aplicación lo más pequeña posible. Los métodos disponibles son fileOpen()
, directoryOpen()
y fileSave()
. Internamente, la función de biblioteca detecta si la API de File System Access es compatible y luego importa la ruta del código correspondiente.
Usando la biblioteca browser-fs-access
Los tres métodos son intuitivos de usar. Puedes especificar los tipos aceptados de mimeTypes
o las extensions
de los archivos aceptados por tu aplicación, y establecer una bandera de multiple
para permitir o no permitir la selección de varios archivos o directorios. Para obtener todos los detalles, consulta la documentación de la API de browser-fs-access. El siguiente ejemplo de código muestra cómo puede abrir y guardar archivos de imagen.
// Los metodos importados utilizarán la API de File
// System Access o una implementación de respaldo.
import {
fileOpen,
directoryOpen,
fileSave,
} from 'https://unpkg.com/browser-fs-access';
(async () => {
// Abre un archivo de imagen.
const blob = await fileOpen({
mimeTypes: ['image/*'],
});
// Abre multiples archivos de imagenes.
const blobs = await fileOpen({
mimeTypes: ['image/*'],
multiple: true,
});
// Abre todos los archivos en una carpeta
// de manera recursiva incluyendo las subcarpetas.
const blobsInDirectory = await directoryOpen({
recursive: true
});
// Guarda un archivo.
await fileSave(blob, {
fileName: 'Untitled.png',
});
})();
Demostración
Puedes ver el código anterior en acción en una demostración de Glitch. Su código fuente también se encuentra disponible allí. Dado que, por razones de seguridad, los subcuadros de origen cruzado no pueden mostrar un selector de archivos, la demostración no se puede incrustar en este artículo.
La biblioteca browser-fs-access en lo salvaje
En mi tiempo libre, contribuyo un poco a una PWA instalable llamada Excalidraw, una herramienta de pizarra que te permite dibujar diagramas fácilmente con una sensación de dibujo a mano. Es totalmente responsiva y funciona bien en una variedad de dispositivos, desde pequeños teléfonos móviles hasta computadoras con pantallas grandes. Esto significa que debes manejar archivos en todas las diversas plataformas, ya sea que admitan o no la API de File System Access. Esto la convierte en una gran candidata para la biblioteca browser-fs-access.
Puedo, por ejemplo, iniciar un dibujo en mi iPhone, guardarlo (técnicamente: descargarlo, ya que Safari no es compatible con la API de File System Access) en la carpeta Descargas de mi iPhone, abrir el archivo en mi escritorio (después de transferirlo desde mi teléfono), modificar el archivo y sobrescribirlo con mis cambios, o incluso guardarlo como un archivo nuevo.
Ejemplo de código de la vida real
A continuación, puedes ver un ejemplo real de browser-fs-access tal como se usa en Excalidraw. Este extracto está tomado de /src/data/json.ts
. Es de especial interés cómo el método saveAsJSON()
pasa un identificador de archivo o un null
al método fileSave()
de browser-fs-access, lo que hace que se sobrescriba cuando se proporciona un identificador, o que se guarde en un nuevo archivo en caso contrario.
export const saveAsJSON = async (
elements: readonly ExcalidrawElement[],
appState: AppState,
fileHandle: any,
) => {
const serialized = serializeAsJSON(elements, appState);
const blob = new Blob([serialized], {
type: "application/json",
});
const name = `${appState.name}.excalidraw`;
(window as any).handle = await fileSave(
blob,
{
fileName: name,
description: "Excalidraw file",
extensions: ["excalidraw"],
},
fileHandle || null,
);
};
export const loadFromJSON = async () => {
const blob = await fileOpen({
description: "Excalidraw files",
extensions: ["json", "excalidraw"],
mimeTypes: ["application/json"],
});
return loadFromBlob(blob);
};
Consideraciones sobre la interfaz de usuario
Ya sea en Excalidraw o en tu aplicación, la interfaz de usuario debe adaptarse a la situación que el navegador es compatible. Si la API de File System Access es compatible (if ('showOpenFilePicker' in window){}
) puedes mostrar un botón de Guardar como además de un botón de Guardar. Las capturas de pantalla a continuación muestran la diferencia entre la barra de herramientas de la aplicación principal responsiva de Excalidraw en el iPhone y en el escritorio de Chrome. Ten en cuenta que en el iPhone hace falta el botón de Guardar como.
Conclusiones
Trabajar con archivos del sistema funciona técnicamente en todos los navegadores modernos. En los navegadores que admiten la API de File System Access, puedes mejorar la experiencia al permitir el verdadero guardado y la sobrescritura (no solo la descarga) de archivos y al permitir que tus usuarios creen nuevos archivos donde lo deseen, todo sin dejar de ser funcional en los navegadores que no sean compatibles con la API de File System Access API. El browser-fs-access te facilita la vida al lidiar con las sutilezas de la mejora progresiva y hacer que tu código sea lo más simple posible.
Agradecimientos
Este artículo fue revisado por Joe Medley y Kayce Basques. Gracias a los colaboradores de Excalidraw por su trabajo en el proyecto y por revisar mis Pull Requests. Imagen de héroe de Ilya Pavlov en Unsplash.