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

Los navegadores han podido manejar archivos y directorios durante mucho tiempo. La API de File proporciona funciones para representar objetos de archivo en aplicaciones web, así como seleccionarlos de forma programática y acceder a sus datos. Sin embargo, en cuanto te acercas, te das cuenta de que no todo lo que brilla es oro.

Cómo 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 verse como el siguiente ejemplo de código. El objeto input te proporciona un FileList, que, en el siguiente caso, consta de un solo File. Un File es un tipo específico de Blob y se puede usar en cualquier contexto que pueda usar 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 configurar 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 el Edge heredado basado en EdgeHTML y en Firefox.

Guardar (en lugar de descargar) archivos

Tradicionalmente, para guardar un archivo, solo puedes descargarlo, lo que funciona gracias al atributo <a download>. Dado un BLOB, puedes establecer 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 hacer que se produzca un flujo clásico de abrir→editar→guardar, es decir, no hay forma de reemplazar el archivo original. En cambio, obtienes una nueva copia del archivo original en la carpeta Descargas predeterminada del sistema operativo cada vez que “guardas”.

La API de File System Access

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

Cómo abrir archivos

Con la API de File System Access, se debe llamar al método window.showOpenFilePicker() para abrir un archivo. Esta llamada muestra un identificador 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

Para abrir un directorio, llama a window.showDirectoryPicker(), que permite seleccionar directorios en el cuadro de diálogo del archivo.

Cómo guardar archivos

Guardar archivos es igualmente sencillo. Desde un identificador de archivo, creas un flujo de escritura a través de createWritable(), luego, escribes los datos del BLOB llamando al método write() del flujo y, por último, cierras el flujo 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);
  }
};

Presentación de browser-fs-access

A pesar de que la API de File System Access es muy buena, aún no está disponible en todos los lugares.

Tabla de compatibilidad de navegadores para la API de File System Access. Todos los navegadores se marcan como &quot;no compatibles&quot; o &quot;con una marca&quot;.
Tabla de compatibilidad de navegadores para la API de File System Access. (Fuente)

Por eso, considero que la API de File System Access es una mejora progresiva. Por lo tanto, quiero usarlo cuando el navegador lo admita y usar el enfoque tradicional si no es así, 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 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 importar (de forma estática o dinámica) exclusivamente cualquier funcionalidad que necesites para mantener tu app lo más pequeña posible. Los métodos disponibles son los apropiadamente nombrados 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.

Cómo usar la biblioteca browser-fs-access

Los tres métodos son intuitivos. Puedes especificar el mimeTypes o el archivo extensions aceptados de tu app, y establecer una marca multiple para permitir o denegar la selección de varios archivos o directorios. Para obtener más información, consulta la documentación de la API debrowser-fs-access. En la siguiente muestra de código, se indica cómo 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í. Dado 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 de navegador-fs-access en el exterior

En mi tiempo libre, colaboro un poco en una PWA instalable llamada Excalidraw, una herramienta de pizarra que te permite esbozar diagramas fácilmente con un estilo de dibujo a mano. Es totalmente responsivo y funciona bien en una variedad de dispositivos, desde teléfonos celulares pequeños hasta computadoras con pantallas grandes. Esto significa que debe administrar 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 admite la API de acceso al sistema de archivos) en la carpeta Descargas de mi iPhone, abrir el archivo en mi computadora de escritorio (después de transferirlo desde mi teléfono), modificarlo y reemplazarlo con mis cambios, o incluso guardarlo como un archivo nuevo.

Un dibujo de Excalidraw en un iPhone.
Iniciar un dibujo de Excalidraw en un iPhone en el que no se admite la API de File System Access, pero en el que se puede guardar (descargar) un archivo en la carpeta Descargas.
El dibujo modificado de Excalidraw en Chrome para computadoras.
Abrir y modificar el dibujo de Excalidraw en el escritorio en el que se admite la API de File System Access y, por lo tanto, se puede acceder al archivo a través de la API.
Reemplaza el archivo original con las modificaciones.
Reemplaza el archivo original con las modificaciones del archivo de dibujo original de Excalidraw. El navegador muestra un diálogo en el que se me pregunta si estoy de acuerdo.
Guardar los cambios en un nuevo archivo de dibujo de Excalidraw
Guardar las modificaciones en un nuevo archivo de Excalidraw El archivo original no se modifica.

Muestra 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 se toma de /src/data/json.ts. De especial interés es cómo el método saveAsJSON() pasa un identificador de archivo o null al método fileSave() de browser-fs-access, lo que hace que se reemplace cuando se proporciona un identificador o se guarde en un archivo nuevo si no es así.

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 asistencia del navegador. Si la API de File System Access es compatible (if ('showOpenFilePicker' in window) {}), puedes mostrar un botón Save As además del botón Save. Las siguientes capturas de pantalla muestran la diferencia entre la barra de herramientas responsiva de la app principal de Excalidraw en iPhone y en Chrome para computadoras de escritorio. Observa que en el iPhone falta el botón Guardar como.

Barra de herramientas de la app de Excalidraw en iPhone con solo un botón “Guardar”.
Barra de herramientas de la app de Excalidraw en iPhone con solo un botón Guardar.
Barra de herramientas de la app de Excalidraw en la versión para computadoras de Chrome con los botones &quot;Guardar&quot; y &quot;Guardar como&quot;.
Barra de herramientas de la app de Excalidraw en Chrome con los botones Guardar y Guardar como enfocados.

Conclusiones

Técnicamente, trabajar con archivos del sistema funciona en todos los navegadores modernos. En los navegadores que admiten la API de File System Access, puedes mejorar la experiencia permitiendo el guardado y la sustitución reales (no solo la descarga) de archivos, y permitiendo que los usuarios creen archivos nuevos donde quieran, todo sin dejar de ser funcional en los navegadores que no admiten la API de File System Access. 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. Imagen hero de Ilya Pavlov en Unsplash.