O sistema de arquivos privados de origem

O padrão do sistema de arquivos apresenta um sistema de arquivos privado de origem (OPFS, na sigla em inglês) como um endpoint de armazenamento privado para a origem da página e não visível para o usuário, que oferece acesso opcional a um tipo especial de arquivo altamente otimizado para desempenho.

Suporte ao navegador

O sistema de arquivos privado de origem é compatível com navegadores modernos e padronizado pelo grupo de trabalho de tecnologia de aplicativos de hipertexto da Web (WHATWG) no Padrão ativo do sistema de arquivos (em inglês).

Browser Support

  • Chrome: 86.
  • Edge: 86.
  • Firefox: 111.
  • Safari: 15.2.

Source

Motivação

Quando você pensa em arquivos no computador, provavelmente pensa em uma hierarquia: arquivos organizados em pastas que podem ser exploradas com o gerenciador de arquivos do sistema operacional. Por exemplo, no Windows, para um usuário chamado Tom, a lista de tarefas pode estar em C:\Users\Tom\Documents\ToDo.txt. Neste exemplo, ToDo.txt é o nome do arquivo, e Users, Tom e Documents são nomes de pastas. `C:` no Windows representa o diretório raiz da unidade.

Maneira tradicional de trabalhar com arquivos na Web

Para editar a lista de tarefas em um aplicativo da Web, o fluxo normal é este:

  1. O usuário faz upload do arquivo para um servidor ou o abre no cliente com <input type="file">.
  2. O usuário faz as mudanças e baixa o arquivo resultante com um <a download="ToDo.txt> injetado que você click() de forma programática via JavaScript.
  3. Para abrir pastas, use um atributo especial em <input type="file" webkitdirectory>, que, apesar do nome proprietário, tem suporte de navegador praticamente universal.

Forma moderna de trabalhar com arquivos na Web

Esse fluxo não representa como os usuários pensam em editar arquivos e significa que eles acabam com cópias baixadas dos arquivos de entrada. Portanto, a API File System Access introduziu três métodos de seleção: showOpenFilePicker(), showSaveFilePicker() e showDirectoryPicker(), que fazem exatamente o que o nome sugere. Elas permitem um fluxo da seguinte maneira:

  1. Abra ToDo.txt com showOpenFilePicker() e receba um objeto FileSystemFileHandle.
  2. Do objeto FileSystemFileHandle, receba um File chamando o método getFile() do identificador de arquivo.
  3. Modifique o arquivo e chame requestPermission({mode: 'readwrite'}) no identificador.
  4. Se o usuário aceitar a solicitação de permissão, salve as mudanças no arquivo original.
  5. Se preferir, chame showSaveFilePicker() e deixe o usuário escolher um novo arquivo. Se o usuário escolher um arquivo aberto anteriormente, o conteúdo dele será substituído. Para salvamentos repetidos, você pode manter o identificador de arquivo por perto para não precisar mostrar a caixa de diálogo de salvamento de arquivo novamente.

Restrições ao trabalhar com arquivos na Web

Os arquivos e pastas acessíveis por esses métodos ficam no que pode ser chamado de sistema de arquivos visível para o usuário. Os arquivos salvos da Web, principalmente os executáveis, são marcados com a marca da Web. Assim, o sistema operacional pode mostrar um aviso adicional antes que um arquivo potencialmente perigoso seja executado. Como um recurso de segurança adicional, os arquivos obtidos da Web também são protegidos pela Navegação segura, que, para simplificar e no contexto deste artigo, pode ser considerada uma verificação de vírus baseada na nuvem. Quando você grava dados em um arquivo usando a API File System Access, as gravações não são feitas no local, mas usam um arquivo temporário. O arquivo só é modificado se passar em todas essas verificações de segurança. Como você pode imaginar, esse trabalho torna as operações de arquivo relativamente lentas, apesar das melhorias aplicadas sempre que possível, por exemplo, no macOS. No entanto, cada chamada de write() é independente. Portanto, por baixo dos panos, ela abre o arquivo, busca o deslocamento especificado e, por fim, grava os dados.

Arquivos como base do processamento

