Como ler e gravar arquivos e diretórios com a biblioteca browser-fs-access

Os navegadores conseguem lidar com arquivos e diretórios há muito tempo. A API File oferece recursos para representar objetos de arquivo em aplicativos da Web, além de selecionar e acessar os dados deles de maneira programática. Mas, quando você olha mais de perto, nem tudo que reluz é ouro.

A maneira tradicional de lidar com arquivos

Abrir arquivos

Como desenvolvedor, você pode abrir e ler arquivos usando o elemento <input type="file">. Na forma mais simples, abrir um arquivo pode ser parecido com o exemplo de código abaixo. O objeto input fornece um FileList, que, no caso abaixo, consiste em apenas um File. Um File é um tipo específico de Blob e pode ser usado em qualquer contexto em que um Blob pode.

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 diretórios

Para abrir pastas (ou diretórios), defina o atributo <input webkitdirectory>. Fora isso, tudo funciona da mesma forma que acima. Apesar do nome com prefixo do fornecedor, webkitdirectory pode ser usado não apenas em navegadores Chromium e WebKit, mas também no Edge legado baseado em EdgeHTML e no Firefox.

Como salvar (ou melhor, baixar) arquivos

Para salvar um arquivo, tradicionalmente, você precisa fazer o download, o que funciona graças ao atributo <a download>. Com um blob, é possível definir o atributo href da âncora como um URL blob: que pode ser obtido com o 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();
};

O problema

Uma grande desvantagem da abordagem de download é que não há como fazer um fluxo clássico de abrir→editar→salvar, ou seja, não há como substituir o arquivo original. Em vez disso, você acaba com uma nova cópia do arquivo original na pasta de downloads padrão do sistema operacional sempre que você "salva".

A API File System Access

A API File System Access simplifica muito as duas operações, abrir e salvar. Ele também permite o salvamento verdadeiro, ou seja, você pode escolher onde salvar um arquivo e substituir um arquivo existente.

Abrir arquivos

Com a API File System Access, abrir um arquivo é uma questão de uma chamada para o método window.showOpenFilePicker(). Essa chamada retorna um identificador de arquivo, com o qual você pode receber o File real pelo 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);
  }
};

Abrir diretórios

Abra um diretório chamando window.showDirectoryPicker(), que torna os diretórios selecionáveis na caixa de diálogo de arquivos.

Como salvar arquivos

Salvar arquivos também é simples. De um identificador de arquivo, crie um stream gravável via createWritable(), grave os dados do blob chamando o método write() do stream e, por fim, feche o stream chamando o 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);
  }
};

Conheça o browser-fs-access

Por mais perfeita que seja a API File System Access, ela ainda não está disponível para todos.

Tabela de suporte do navegador para a API File System Access. Todos os navegadores são marcados como &quot;sem suporte&quot; ou &quot;atrás de uma flag&quot;.
Tabela de compatibilidade de navegadores com a API File System Access. (Fonte)

Por isso, considero a API File System Access um aprimoramento progressivo. Por isso, quero usá-lo quando o navegador for compatível e usar a abordagem tradicional caso contrário. Tudo isso sem punir o usuário com downloads desnecessários de código JavaScript não compatível. A biblioteca browser-fs-access é minha resposta a esse desafio.

Filosofia de design

Como a API File System Access ainda pode mudar no futuro, a API browser-fs-access não é modelada com base nela. Ou seja, a biblioteca não é um polyfill, mas sim um ponyfill. Você pode importar (de forma estática ou dinâmica) exclusivamente a funcionalidade necessária para manter o app o menor possível. Os métodos disponíveis são fileOpen(), directoryOpen() e fileSave(). Internamente, a biblioteca detecta se a API File System Access é compatível e importa o caminho de código correspondente.

Como usar a biblioteca browser-fs-access

Os três métodos são intuitivos de usar. É possível especificar o mimeTypes ou o arquivo extensions aceito do app e definir uma flag multiple para permitir ou não a seleção de vários arquivos ou diretórios. Para mais detalhes, consulte a documentação da API browser-fs-access. O exemplo de código abaixo mostra como abrir e salvar arquivos de imagem.

