O sistema de arquivos privados de origem

O padrão do sistema de arquivos apresenta um sistema de arquivos particular de origem (OPFS, na sigla em inglês) como um endpoint de armazenamento particular à 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 particular de origem é compatível com navegadores modernos e é padronizado pelo Web Hypertext Application Technology Working Group (whatWG) no File System Living Standard.

Compatibilidade com navegadores

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

Origem

Motivação

Quando você pensa em arquivos no seu computador, provavelmente pensa em uma hierarquia de arquivos, ou seja, arquivos organizados em pastas que você pode explorar com o explorador de arquivos do seu sistema operacional. Por exemplo, no Windows, para um usuário chamado Tom, a lista de tarefas dele 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 pasta. No Windows, "C:" representa o diretório raiz da unidade.

Forma tradicional de trabalhar com arquivos na Web

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

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

Maneira moderna de trabalhar com arquivos na Web

Esse fluxo não representa a forma como os usuários pensam sobre a edição de arquivos, e eles acabam fazendo o download de cópias dos arquivos de entrada. Por isso, a API File System Access lançou três métodos de seletor, showOpenFilePicker(), showSaveFilePicker() e showDirectoryPicker(), que fazem exatamente o que o nome sugere. Eles permitem um fluxo da seguinte forma:

  1. Abra ToDo.txt com showOpenFilePicker() e receba um objeto FileSystemFileHandle.
  2. No objeto FileSystemFileHandle, receba um File chamando o método getFile() do gerenciador de arquivos.
  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 alterações no arquivo original.
  5. Como alternativa, chame showSaveFilePicker() e permita que o usuário escolha um novo arquivo. (Se o usuário selecionar um arquivo aberto anteriormente, seu conteúdo será substituído.) Para salvar novamente, é possível manter o identificador do arquivo.

Restrições de trabalho com arquivos na Web

Os arquivos e as pastas que podem ser acessados por meio desses métodos ficam no que pode ser chamado de sistema de arquivos visível pelo usuário. Arquivos salvos da Web e os executáveis especificamente, são marcados com a marca da Web, então há um aviso adicional que o sistema operacional pode mostrar antes que um arquivo potencialmente perigoso seja executado. Como um recurso de segurança adicional, os arquivos baixados da Web também são protegidos pelo recurso Navegação segura, que, por uma questão de simplicidade e no contexto deste artigo, é considerado 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 em si não é modificado, a menos que ele passe por todas essas verificações de segurança. Como você pode imaginar, esse trabalho torna as operações de arquivos relativamente lentas, apesar das melhorias aplicadas sempre que possível, por exemplo, no macOS. Ainda assim, cada chamada write() é independente. Portanto, internamente, ela abre o arquivo, procura o deslocamento especificado e, por fim, grava dados.

Arquivos como a base do processamento

Ao mesmo tempo, os arquivos são uma excelente forma de gravar 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 delas é uma representação com resolução progressivamente menor da anterior, o que agiliza muitas operações, como aumentar o zoom. Como os aplicativos da Web podem obter os benefícios dos arquivos, mas sem os custos de desempenho do processamento de arquivos baseado na Web? A resposta é o sistema de arquivos particular de origem.

Sistema de arquivos visível para o usuário vs. sistema de arquivos particular de origem

Ao contrário do sistema de arquivos visível ao usuário navegado usando o explorador de arquivos do sistema operacional, com arquivos e pastas que você pode ler, gravar, mover e renomear, o sistema de arquivos particular de origem não é feito para ser visto pelos usuários. Os arquivos e as pastas no sistema de arquivos particular de origem, como o nome sugere, são particulares e, de maneira mais concreta, particulares em relação à 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. Leia mais sobre a teoria da origem em Noções básicas sobre "mesmo site" e "same-origin". Todas as páginas que compartilham a mesma origem podem ver os mesmos dados particulares do sistema de arquivos de origem, então https://developer.chrome.com/docs/extensions/mv3/getstarted/extensions-101/ pode ver os mesmos detalhes do exemplo anterior. Cada origem tem o próprio sistema de arquivos particular de origem independente, o que significa que o sistema de arquivos particular de origem do https://developer.chrome.com é completamente diferente de, por exemplo, https://web.dev. No Windows, o diretório raiz do sistema de arquivos visível ao usuário é C:\\. O equivalente para o sistema de arquivos particular de origem é um diretório raiz inicialmente vazio por origem, acessado chamando o método assíncrono navigator.storage.getDirectory() Para uma comparação do sistema de arquivos visível ao usuário e do sistema de arquivos particular de origem, consulte o diagrama a seguir. O diagrama mostra que, além do diretório raiz, todo o restante é conceitualmente o mesmo, com uma hierarquia de arquivos e pastas para organizar e organizar conforme necessário para suas necessidades de dados e armazenamento.

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

Detalhes do sistema de arquivos particular de origem

Assim como outros mecanismos de armazenamento no navegador (por exemplo, localStorage ou IndexedDB), o sistema de arquivos particular 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 particular de origem também é excluído. Chame navigator.storage.estimate() e, no objeto de resposta resultante, consulte a entrada usage para saber a quantidade de armazenamento que seu app já consome. Essa informação é dividida pelo mecanismo de armazenamento no objeto usageDetails, em que você quer analisar especificamente a entrada fileSystem. Como o sistema de arquivos particular de origem não é visível para o usuário, não há solicitações de permissão nem verificações do Navegação segura.