Ao mesmo tempo, os arquivos são uma excelente maneira de registrar dados. Por exemplo, o SQLite armazena bancos de dados inteiros em um único arquivo. Outro exemplo são os mipmaps usados no processamento de imagens. Os mipmaps são sequências de imagens pré-calculadas e otimizadas, cada uma com uma representação de resolução progressivamente menor da anterior, o que acelera muitas operações, como o zoom. Então, como os aplicativos da Web podem aproveitar os benefícios dos arquivos sem os custos de performance do processamento de arquivos baseado na Web? A resposta é o sistema de arquivos privado de origem.

O sistema de arquivos particulares visível para o usuário x o sistema de arquivos particulares de origem

Ao contrário do sistema de arquivos visível para o usuário, que é navegado usando o explorador de arquivos do sistema operacional e permite ler, gravar, mover e renomear arquivos e pastas, o sistema de arquivos privado de origem não foi criado para ser visto pelos usuários. Como o nome sugere, os arquivos e pastas no sistema de arquivos privado de origem são particulares, mais especificamente, particulares à origem de um site. Descubra a origem de uma página digitando location.origin no console do DevTools. Por exemplo, a origem da página https://developer.chrome.com/articles/ é https://developer.chrome.com. Ou seja, a parte /articles não faz parte da origem. Saiba mais sobre a teoria das origens em Entender "mesmo site" e "mesma origem". Todas as páginas que compartilham a mesma origem podem ver os mesmos dados do sistema de arquivos privado de origem. Portanto, https://developer.chrome.com/docs/extensions/mv3/getstarted/extensions-101/ pode ver os mesmos detalhes do exemplo anterior. Cada origem tem seu próprio sistema de arquivos privado independente. Isso significa que o sistema de arquivos privado de origem de https://developer.chrome.com é completamente diferente do de, por exemplo, https://web.dev. No Windows, o diretório raiz do sistema de arquivos visível para o usuário é C:\\. O equivalente para o sistema de arquivos privado de origem é um diretório raiz inicialmente vazio por origem acessado chamando o método assíncrono navigator.storage.getDirectory(). Para uma comparação entre o sistema de arquivos visível para o usuário e o sistema de arquivos privado de origem, consulte o diagrama a seguir. O diagrama mostra que, além do diretório raiz, todo o resto é conceitualmente igual, com uma hierarquia de arquivos e pastas para organizar e dispor conforme necessário para seus dados e necessidades de armazenamento.

Diagrama do sistema de arquivos visível para o usuário e do sistema de arquivos privado de origem com duas hierarquias de arquivos exemplares. O ponto de entrada para o sistema de arquivos visível ao usuário é um disco rígido simbólico. Já o ponto de entrada para o sistema de arquivos particular da origem é a chamada do método &quot;navigator.storage.getDirectory&quot;.

Detalhes do sistema de arquivos privados de origem

Assim como outros mecanismos de armazenamento no navegador (por exemplo, localStorage ou IndexedDB), o sistema de arquivos privado de origem está sujeito a restrições de cota do navegador. Quando um usuário limpa todos os dados de navegação ou todos os dados do site, o sistema de arquivos privado de origem também é excluído. Chame navigator.storage.estimate() e, no objeto de resposta resultante, confira a entrada usage para saber quanto armazenamento seu app já consome, que é dividido por mecanismo de armazenamento no objeto usageDetails. Nele, você precisa analisar especificamente a entrada fileSystem. Como o sistema de arquivos privado de origem não fica visível para o usuário, não há solicitações de permissão nem verificações da Navegação segura.

Como acessar o diretório raiz

Para acessar o diretório raiz, execute o seguinte comando. Você acaba com um identificador de diretório vazio, mais especificamente, um FileSystemDirectoryHandle.

const opfsRoot = await navigator.storage.getDirectory();
// A FileSystemDirectoryHandle whose type is "directory"
// and whose name is "".
console.log(opfsRoot);

Linha de execução principal ou Web Worker

Há duas maneiras de usar o sistema de arquivos privado de origem: na linha de execução principal ou em um Web Worker. Os Web Workers não podem bloquear a linha de execução principal, o que significa que, nesse contexto, as APIs podem ser síncronas, um padrão geralmente proibido na linha de execução principal. As APIs síncronas podem ser mais rápidas porque evitam ter que lidar com promessas, e as operações de arquivo geralmente são síncronas em linguagens como C, que podem ser compiladas para WebAssembly.