// 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',
  });
})();

Demonstração

Confira o código acima em ação em uma demonstração no GitHub. O código-fonte também está disponível lá. Por motivos de segurança, os subframes de origem cruzada não podem mostrar um seletor de arquivos. Por isso, não é possível incorporar a demonstração neste artigo.

A biblioteca browser-fs-access em uso

No meu tempo livre, contribuo um pouco para um PWA instalável chamado Excalidraw, uma ferramenta de lousa interativa que permite esboçar diagramas com facilidade e um toque de desenho à mão. Ele é totalmente responsivo e funciona bem em vários dispositivos, desde smartphones pequenos até computadores com telas grandes. Isso significa que ele precisa lidar com arquivos em todas as plataformas, com ou sem suporte à API File System Access. Isso o torna um ótimo candidato para a biblioteca browser-fs-access.

Por exemplo, posso começar um desenho no iPhone, salvar (tecnicamente: fazer o download, já que o Safari não é compatível com a API File System Access) na pasta de downloads do iPhone, abrir o arquivo no computador (depois de transferir do smartphone), modificar o arquivo e substituir com minhas mudanças ou até mesmo salvar como um novo arquivo.

Um desenho do Excalidraw em um iPhone.
Começar um desenho do Excalidraw em um iPhone em que a API File System Access não é compatível, mas em que um arquivo pode ser salvo (baixado) na pasta "Downloads".
O desenho modificado do Excalidraw no Chrome para computador.
Abrir e modificar o desenho do Excalidraw no computador em que a API File System Access é compatível. Assim, o arquivo pode ser acessado pela API.
Substituindo o arquivo original pelas modificações.
Substituindo o arquivo original com as modificações no arquivo de desenho original do Excalidraw. O navegador mostra uma caixa de diálogo perguntando se isso está tudo bem.
Salvar as modificações em um novo arquivo de desenho do Excalidraw.
Salvar as modificações em um novo arquivo do Excalidraw. O arquivo original permanece intacto.

Exemplo de código da vida real

Confira abaixo um exemplo real de browser-fs-access usado no Excalidraw. Este trecho foi extraído de /src/data/json.ts. É interessante saber como o método saveAsJSON() transmite um identificador de arquivo ou null para o método fileSave() do browser-fs-access. Isso faz com que ele substitua quando um identificador é fornecido ou salve em um novo arquivo, caso contrário.

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);
};

Considerações sobre a interface

No Excalidraw ou no seu app, a interface precisa se adaptar à situação de suporte do navegador. Se a API File System Access for compatível (if ('showOpenFilePicker' in window) {}), você poderá mostrar um botão Salvar como além de um botão Salvar. As capturas de tela abaixo mostram a diferença entre a barra de ferramentas principal responsiva do app Excalidraw no iPhone e no Chrome para computador. No iPhone, o botão Salvar como não aparece.

Barra de ferramentas do app Excalidraw no iPhone com apenas um botão &quot;Salvar&quot;.
Barra de ferramentas do app Excalidraw no iPhone com apenas um botão Salvar.
Barra de ferramentas do app Excalidraw no computador Chrome com um botão &quot;Salvar&quot; e outro &quot;Salvar como&quot;.
Barra de ferramentas do app Excalidraw no Chrome com um botão Salvar e um botão Salvar como em destaque.

Conclusões

Tecnicamente, trabalhar com arquivos de sistema funciona em todos os navegadores modernos. Em navegadores que oferecem suporte à API File System Access, é possível melhorar a experiência permitindo o salvamento e a substituição (não apenas o download) de arquivos e deixando que os usuários criem novos arquivos onde quiserem, tudo isso sem perder a funcionalidade em navegadores que não oferecem suporte à API File System Access. A browser-fs-access facilita sua vida ao lidar com as sutilezas do aprimoramento progressivo e simplificar ao máximo seu código.

Agradecimentos

Este artigo foi revisado por Joe Medley e Kayce Basques. Agradeço aos colaboradores do Excalidraw pelo trabalho no projeto e pela revisão dos meus pull requests. Imagem principal de Ilya Pavlov no Unsplash.