Ler e gravar arquivos e diretórios com a biblioteca browser-fs-access
Os navegadores já conseguem lidar com arquivos e diretórios há muito tempo. A API Arquivo fornece recursos para representar objetos de arquivo em aplicativos da web, bem como selecioná-los programaticamente e acessar seus dados. No momento em que você olha mais de perto, porém, nem tudo o que reluz é ouro.
A maneira tradicional de lidar com arquivos
Se você sabe como funcionava do jeito antigo, pode pular direto para o novo jeito.
Abrindo arquivos
Como desenvolvedor, você pode abrir e ler arquivos por meio do elemento <input type="file">
. Em sua forma mais simples, abrir um arquivo pode ser parecido com o exemplo de código abaixo. O objeto de input
FileList
, que no caso a seguir consiste em apenas um File
. Um File
é um tipo específico de Blob
e pode ser usado em qualquer contexto em que um 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();
});
};
Abrindo diretórios
Para abrir pastas (ou diretórios), você pode definir o atributo <input webkitdirectory>
Tirando isso, todo o resto funciona da mesma forma que acima. Apesar de seu nome com o prefixo do fornecedor, webkitdirectory
não pode ser usado apenas nos navegadores Chromium e WebKit, mas também no Edge baseado em EdgeHTML legado e no Firefox.
Salvando (em vez de baixando) arquivos
Para salvar um arquivo, tradicionalmente, você está limitado a baixar um arquivo, o que funciona graças ao atributo <a download>
Dado um Blob, você pode definir o href
da âncora como um blob:
URL que pode ser obtido no método URL.createObjectURL()
Para evitar vazamentos de memória, sempre revogue o URL após o download.
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 abrir → editar → salvar, ou seja, não há como sobrescrever 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 "salva".
A API de acesso ao sistema de arquivos
A API de acesso ao sistema de arquivos torna ambas as operações, abrir e salvar, muito mais simples. Também possibilita o salvamento real, ou seja, você não só pode escolher onde salvar um arquivo, mas também sobrescrever um arquivo existente.
Para obter uma introdução mais completa à API de acesso ao sistema de arquivos, consulte o artigo A API de acesso ao sistema de arquivos: simplificando o acesso a arquivos locais.
Abrindo arquivos
Com a API de acesso ao sistema de arquivos, abrir um arquivo é uma questão de chamar o método window.showOpenFilePicker()
. Esta chamada retorna um identificador de arquivo, do qual você pode obter o File
real por meio do 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);
}
};
Abrindo diretórios
Abra um diretório chamando window.showDirectoryPicker()
que torna os diretórios selecionáveis na caixa de diálogo do arquivo.
Salvando arquivos
Salvar arquivos é igualmente simples. A partir de um identificador de arquivo, você cria um fluxo gravável por meio de createWritable()
, depois grava os dados Blob chamando o write()
do fluxo e, por fim, fecha o fluxo chamando seu 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 o navegador-fs-access
Por mais perfeita que seja a API de acesso ao sistema de arquivos, ela ainda não está amplamente disponível.
É por isso que vejo a API de acesso ao sistema de arquivos como um aprimoramento progressivo. Como tal, quero usá-lo quando o navegador oferecer suporte e, se não for, usar a abordagem tradicional; ao mesmo tempo, nunca pune o usuário com downloads desnecessários de código JavaScript não suportado. A biblioteca browser-fs-access é minha resposta para esse desafio.
Filosofia de design
Como a API de acesso ao sistema de arquivos provavelmente ainda mudará no futuro, a API browser-fs-access não é modelada a partir dela. Ou seja, a biblioteca não é um polyfill, mas sim um ponyfill. Você pode (estaticamente ou dinamicamente) importar exclusivamente qualquer funcionalidade necessária para manter seu aplicativo o menor possível. Os métodos disponíveis são os nomeados apropriadamente fileOpen()
, directoryOpen()
e fileSave()
. Internamente, o recurso de biblioteca detecta se a API de acesso ao sistema de arquivos é compatível e, a seguir, importa o caminho do código correspondente.
Usando a biblioteca browser-fs-access
Os três métodos são intuitivos de usar. Você pode especificar os mimeTypes
ou extensions
arquivo aceitos do seu aplicativo e definir um multiple
para permitir ou proibir a seleção de vários arquivos ou diretórios. Para obter detalhes completos, consulte a documentação da API browser-fs-access. O exemplo de código abaixo mostra como você pode 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',
});
})();
Demo
Você pode ver o código acima em ação em uma demonstração no Glitch. Seu código-fonte também está disponível lá. Como, por razões de segurança, os subquadros de origem cruzada não têm permissão para mostrar um seletor de arquivos, a demonstração não pode ser incorporada neste artigo.
A biblioteca browser-fs-access em liberdade
Em meu tempo livre, contribuo um pouquinho para um PWA instalável chamado Excalidraw, uma ferramenta de quadro branco que permite esboçar diagramas facilmente com uma sensação de desenho à mão. É totalmente responsivo e funciona bem em uma variedade de dispositivos, desde pequenos telefones celulares a computadores com telas grandes. Isso significa que ele precisa lidar com arquivos em todas as várias plataformas, independentemente de serem ou não compatíveis com a API de acesso ao sistema de arquivos. Isso o torna um ótimo candidato para a biblioteca browser-fs-access.
Posso, por exemplo, iniciar um desenho no meu iPhone, salvá-lo (tecnicamente: baixe-o, pois o Safari não oferece suporte à API de acesso ao sistema de arquivos) na pasta Downloads do meu iPhone, abra o arquivo no meu desktop (após transferi-lo do meu telefone), modifique o arquivo e substitua-o com minhas alterações ou mesmo salve-o como um novo arquivo.
Amostra de código de aplicação real
Abaixo, você pode ver um exemplo real de navegador-fs-access como ele é usado no Excalidraw. Este trecho foi retirado de /src/data/json.ts
. É de interesse especial como o saveAsJSON()
passa um identificador de arquivo ou null
para o método browser-fs-access ' fileSave()
, o que faz com que ele seja sobrescrito quando um identificador é fornecido, ou salva 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 de interface do usuário
Seja no Excalidraw ou em seu aplicativo, a IU deve se adaptar à situação de suporte do navegador. Se a API de acesso ao sistema de arquivos for suportada (if ('showOpenFilePicker' in window) {}
), você pode mostrar um botão Salvar como além de um botão Salvar. As imagens abaixo mostram a diferença entre a barra de ferramentas responsiva do aplicativo principal do Excalidraw no iPhone e na área de trabalho do Chrome. Observe como no iPhone o botão Salvar como está faltando.
Conclusões
Trabalhar com arquivos de sistema funciona tecnicamente em todos os navegadores modernos. Em navegadores que suportam a API de acesso ao sistema de arquivos, você pode tornar a experiência melhor permitindo o verdadeiro salvamento e sobrescrita (não apenas o download) de arquivos e permitindo que seus usuários criem novos arquivos onde quiserem, ao mesmo tempo em que permanecem funcionais em navegadores que não suporta a API de acesso ao sistema de arquivos. O navegador-fs-access torna sua vida mais fácil, lidando com as sutilezas do aprimoramento progressivo e tornando seu código o mais simples possível.
Reconhecimentos
Este artigo foi revisado por Joe Medley e Kayce Basques. Agradeço aos colaboradores da Excalidraw por seu trabalho no projeto e por revisar minhas solicitações de pull. Imagem do herói por Ilya Pavlov em Unsplash.