// This is synchronous C code.
FILE *f;
f = fopen("example.txt", "w+");
fputs("Some text\n", f);
fclose(f);

Se você precisar das operações de arquivo mais rápidas possíveis ou trabalhar com WebAssembly, pule para Usar o sistema de arquivos privado de origem em um Web Worker. Caso contrário, continue lendo.

Usar o sistema de arquivos privado de origem na linha de execução principal

Criar arquivos e pastas

Depois de ter uma pasta raiz, crie arquivos e pastas usando os métodos getFileHandle() e getDirectoryHandle(), respectivamente. Ao transmitir {create: true}, o arquivo ou a pasta será criado se não existir. Crie uma hierarquia de arquivos chamando essas funções usando um diretório recém-criado como ponto de partida.

const fileHandle = await opfsRoot
    .getFileHandle('my first file', {create: true});
const directoryHandle = await opfsRoot
    .getDirectoryHandle('my first folder', {create: true});
const nestedFileHandle = await directoryHandle
    .getFileHandle('my first nested file', {create: true});
const nestedDirectoryHandle = await directoryHandle
    .getDirectoryHandle('my first nested folder', {create: true});

A hierarquia de arquivos resultante do exemplo de código anterior.

Acessar arquivos e pastas

Se você souber o nome, acesse arquivos e pastas criados anteriormente chamando os métodos getFileHandle() ou getDirectoryHandle() e transmitindo o nome do arquivo ou da pasta.

const existingFileHandle = await opfsRoot.getFileHandle('my first file');
const existingDirectoryHandle = await opfsRoot
    .getDirectoryHandle('my first folder');

Como receber o arquivo associado a um identificador de arquivo para leitura

Um FileSystemFileHandle representa um arquivo no sistema de arquivos. Para receber o File associado, use o método getFile(). Um objeto File é um tipo específico de Blob e pode ser usado em qualquer contexto em que um Blob pode ser usado. Em particular, FileReader, URL.createObjectURL(), createImageBitmap() e XMLHttpRequest.send() aceitam Blobs e Files. Se você quiser, obter um File de um FileSystemFileHandle "libera" os dados, para que você possa acessá-los e disponibilizá-los ao sistema de arquivos visível para o usuário.

const file = await fileHandle.getFile();
console.log(await file.text());

Gravar em um arquivo por streaming

Transmita dados para um arquivo chamando createWritable(), que cria um FileSystemWritableFileStream para que você write() o conteúdo. No final, é necessário close() o stream.

const contents = 'Some text';
// Get a writable stream.
const writable = await fileHandle.createWritable();
// Write the contents of the file to the stream.
await writable.write(contents);
// Close the stream, which persists the contents.
await writable.close();

Excluir arquivos e pastas

Exclua arquivos e pastas chamando o método remove() específico do arquivo ou do identificador de diretório. Para excluir uma pasta, incluindo todas as subpastas, transmita a opção {recursive: true}.

await fileHandle.remove();
await directoryHandle.remove({recursive: true});

Como alternativa, se você souber o nome do arquivo ou da pasta a ser excluída em um diretório, use o método removeEntry().

directoryHandle.removeEntry('my first nested file');

Mover e renomear arquivos e pastas

Renomeie e mova arquivos e pastas usando o método move(). É possível mover e renomear ao mesmo tempo ou separadamente.

// Rename a file.
await fileHandle.move('my first renamed file');
// Move a file to another directory.
await fileHandle.move(nestedDirectoryHandle);
// Move a file to another directory and rename it.
await fileHandle
    .move(nestedDirectoryHandle, 'my first renamed and now nested file');

Resolver o caminho de um arquivo ou pasta

Para saber onde um determinado arquivo ou pasta está localizado em relação a um diretório de referência, use o método resolve(), transmitindo um FileSystemHandle como argumento. Para conseguir o caminho completo de um arquivo ou pasta no sistema de arquivos particular da origem, use o diretório raiz como o diretório de referência obtido via navigator.storage.getDirectory().

const relativePath = await opfsRoot.resolve(nestedDirectoryHandle);
// `relativePath` is `['my first folder', 'my first nested folder']`.

Verifica se dois identificadores de arquivo ou pasta apontam para o mesmo arquivo ou pasta.

Às vezes, você tem dois identificadores e não sabe se eles apontam para o mesmo arquivo ou pasta. Para verificar se esse é o caso, use o método isSameEntry().

