Lee y escribe archivos y directorios con la biblioteca browser-fs-access

Los navegadores han podido trabajar con archivos y directorios durante mucho tiempo. La API de File proporciona funciones para representar objetos de archivo en aplicaciones web, además de seleccionarlos de manera programática y acceder a sus datos. Sin embargo, en el momento que miras más de cerca, todo lo que brilla no es oro.

La forma tradicional de trabajar con archivos

Cómo abrir archivos

Como desarrollador, puedes abrir y leer archivos mediante el elemento <input type="file">. En su forma más simple, abrir un archivo puede verse como el ejemplo de código que se muestra a continuación. El objeto input te proporciona un FileList que, en el siguiente caso, consta de solo una File. Un File es un tipo específico de Blob y se puede usar en cualquier contexto que pueda hacer un BLOB.

const openFile = async () => {
  return new Promise((resolve) => {
    const input = document.createElement('input');
    input.type = 'file';
    input.addEventListener('change', () => {
      resolve(input.files[0]);
    });
    input.click();
  });
};

Cómo abrir directorios

Para abrir carpetas (o directorios), puedes establecer el atributo <input webkitdirectory>. Aparte de eso, todo lo demás funciona igual que lo anterior. A pesar de su nombre con prefijo del proveedor, webkitdirectory no solo se puede usar en los navegadores Chromium y WebKit, sino también en la versión heredada de Edge basada en EdgeHTML y en Firefox.

Guardar (es decir, descargar) archivos

Para guardar un archivo, por lo general, solo puedes descargar un archivo, lo que funciona gracias al atributo <a download>. En un BLOB, puedes configurar el atributo href del ancla en una URL blob: que puedes obtener del método URL.createObjectURL().

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 gran desventaja del enfoque de descarga es que no hay forma de que ocurra un flujo clásico de apertura→edición→guardar, es decir, no hay forma de reemplazar el archivo original. En su lugar, cada vez que realizas una acción, obtienes una copia nueva del archivo original en la carpeta Descargas predeterminada del sistema operativo.

La API de File System Access

La API de File System Access facilita mucho las operaciones, la apertura y el guardado. También habilita el guardado real, es decir, no solo puedes elegir dónde guardar un archivo, sino también reemplazar uno existente.

Cómo abrir archivos

Con la API de File System Access, abrir un archivo solo requiere una llamada al método window.showOpenFilePicker(). Esta llamada muestra un controlador de archivo, desde el que puedes obtener el File real a través del método getFile().

const openFile = async () => {
  try {
    // Always returns an array.
    const [handle] = await window.showOpenFilePicker();
    return handle.getFile();
  } catch (err) {
    console.error(err.name, err.message);
  }
};

Cómo abrir directorios

Abre un directorio llamando a window.showDirectoryPicker(), que permite seleccionar directorios en el cuadro de diálogo de archivos.

Cómo guardar archivos

Guardar archivos es igualmente sencillo. Desde un controlador de archivos, debes crear una transmisión que admita escritura a través de createWritable(); luego, debes escribir los datos BLOB mediante una llamada al método write() de la transmisión y, por último, cerrar la transmisión 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);
  }
};

Presentamos navegador-fs-access

Tan perfectamente bien como lo es la API de File System Access, aún no está ampliamente disponible.

Tabla de compatibilidad del navegador para la API de File System Access. Todos los navegadores están marcados como &quot;no compatible&quot; o &quot;detrás de una marca&quot;.
Tabla de compatibilidad del navegador para la API de File System Access. (Fuente)

Es por eso que veo la API de File System Access como una mejora progresiva. Por lo tanto, quiero usarlo cuando el navegador lo admita y, de lo contrario, usar el enfoque tradicional; todo sin castigar al usuario con descargas innecesarias de código JavaScript no compatible. La biblioteca browser-fs-access es mi respuesta a este desafío.

Filosofía de diseño

Dado que es probable que la API de File System Access aún 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 (de forma estática o dinámica) importar de forma exclusiva cualquier funcionalidad que necesites para mantener tu app lo más pequeña posible. Los métodos disponibles son los nombrados correctamente fileOpen(), directoryOpen() y fileSave(). De forma interna, la función de la biblioteca detecta si se admite la API de File System Access y, luego, importa la ruta de código correspondiente.

Usa la biblioteca browser-fs-access

Los tres métodos son intuitivos. Puedes especificar el mimeTypes o el extensions del archivo aceptado de tu app y establecer una marca multiple para permitir o rechazar la selección de varios archivos o directorios. Para obtener más información, consulta la documentación de la API de navegador-fs-access. El siguiente ejemplo de código muestra cómo puedes abrir y guardar archivos de imagen.

