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

Os navegadores lidam 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 selecioná-los e acessar os dados de maneira programática. No entanto, quando você olha mais de perto, nem tudo que reluz é ouro.

A forma tradicional de lidar com arquivos

Abrir arquivos

Como desenvolvedor, você pode abrir e ler arquivos usando o elemento <input type="file">. Em sua forma mais simples, abrir um arquivo pode ser semelhante ao 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 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();
  });
};

Como abrir diretórios

Para abrir pastas (ou diretórios), defina o atributo <input webkitdirectory>. Além disso, todo o restante funciona da mesma forma acima. Apesar do nome prefixado pelo fornecedor, o webkitdirectory não é apenas utilizável nos navegadores Chromium e WebKit, mas também no Edge legado baseado em EdgeHTML, bem como no Firefox.

Salvar (em vez de fazer o download) de arquivos

Tradicionalmente, para salvar um arquivo, você só pode fazer o download de um arquivo, o que funciona graças ao atributo <a download>. Dado um Blob, você pode definir o atributo href da âncora como um URL blob: que pode ser recebido do 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 acontecer, ou seja, não há como substituir o arquivo original. Em vez disso, você vai acabar com uma nova cópia do arquivo original na pasta "Downloads" padrão do sistema operacional sempre que "salvar".

A API File System Access

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

Abrir arquivos

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

Como abrir diretórios

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

Como salvar arquivos

Salvar arquivos é igualmente simples. Em um gerenciador de arquivo, crie um stream gravável por createWritable() e 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);
  }
};

Apresentando browser-fs-access

A API File System Access pode ser perfeita, mas ainda não está amplamente disponível.

Tabela de suporte a navegadores para a API File System Access. Todos os navegadores estão marcados como &quot;sem suporte&quot; ou &quot;atrás de uma sinalização&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. Sendo assim, quero usá-lo quando o navegador oferecer suporte a ele e, caso contrário, usar a abordagem tradicional, sem punir o usuário com downloads desnecessários de código JavaScript sem suporte. A biblioteca browser-fs-access é a minha resposta para esse desafio.

Filosofia de design

Como a API File System Access ainda provavelmente será alterada no futuro, a API browser-fs-access não é modelada com base nela. Ou seja, a biblioteca não é um polyfill, mas um preenchimento de pônei. Você pode importar (estática ou dinamicamente) exclusivamente qualquer funcionalidade necessária para manter o app o menor possível. Os métodos disponíveis são os nomes fileOpen(), directoryOpen() e fileSave(). Internamente, o recurso da biblioteca detecta se há suporte para a API File System Access e importa o caminho do código correspondente.

Como usar a biblioteca browser-fs-access

Os três métodos são intuitivos. Você pode especificar a mimeTypes aceita pelo app ou o extensions do arquivo e definir uma sinalização multiple para permitir ou proibir 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

Veja o código acima em ação em uma demonstração do Glitch. O código-fonte também está disponível nessa seção. Como os subframes de origem cruzada não podem mostrar um seletor de arquivos por motivos de segurança, a demonstração não pode ser incorporada a este artigo.

A biblioteca browser-fs-access em execução

No meu tempo livre, contribuo um pouco para um PWA instalável chamado Excalidraw, uma ferramenta de lousa interativa que permite esboçar diagramas facilmente com uma sensação 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 processar arquivos em todas as plataformas, independentemente de serem compatíveis ou não com a API File System Access. Isso o torna um ótimo candidato para a biblioteca browser-fs-access.

Posso, por exemplo, iniciar um desenho no meu iPhone, salvá-lo (tecnicamente: fazer o download, já que o Safari não é compatível com a API File System Access) na minha pasta de downloads do iPhone, abrir o arquivo na área de trabalho (depois de transferi-lo do meu smartphone), modificar o arquivo e substituí-lo pelas minhas alterações ou até mesmo salvá-lo 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 é compatível, mas onde um arquivo pode ser salvo (transferido por download) na pasta "Downloads".
O desenho Excalidraw modificado no Chrome na área de trabalho.
Abrir e modificar o desenho do Excalidraw na área de trabalho em que a API File System Access é compatível e, portanto, o arquivo pode ser acessado pela API.
Substituindo o arquivo original com as 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 está tudo certo.
As modificações serão salvas em um novo arquivo de desenho do Excalidraw.
As modificações foram salvas em um novo arquivo do Excalidraw. O arquivo original permanece inalterado.

Exemplo de código real

Abaixo, você pode conferir um exemplo real de browser-fs-access como é usado no Excalidraw. Este trecho foi retirado de /src/data/json.ts. É interessante como o método saveAsJSON() transmite um identificador de arquivo ou null para o método fileSave() do navegador-fs-access, o que faz com que ele seja substituído quando um identificador é fornecido ou, caso contrário, para salvar em um novo arquivo.

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 houver suporte para a API File System Access (if ('showOpenFilePicker' in window) {}), você poderá mostrar um botão Save As e Save. As capturas de tela abaixo mostram a diferença entre a barra de ferramentas principal responsiva do Excalidraw no iPhone e no Chrome para computador. Observe como o botão Save As está ausente no iPhone.

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 na área de trabalho do Chrome com um botão &quot;Salvar&quot; e um botão &quot;Salvar como&quot;.
A barra de ferramentas do app Excalidraw no Chrome com um botão Salvar e um botão Salvar como focados.

Conclusões

Tecnicamente, trabalhar com arquivos de sistema funciona em todos os navegadores mais recentes. Em navegadores compatíveis com a API File System Access, você pode melhorar a experiência permitindo salvar e substituir (não apenas fazer o download) de arquivos e permitir que os usuários criem novos arquivos onde quiserem, tudo isso enquanto continua funcional em navegadores que não são compatíveis com a 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 analisarem minhas solicitações de envio. Imagem principal por Ilya Pavlov (link em inglês) no Unsplash.