fileHandle.isSameEntry(nestedFileHandle);
// Returns `false`.

Listar o conteúdo de uma pasta

FileSystemDirectoryHandle é um iterador assíncrono que você itera com um loop for await…of. Como um iterador assíncrono, ele também é compatível com os métodos entries(), values() e keys(), que podem ser escolhidos dependendo das informações necessárias:

for await (let [name, handle] of directoryHandle) {}
for await (let [name, handle] of directoryHandle.entries()) {}
for await (let handle of directoryHandle.values()) {}
for await (let name of directoryHandle.keys()) {}

Listar recursivamente o conteúdo de uma pasta e de todas as subpastas

É fácil errar ao lidar com loops e funções assíncronos combinados com recursão. A função abaixo pode servir como ponto de partida para listar o conteúdo de uma pasta e todas as subpastas dela, incluindo todos os arquivos e tamanhos. Você pode simplificar a função se não precisar dos tamanhos dos arquivos. Para isso, onde diz directoryEntryPromises.push, não envie a promessa handle.getFile(), mas o handle diretamente.

  const getDirectoryEntriesRecursive = async (
    directoryHandle,
    relativePath = '.',
  ) => {
    const fileHandles = [];
    const directoryHandles = [];
    const entries = {};
    // Get an iterator of the files and folders in the directory.
    const directoryIterator = directoryHandle.values();
    const directoryEntryPromises = [];
    for await (const handle of directoryIterator) {
      const nestedPath = `${relativePath}/${handle.name}`;
      if (handle.kind === 'file') {
        fileHandles.push({ handle, nestedPath });
        directoryEntryPromises.push(
          handle.getFile().then((file) => {
            return {
              name: handle.name,
              kind: handle.kind,
              size: file.size,
              type: file.type,
              lastModified: file.lastModified,
              relativePath: nestedPath,
              handle
            };
          }),
        );
      } else if (handle.kind === 'directory') {
        directoryHandles.push({ handle, nestedPath });
        directoryEntryPromises.push(
          (async () => {
            return {
              name: handle.name,
              kind: handle.kind,
              relativePath: nestedPath,
              entries:
                  await getDirectoryEntriesRecursive(handle, nestedPath),
              handle,
            };
          })(),
        );
      }
    }
    const directoryEntries = await Promise.all(directoryEntryPromises);
    directoryEntries.forEach((directoryEntry) => {
      entries[directoryEntry.name] = directoryEntry;
    });
    return entries;
  };

Usar o sistema de arquivos particular de origem em um Web Worker

Como descrito antes, os Web Workers não podem bloquear a linha de execução principal. Por isso, nesse contexto, os métodos síncronos são permitidos.

Como receber um identificador de acesso síncrono

O ponto de entrada para as operações de arquivo mais rápidas possíveis é um FileSystemSyncAccessHandle, obtido de um FileSystemFileHandle regular chamando createSyncAccessHandle().

const fileHandle = await opfsRoot
    .getFileHandle('my highspeed file.txt', {create: true});
const syncAccessHandle = await fileHandle.createSyncAccessHandle();

Métodos síncronos de arquivo in-loco

Depois de ter um identificador de acesso síncrono, você terá acesso a métodos de arquivo rápidos e in-loco que são todos síncronos.

  • getSize(): retorna o tamanho do arquivo em bytes.
  • write(): grava o conteúdo de um buffer no arquivo, opcionalmente em um determinado deslocamento, e retorna o número de bytes gravados. A verificação do número retornado de bytes gravados permite que os chamadores detectem e processem erros e gravações parciais.
  • read(): lê o conteúdo do arquivo em um buffer, opcionalmente em um determinado deslocamento.
  • truncate(): redimensiona o arquivo para o tamanho especificado.
  • flush(): garante que o conteúdo do arquivo tenha todas as modificações feitas com write().
  • close(): fecha o identificador de acesso.

Confira um exemplo que usa todos os métodos mencionados acima.

const opfsRoot = await navigator.storage.getDirectory();
const fileHandle = await opfsRoot.getFileHandle('fast', {create: true});
const accessHandle = await fileHandle.createSyncAccessHandle();

const textEncoder = new TextEncoder();
const textDecoder = new TextDecoder();

