Publicado em: 27 de julho de 2020
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
Você pode abrir e ler arquivos com o elemento
<input type="file">.
Na forma mais simples, abrir um arquivo pode ser parecido com o exemplo de código.
O objeto input oferece um FileList, que, no caso do nosso exemplo, 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 baseado em EdgeHTML e no Firefox.
Salvar e baixar arquivos
Para salvar um arquivo, tradicionalmente, você precisa baixar o arquivo, 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. Isso também permite uma economia real. Isso significa que você pode escolher onde salvar o 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 é possível 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.
Salvar arquivos
Salvar arquivos também é simples.
De um identificador de arquivo, crie um stream gravável usando createWritable(),
grave os dados do blob chamando o método write() do stream
e 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.
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 os apropriadamente chamados
fileOpen(),
directoryOpen() e
fileSave().
Internamente, a biblioteca detecta se a API File System Access é compatível
e importa o caminho de código correspondente.
Usar a biblioteca
Os três métodos são intuitivos de usar.
É possível especificar o mimeTypes ou o 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 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 em ação em uma demonstração do GitHub. O código-fonte também está disponível lá.
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 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 meu 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.
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.
Observe como o botão Salvar como não aparece no iPhone.
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
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.