Como acessar o diretório raiz

Para ter acesso ao diretório raiz, execute o comando a seguir. Você vai ter 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 particular 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. Isso significa que, nesse contexto, as APIs podem ser síncronas, um padrão geralmente não permitido na linha de execução principal. As APIs síncronas podem ser mais rápidas porque evitam a necessidade de lidar com promessas. As operações de arquivos normalmente são síncronas em linguagens como C, que podem ser compiladas no 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 arquivos mais rápidas possíveis ou precisar trabalhar com o WebAssembly, pule para a seção Usar o sistema de arquivos particular de origem em um web worker. Caso contrário, você pode continuar lendo.

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

Criar novos arquivos e pastas

Depois de criar 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ão criados se não existirem. Crie uma hierarquia de arquivos chamando essas funções com 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 deles, 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 conseguir o arquivo associado a um gerenciador de arquivos para leitura

Um FileSystemFileHandle representa um arquivo no sistema de arquivos. Para acessar 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 uma Blob puder. Particularmente, FileReader, URL.createObjectURL(), createImageBitmap() e XMLHttpRequest.send() aceitam Blobs e Files. Se quiser, receber um File de um FileSystemFileHandle "sem custo financeiro" os dados, para que você possa acessá-los e disponibilizá-los para o sistema de arquivos visível ao usuário.

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

Gravar em um arquivo por streaming

Faça streaming de dados para um arquivo chamando createWritable(), que cria uma FileSystemWritableFileStream e depois write() para o conteúdo. No final, você precisa close() a transmissão.

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 gerenciador de arquivos ou diretórios. Para excluir uma pasta com 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 que 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(). A mudança e a renomeação podem acontecer juntas ou isoladamente.

// 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 conferir o caminho completo de um arquivo ou pasta no sistema de arquivos particular de origem, use o diretório raiz como o diretório de referência acessado por navigator.storage.getDirectory().

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

Verificar se dois identificadores de arquivos ou pastas 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 uma repetição for await…of. Como um iterador assíncrono, ele também oferece suporte aos métodos entries(), values() e keys(), que podem ser escolhidos de acordo com as 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 todas as subpastas

É fácil lidar com loops assíncronos e funções pareados com a recursão. A função abaixo pode servir como ponto de partida para listar o conteúdo de uma pasta e todas as subpastas, incluindo todos os arquivos e seus tamanhos. Você pode simplificar a função se não precisar dos tamanhos de arquivo, em que está escrito directoryEntryPromises.push, sem enviar 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

Conforme descrito anteriormente, 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 gerar um identificador de acesso síncrono

O ponto de entrada para as operações de arquivo mais rápidas é um FileSystemSyncAccessHandle, extraído de um FileSystemFileHandle normal chamando createSyncAccessHandle().

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

Métodos de arquivo síncronos no local

Com um identificador de acesso síncrono, você tem acesso a métodos de arquivo rápidos no local 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. Verificar o número retornado de bytes gravados permite que os autores da chamada 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 contenha todas as modificações feitas pelo write().
  • close(): fecha o identificador de acesso.

Aqui está 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 particular de origem para o sistema de arquivos visível ao usuário

Como mencionado acima, não é possível mover arquivos do sistema de arquivos particular de origem para o sistema de arquivos visível ao usuário, mas você pode copiá-los. Como showSaveFilePicker() só é exposto na linha de execução principal, mas não na linha de execução de worker, execute o código nela.

// 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 particular de origem

Até que o suporte integrado ao DevTools seja adicionado (consulte crbug/1284595), use a extensão do Chrome OPFS Explorer para depurar o sistema de arquivos particular de origem. A captura de tela acima da seção Como criar novos arquivos e pastas foi tirada 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 para inspecionar a hierarquia de arquivos. Para salvar arquivos do sistema de arquivos particular de origem no sistema de arquivos visível para o usuário, clique no nome do arquivo e exclua arquivos e pastas clicando no ícone de lixeira.

Demonstração

Veja o sistema de arquivos particular de origem em ação (se você instalar a extensão OPFS Explorer) em uma demonstração que o usa como back-end de 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 particular de origem (porque o iframe tem origem cruzada), mas quando você abre a demonstração em uma guia separada, ele usa.

Conclusões

O sistema de arquivos particular de origem, conforme especificado pelo WhatWG, moldou a maneira como usamos e interagimos com arquivos na Web. Ele possibilitou novos casos de uso que eram impossíveis de atingir com o sistema de arquivos visível para o usuário. Todos os principais fornecedores de navegadores, Apple, Mozilla e Google, estão de acordo e compartilham uma visão conjunta. O desenvolvimento do sistema de arquivos particular de origem é um esforço colaborativo, e o feedback de desenvolvedores e usuários é essencial para o progresso dele. À medida que continuamos refinando e melhorando o padrão, queremos receber feedback sobre o repositório whatwg/fs na forma de problemas ou solicitações de envio.

Agradecimentos

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