// Initialize this variable for the size of the file.
let size;
// The current size of the file, initially `0`.
size = accessHandle.getSize();
// Encode content to write to the file.
const content = textEncoder.encode('Some text');
// Write the content at the beginning of the file.
accessHandle.write(content, {at: size});
// Flush the changes.
accessHandle.flush();
// The current size of the file, now `9` (the length of "Some text").
size = accessHandle.getSize();

// Encode more content to write to the file.
const moreContent = textEncoder.encode('More content');
// Write the content at the end of the file.
accessHandle.write(moreContent, {at: size});
// Flush the changes.
accessHandle.flush();
// The current size of the file, now `21` (the length of
// "Some textMore content").
size = accessHandle.getSize();

// Prepare a data view of the length of the file.
const dataView = new DataView(new ArrayBuffer(size));

// Read the entire file into the data view.
accessHandle.read(dataView);
// Logs `"Some textMore content"`.
console.log(textDecoder.decode(dataView));

// Read starting at offset 9 into the data view.
accessHandle.read(dataView, {at: 9});
// Logs `"More content"`.
console.log(textDecoder.decode(dataView));

// Truncate the file after 4 bytes.
accessHandle.truncate(4);

Copiar um arquivo do sistema de arquivos privado de origem para o sistema de arquivos visível para o usuário

Como mencionado acima, não é possível mover arquivos do sistema de arquivos privado de origem para o sistema de arquivos visível para o usuário, mas é possível copiar arquivos. Como showSaveFilePicker() só é exposto na linha de execução principal, mas não na linha de execução do worker, execute o código lá.

// On the main thread, not in the Worker. This assumes
// `fileHandle` is the `FileSystemFileHandle` you obtained
// the `FileSystemSyncAccessHandle` from in the Worker
// thread. Be sure to close the file in the Worker thread first.
const fileHandle = await opfsRoot.getFileHandle('fast');
try {
  // Obtain a file handle to a new file in the user-visible file system
  // with the same name as the file in the origin private file system.
  const saveHandle = await showSaveFilePicker({
    suggestedName: fileHandle.name || ''
  });
  const writable = await saveHandle.createWritable();
  await writable.write(await fileHandle.getFile());
  await writable.close();
} catch (err) {
  console.error(err.name, err.message);
}

Depurar o sistema de arquivos privado de origem

Até que o suporte integrado do DevTools seja adicionado (consulte crbug/1284595), use a extensão do Chrome OPFS Explorer para depurar o sistema de arquivos privado de origem. A captura de tela acima da seção Criar novos arquivos e pastas foi feita diretamente da extensão.

A extensão OPFS Explorer do Chrome DevTools na Chrome Web Store.

Depois de instalar a extensão, abra o Chrome DevTools, selecione a guia OPFS Explorer e inspecione a hierarquia de arquivos. Para salvar arquivos do sistema de arquivos privado de origem no sistema de arquivos visível para o usuário, clique no nome do arquivo. Para excluir arquivos e pastas, clique no ícone da lixeira.

Demonstração

Confira o sistema de arquivos privado de origem em ação (se você instalar a extensão OPFS Explorer) em uma demonstração que o usa como back-end para um banco de dados SQLite compilado para WebAssembly. Confira o código-fonte no Glitch. Observe como a versão incorporada abaixo não usa o back-end do sistema de arquivos privado de origem (porque o iframe é de origem cruzada), mas quando você abre a demonstração em uma guia separada, ela usa.

Conclusões

O sistema de arquivos privado de origem, conforme especificado pelo WHATWG, moldou a maneira como usamos e interagimos com arquivos na Web. Ele permitiu novos casos de uso que eram impossíveis de alcançar com o sistema de arquivos visível para o usuário. Todos os principais fornecedores de navegadores (Apple, Mozilla e Google) estão a bordo e compartilham uma visão conjunta. O desenvolvimento do sistema de arquivos particulares de origem é um esforço colaborativo, e o feedback de desenvolvedores e usuários é essencial para o progresso. À medida que continuamos a refinar e melhorar o padrão, o feedback no repositório whatwg/fs (link em inglês) na forma de problemas ou solicitações de pull é bem-vindo.

Agradecimentos

Este artigo foi revisado por Austin Sully, Etienne Noël e Rachel Andrew. Imagem principal de Christina Rumpf no Unsplash.