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

Os navegadores são capazes de 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 selecioná-las e acessar os dados de maneira programática. Contudo, no momento em que você olha mais de perto, nem tudo que reluz é ouro.

A maneira tradicional de lidar com arquivos

Como abrir arquivos

Como desenvolvedor, você pode abrir e ler arquivos por meio do <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 uma 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();
  });
};

Como abrir diretórios

Para abrir pastas (ou diretórios), você pode definir o <input webkitdirectory> . Fora isso, todo o resto funciona da mesma forma que acima. Apesar do nome prefixado pelo fornecedor, O webkitdirectory não pode ser usado apenas nos navegadores Chromium e WebKit, mas também no Edge baseado em EdgeHTML legado e no Firefox.

Salvar (em vez de fazer o download) de arquivos

Tradicionalmente, você só precisa fazer o download de um arquivo para salvar. que funciona graças à <a download> . Com um Blob, é possível definir o atributo href da âncora para um URL blob: que você pode conseguir do 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 desvantagem da abordagem de download é que não há como criar uma versão clássica abrir → editar → salvar o fluxo acontecer, ou seja, não será possível substituir o arquivo original. Em vez disso, você terá uma nova cópia do arquivo original na pasta "Downloads" padrão do sistema operacional sempre que você "salvar".

A API File System Access

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

Como abrir arquivos

Com a API File System Access, basta chamar o método window.showOpenFilePicker() para abrir um arquivo. Essa chamada retorna um identificador de arquivo, de que você pode acessar o File real com 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 também é simples. Usando um identificador de arquivo, crie um stream gravável via createWritable(). em seguida, grava os dados do Blob chamando o método write() do stream, Por fim, você 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);
  }
};

Conheça o browser-fs-access

Tão perfeitamente quanto a API File System Access, ainda não está amplamente disponível.

Tabela de suporte do navegador 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 do navegador para a API File System Access. (fonte).

É por isso que vejo a API File System Access como um aprimoramento progressivo. Por isso, quero usá-lo quando o navegador oferecer suporte, e, em caso negativo, usar a abordagem tradicional; sem punir o usuário com downloads desnecessários de códigos JavaScript sem suporte. O arquivo browser-fs-access biblioteca é a minha resposta para esse desafio.

Filosofia de design

Como é provável que a API File System Access ainda mude no futuro, a API browser-fs-access não é modelada com base nela. Ou seja, a biblioteca não é um polyfill (link em inglês). mas sim um ponyfill. É possível importar (de maneira estática ou dinâmica) qualquer funcionalidade necessária para manter o app o menor possível. Os métodos disponíveis são os devidamente nomeados fileOpen(), directoryOpen() e fileSave(). Internamente, o recurso da biblioteca detecta se há suporte para a API File System Access e, em seguida, 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 do app ou o arquivo extensions e definir uma flag multiple para permitir ou impedir 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á. Como, por motivos de segurança, subframes de origem cruzada não podem mostrar um seletor de arquivos, não é possível incorporar a demonstração neste artigo.

A biblioteca browser-fs-access em ambiente aberto

No meu tempo livre, contribuo um pouco com uma PWA instalável chamado Excalidraw, uma ferramenta de quadro branco que permite esboçar diagramas facilmente com uma sensação de desenho à mão. Ele é totalmente responsivo e funciona bem em diversos dispositivos, de pequenos celulares a computadores com telas grandes. Isso significa que ele precisa lidar com arquivos em todas as plataformas se eles são compatíveis ou não com a API File System Access. Isso o torna um ótimo candidato para a biblioteca browser-fs-access.

Eu posso, por exemplo, começar a desenhar no meu iPhone, salvá-lo (tecnicamente: fazer download, pois o Safari não é compatível com a API File System Access) na pasta "Downloads" do iPhone, abra o arquivo na área de trabalho (depois de transferi-lo do celular) modificar o arquivo, substituí-lo por minhas alterações ou até 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 em que um arquivo pode ser salvo (baixado) na pasta "Downloads".
.
O desenho do Excalidraw modificado no Chrome na área de trabalho.
Abrir e modificar o desenho do Excalidraw na área de trabalho em que há suporte para a API File System Access e, assim, 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 está tudo bem.
.
Salve as modificações em um novo arquivo de desenho do Excalidraw.
As modificações serão salvas em um novo arquivo do Excalidraw. O arquivo original permanece inalterado.

Exemplo de código real

Confira abaixo um exemplo real do browser-fs-access usado no Excalidraw. Este trecho foi retirado de /src/data/json.ts É interessante observar como o método saveAsJSON() transmite um identificador de arquivo ou null para o browser-fs-access'. fileSave(), que faz com que ele seja substituído quando um identificador é fornecido, ou salvar 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

Seja no Excalidraw ou no seu app, a interface do usuário deve se adaptar à situação de suporte do navegador. Se a API File System Access for compatível (if ('showOpenFilePicker' in window) {}) é possível 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 responsiva do aplicativo principal do Excalidraw no iPhone e no Chrome para computadores. No iPhone, o botão Salvar como 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 a opção &quot;Salvar&quot; e selecione &quot;Salvar como&quot; .
Barra de ferramentas do app Excalidraw no Chrome com um botão Salvar e um botão Salvar como em foco.

Conclusões

Trabalhar com arquivos de sistema tecnicamente funciona em todos os navegadores mais recentes. Em navegadores compatíveis com a API File System Access, é possível melhorar a experiência permitindo para realmente salvar e substituir (não apenas o download) de arquivos e permitindo que os usuários criem novos arquivos onde quiserem continua funcionando em navegadores não compatíveis com a API File System Access. O browser-fs-access facilita sua vida lidando com as sutilezas do aprimoramento progressivo e tornando seu código o mais simples possível.

Agradecimentos

Este artigo foi revisado por Joe Medley e basco kayce. Agradecemos aos colaboradores do Excalidraw pelo trabalho no projeto e por revisar minhas solicitações de envio. Imagem principal de Ilya Pavlov no Unsplash.