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

Os navegadores já lidam com arquivos e diretórios há muito tempo. A API File oferece recursos para representar objetos de arquivo em aplicativos da Web, bem como selecioná-los e acessar os dados de maneira programática. No entanto, quando você olha mais de perto, percebe que nem tudo que reluz é ouro.

A maneira tradicional de lidar com arquivos

Como abrir arquivos

Como desenvolvedor, você pode abrir e ler arquivos pelo 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 uma 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 possa.

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 prefixado pelo fornecedor, webkitdirectory não pode ser usado apenas nos navegadores Chromium e WebKit, mas também no Edge baseado em EdgeHTML legado, bem como no Firefox.

Salvar (em vez de fazer o download) de arquivos

Para salvar um arquivo, tradicionalmente, você está limitado a fazer o download de um arquivo, o que funciona graças ao atributo <a download>. Com um Blob, é possível definir o atributo href do âncora para um URL blob: que pode ser acessado pelo 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ê terá uma nova cópia do arquivo original na pasta de downloads padrão do sistema operacional sempre que "salvar".

A API File System Access

A API File System Access simplifica muito as operações de abertura e salvamento. Ele também permite salvamento real, ou seja, você pode não apenas escolher onde salvar um arquivo, mas também substituir um arquivo existente.

Como 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, em que você pode acessar 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 arquivo.

Como salvar arquivos

Salvar arquivos também é simples. Em um identificador de arquivo, você cria um stream gravável usando createWritable(), grava os dados do Blob chamando o método write() do stream e, por fim, fecha o stream chamando o método close() dele.

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

Introdução ao browser-fs-access

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

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

É por isso que vejo a API File System Access como um aprimoramento progressivo. Por isso, quero usar o JavaScript quando o navegador for compatível com ele e usar a abordagem tradicional se não for; sem punir o usuário com downloads desnecessários de código JavaScript sem suporte. A biblioteca browser-fs-access é minha resposta para esse desafio.

Filosofia de design

Como a API File System Access ainda pode mudar no futuro, a API browser-fs-access não é modelada a partir dela. Ou seja, a biblioteca não é um polyfill, mas um ponyfill. É possível importar (de forma estática ou dinâmica) exclusivamente qualquer funcionalidade necessária para manter o app o menor possível. Os métodos disponíveis são fileOpen(), directoryOpen() e fileSave(). Internamente, o recurso da biblioteca detecta se a API File System Access é compatível e, em seguida, 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 pelo 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 do Glitch. O código-fonte dele também está disponível lá. Por motivos de segurança, os subframes de origem cruzada não podem mostrar um seletor de arquivos. Portanto, a demonstração não pode ser incorporada a este artigo.

A biblioteca browser-fs-access no mundo real

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

Por exemplo, posso iniciar um desenho no meu iPhone, salvá-lo (tecnicamente: fazer o download, já que o Safari não oferece suporte à API File System Access) na pasta "Downloads" do iPhone, abrir o arquivo no computador (depois de transferir do meu smartphone), modificar o arquivo e substituí-lo com minhas alterações ou até mesmo salvar como um novo arquivo.

Um desenho do Excalidraw em um iPhone.
Iniciar um desenho do Excalidraw em um iPhone em que a API File System Access não tem suporte, mas em que um arquivo pode ser salvo (feito o download) na pasta "Downloads".
O desenho do Excalidraw modificado no Chrome na área de trabalho.
Abrir e modificar o desenho do Excalidraw no computador, em que a API File System Access é compatível e, portanto, o arquivo pode ser acessado pela API.
Substituir o arquivo original pelas modificações.
Substituir o arquivo original pelas modificações no arquivo de desenho original do Excalidraw. O navegador mostra uma caixa de diálogo perguntando se isso está tudo bem.
Salvando 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 real

Confira abaixo um exemplo real de browser-fs-access conforme usado no Excalidraw. Este trecho foi retirado de /src/data/json.ts. O método saveAsJSON() é de interesse especial, pois ele transmite um identificador de arquivo ou null para o método fileSave() do browser-fs-access, o que faz com que ele seja substituído quando um identificador é fornecido ou salvo em um novo arquivo, se não for.

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

Seja no Excalidraw ou no seu app, a interface precisa se adaptar à situação de suporte do navegador. Se a API File System Access tiver suporte (if ('showOpenFilePicker' in window) {}), você poderá mostrar um botão Save As, além de um botão Save. As capturas de tela abaixo mostram a diferença entre a barra de ferramentas responsiva do aplicativo principal do Excalidraw no iPhone e no Chrome para computadores. Observe que, no iPhone, o botão Save As está ausente.

Barra de ferramentas do app Excalidraw no iPhone com apenas um botão &quot;Save&quot;.
Barra de ferramentas do app Excalidraw no iPhone com apenas um botão Save.
Barra de ferramentas do app Excalidraw no Chrome para computador com os botões &quot;Salvar&quot; e &quot;Salvar como&quot;.
Barra de ferramentas do app Excalidraw no Chrome com um botão Save e um botão Save As focado.

Conclusões

Trabalhar com arquivos de sistema tecnicamente funciona em todos os navegadores mais recentes. Em navegadores com suporte à API File System Access, você pode melhorar a experiência permitindo realizar o salvamento e a substituição reais (não apenas o download) de arquivos e permitindo que os usuários criem novos arquivos onde quiserem, tudo isso enquanto permanecem funcionais em navegadores que não oferecem suporte à API File System Access. O browser-fs-access facilita sua vida ao lidar com as sutilezas do aprimoramento progressivo e tornar seu código o mais simples possível.

Agradecimentos

Este artigo foi revisado por Joe Medley e Kayce Basques. Agradeço aos colaboradores do Excalidraw pelo trabalho no projeto e por analisar minhas solicitações de pull. Imagem principal de Ilya Pavlov no Unsplash.