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

Los navegadores tienen la capacidad de administrar archivos y directorios desde hace mucho tiempo. La API de File ofrece funciones para representar objetos de archivo en aplicaciones web. además de seleccionarlos programáticamente y acceder a sus datos. Sin embargo, cuando miras más de cerca, no todo lo que brilla es oro.

La forma tradicional de tratar los archivos

Cómo abrir archivos

Como desarrollador, puedes abrir y leer archivos a través de la <input type="file"> . En su forma más simple, abrir un archivo puede verse como el ejemplo de código que aparece a continuación. El objeto input te proporciona un FileList. que, en el siguiente caso, consiste en solo uno 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 establecer el <input webkitdirectory> . Aparte de eso, todo lo demás funciona igual que lo anterior. A pesar de su nombre con el 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

Para guardar un archivo, tradicionalmente solo hay que descargar un archivo, que funciona gracias al <a download> . Dado un BLOB, puedes establecer el atributo href del ancla en una URL blob: que puedes obtener de la 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 manera de crear un se realizan los flujos de open→edit→save, es decir, no hay forma de reemplazar el archivo original. En cambio, obtendrás una copia nueva del archivo original. en la carpeta Descargas predeterminada del sistema operativo cada vez que guardes los cambios.

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 verdadero guardado, es decir, no solo puede elegir dónde guardar un archivo, pero también sobrescribir un archivo existente.

Cómo abrir archivos

Con la API de File System Access, Para abrir un archivo, solo se necesita una llamada al método window.showOpenFilePicker(). Esta llamada muestra un controlador de archivo, del cual 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 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 controlador de archivos, creas una transmisión que admite escritura a través de createWritable(). Luego, escribes los datos BLOB mediante una llamada al método write() de la transmisión, y, por último, cierras 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);
  }
};

Presentación de browser-fs-access

Aunque está perfectamente bien 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 compatibles&quot;. o &quot;detrás de una bandera&quot;.
Tabla de compatibilidad del navegador para la API de File System Access. (Fuente)
.

Por eso, veo la API de File System Access como mejora progresiva. Por eso, quiero usarlo cuando el navegador lo admita y, si no, usa el enfoque tradicional. sin penalizar al usuario con descargas innecesarias de código JavaScript no compatible. La clase browser-fs-access biblioteca 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) de forma exclusiva cualquier funcionalidad que necesites para que tu app sea lo más pequeña posible. Los métodos disponibles son los métodos fileOpen(): directoryOpen() y fileSave() Internamente, la función de la biblioteca detecta si la API de File System Access es compatible. y, luego, importa la ruta de acceso de código correspondiente.

Cómo usar la biblioteca de browser-fs-access

Los tres métodos son intuitivos. Puedes especificar el mimeTypes aceptado de tu app o el archivo extensions 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 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í. Debido a que, por motivos de seguridad, los submarcos de origen cruzado no pueden mostrar un selector de archivos, la demostración no puede estar incorporada en este artículo.

La biblioteca de browser-fs-access en el exterior

En mi tiempo libre, contribuyo un poco a una AWP instalable llamada Excalidraw una herramienta de pizarra que permite dibujar diagramas fácilmente y dibujar a mano. Es totalmente responsiva y funciona bien en una gran variedad de dispositivos, desde teléfonos celulares pequeños hasta computadoras con pantallas grandes. Esto significa que tiene que trabajar con 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 del navegador fs-access.

Puedo, por ejemplo, empezar a dibujar en mi iPhone guárdalo (técnicamente: descárgalo, ya que Safari no es compatible con la API de File System Access) a 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.
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.
Dibujo de Excalidraw modificado en Chrome en una computadora de escritorio.
Apertura y modificación del 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
Reemplazar el archivo original con las modificaciones realizadas en el archivo de dibujo original de Excalidraw. El navegador muestra un diálogo en el que se me pregunta si está bien.
Guardar las modificaciones en un nuevo archivo de dibujo de Excalidraw.
Guardar las modificaciones en un nuevo archivo Excalidraw. El archivo original permanecerá intacto.

Muestra de código de la vida real

A continuación, puedes ver un ejemplo real de navegador-fs-access, ya que se usa en Excalidraw. Este extracto se tomó del /src/data/json.ts Es de especial interés la manera en que el método saveAsJSON() pasa un controlador de archivos o null a navegador-fs-access. fileSave(), que hace que se sobrescriba cuando se proporciona un identificador o guardarlos 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 sobre la IU

Ya sea en Excalidraw o tu app, la IU debe adaptarse a la situación de asistencia del navegador. Si se admite la API de File System Access (if ('showOpenFilePicker' in window) {}) puedes mostrar el botón Save As además del 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 falta el botón Save As en iPhone.

La barra de herramientas de la app Excalidraw en iPhone, con solo hacer clic en &quot;Guardar&quot; .
Excalidraw la barra de herramientas de la app en iPhone con solo un botón Save.
Barra de herramientas de la app Excalidraw en la versión de escritorio de Chrome con un botón &quot;Guardar&quot; y &quot;Guardar como&quot; .
Excalidraw la barra de herramientas de la app en Chrome con un botón Guardar y un botón Guardar como enfocado.

Conclusiones

Trabajar con archivos del sistema técnicamente funciona en todos los navegadores modernos. Para mejorar la experiencia de los navegadores que admiten la API de File System Access, debes permitir para el verdadero guardado y reemplazo (no solo la descarga) de archivos y permitiendo que los usuarios creen archivos nuevos donde quieran, y siguen funcionando en navegadores que no son compatibles con la API de File System Access. browser-fs-access facilita tu trabajo Aborda las sutilezas de la mejora progresiva y haz tu código lo más simple posible.

Agradecimientos

Joe Medley revisó este artículo, y Kayce Basques. 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.