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 ser usado.
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
não pode ser usado apenas em navegadores Chromium e WebKit, mas também no EdgeHTML legado e no Firefox.
Salvar (ou seja, fazer o download de) arquivos
Para salvar um arquivo, tradicionalmente, você fica limitado a fazer o download dele,
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.
Por isso, 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 é baseada nela.
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 detalhes completos, 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 no Glitch. O código-fonte 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 faz com que ele seja 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 transferi-lo do meu smartphone), modificar o arquivo e substituí-lo com minhas alterações ou até mesmo salvar como um novo arquivo.
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()
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 principal responsiva do Excalidraw no iPhone e no Chrome para computador.
Observe que, no iPhone, o botão Save As está ausente.
Conclusões
Tecnicamente, o trabalho com arquivos do 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 reais (não apenas o download) de arquivos e permitindo que os usuários criem novos arquivos onde quiserem, mantendo a funcionalidade 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.