// The imported methods will use the File
// System Access API or a fallback implementation.
import {
  fileOpen,
  directoryOpen,
  fileSave,
} from 'https://unpkg.com/browser-fs-access';

(async () => {
  // Open an image file.
  const blob = await fileOpen({
    mimeTypes: ['image/*'],
  });

  // Open multiple image files.
  const blobs = await fileOpen({
    mimeTypes: ['image/*'],
    multiple: true,
  });

  // Open all files in a directory,
  // recursively including subdirectories.
  const blobsInDirectory = await directoryOpen({
    recursive: true
  });

  // Save a file.
  await fileSave(blob, {
    fileName: 'Untitled.png',
  });
})();

Demostración

Puedes ver el código anterior en acción en una demostración en Glitch. Su código fuente también está disponible allí. Debido a que, por motivos de seguridad, los submarcos de origen cruzado no pueden mostrar un selector de archivos, la demostración no se puede incorporar en este artículo.

La biblioteca navegador-fs-access en el exterior

En mi tiempo libre, contribuyo un poco a una AWP instalable llamada Excalidraw, una herramienta de pizarra que te permite dibujar diagramas fácilmente con un estilo dibujado a mano. Es totalmente adaptable y funciona bien en una variedad de dispositivos, desde teléfonos celulares pequeños hasta computadoras con pantallas grandes. Esto significa que necesita manejar archivos en todas las plataformas, sin importar si admiten o no la API de File System Access. Esto lo convierte en un gran candidato para la biblioteca browser-fs-access.

Por ejemplo, puedo 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 reemplazarlo con mis cambios o incluso guardarlo como un archivo nuevo.

Un dibujo de Excalidraw en un iPhone.
Cómo iniciar un dibujo de Excalidraw en un iPhone en el que la API de File System Access no es compatible, pero el archivo se puede guardar (descargar) en la carpeta Descargas.
El dibujo modificado de Excalidraw en Chrome para escritorio.
Abrir y modificar el dibujo de Excalidraw en la computadora de escritorio donde se admite la API de File System Access y, por lo tanto, se puede acceder al archivo a través de la API.
Reemplazar el archivo original con las modificaciones
Reemplaza el archivo original con las modificaciones en el archivo de dibujo original de Excalidraw. El navegador muestra un diálogo para preguntarme si se puede hacer eso.
Guardar las modificaciones en un nuevo archivo de dibujo de Excalidraw
Guardar las modificaciones en un nuevo archivo de Excalidraw El archivo original permanecerá intacto.

Muestra de código en la vida real

A continuación, puedes ver un ejemplo real de browser-fs-access como se utiliza en Excalidraw. Este extracto se tomó de /src/data/json.ts. Es de especial interés la forma en que el método saveAsJSON() pasa un controlador de archivo o null al método fileSave() de browser-fs-access, lo que hace que se reemplace cuando se otorgue un identificador o, de lo contrario, se guarde en un archivo nuevo.

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 de la IU

Ya sea en Excalidraw o en tu app, la IU debe adaptarse a la situación de compatibilidad del navegador. Si se admite la API de File System Access (if ('showOpenFilePicker' in window) {}), puedes mostrar un botón Save As (Guardar como) además de Save (Guardar). En las siguientes capturas de pantalla, se muestra la diferencia entre la barra de herramientas responsiva de la app principal de Excalidraw en iPhone y en el escritorio de Chrome. Ten en cuenta que en iPhone falta el botón Save As.

Barra de herramientas de la app Excalidraw en iPhone con solo un botón de “Guardar”.
Barra de herramientas de la app Excalidraw en iPhone con solo un botón Guardar.
Barra de herramientas de la app Excalidraw en el escritorio de Chrome con el botón &quot;Save&quot; y el botón &quot;Save As&quot;.
Barra de herramientas de la app Excalidraw en Chrome con el botón Save y Save As enfocado.

Conclusiones

Técnicamente, trabajar con archivos del sistema funciona en todos los navegadores modernos. En los navegadores compatibles con la API de File System Access, puedes mejorar la experiencia si permites guardar y reemplazar (no solo descargar) archivos, y si permites que los usuarios creen archivos nuevos donde lo deseen, todo sin dejar de funcionar en navegadores que no admiten la API de File System Access. El comando browser-fs-access te facilita la vida, ya que se ocupa de las sutilezas de la mejora progresiva y hace que tu código sea lo más simple posible.

Agradecimientos

Joe Medley y Kayce Basques revisaron este artículo. Gracias a los colaboradores de Excalidraw por su trabajo en el proyecto y por revisar mis solicitudes de extracción. Hero image de Ilya Pavlov en Unsplash.