O sistema de arquivos privados de origem

O padrão de 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 particular de origem é aceito por navegadores modernos e padronizado pelo Grupo de Trabalho de Tecnologia de Aplicativos de Hipertexto da Web (WHATWG, em inglês) no File System Living Standard.

Compatibilidade com navegadores

  • Chrome: 86.
  • Edge: 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, a lista de tarefas de um usuário chamado Tom 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.

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 o upload do arquivo para um servidor ou o 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ê click() por programação usando JavaScript.
  3. Para abrir pastas, use um atributo especial em <input type="file" webkitdirectory>, que, apesar do nome reservado, tem suporte praticamente universal para navegadores.

Uma 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 ativam um fluxo da seguinte maneira:

  1. Abra ToDo.txt com showOpenFilePicker() e receba um objeto FileSystemFileHandle.
  2. No 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 alterações no arquivo original.
  5. Como alternativa, 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 do arquivo para não precisar mostrar a caixa de diálogo de salvamento de arquivos novamente.

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. Os arquivos salvos da Web e os executáveis, especificamente, 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 baixados da Web também são protegidos pelo recurso Navegação segura, que, por questões 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 no local, mas usam um arquivo temporário. O arquivo 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 arquivo relativamente lentas, apesar das melhorias aplicadas sempre que possível, por exemplo, no macOS. No entanto, cada chamada write() é independente, então, em segundo plano, ela abre o arquivo, procura o deslocamento especificado e, por fim, grava os dados.

Arquivos como 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 otimizadas e pré-calculadas, cada uma delas é uma representação de resolução progressivamente mais baixa da anterior, o que torna muitas operações, como o zoom, mais rápidas. Como os aplicativos da Web podem aproveitar 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.

O sistema de arquivos particular da origem em comparação com o visível para o usuário

Ao contrário do sistema de arquivos visível para o usuário, que é acessado usando o explorador de arquivos do sistema operacional, com arquivos e pastas que podem ser lidos, gravados, movidos e renomeados, o sistema de arquivos particular de origem não é destinado a ser visto pelos usuários. Os arquivos e as pastas no sistema de arquivos particular da origem, como o nome sugere, são particulares, e mais especificamente, particulares para a 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 das origens em Como entender "mesmo site" e "mesma origem". Todas as páginas que compartilham a mesma origem podem ver os mesmos dados do sistema de arquivos particular 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 seu próprio sistema de arquivos particular independente, o que significa que o sistema de arquivos particular de https://developer.chrome.com é completamente diferente do de https://web.dev, por exemplo. No Windows, o diretório raiz do sistema de arquivos visível para o usuário é C:\\. O equivalente para o sistema de arquivos particular da origem é um diretório raiz inicialmente vazio por origem acessado chamando o método assíncrono navigator.storage.getDirectory(). Para comparar o sistema de arquivos visível para o usuário e o sistema de arquivos particular de origem, consulte o diagrama a seguir. O diagrama mostra que, além do diretório raiz, tudo o mais é conceitualmente igual, com uma hierarquia de arquivos e pastas para organizar e organizar 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 particular de origem com duas hierarquias de arquivos de exemplo. O ponto de entrada do sistema de arquivos visível para o 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;.

Especificações 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, confira a entrada usage para saber quanto armazenamento seu app já consome, que é dividido por mecanismo de armazenamento no objeto usageDetails, onde você quer conferir a entrada fileSystem especificamente. Como o sistema de arquivos particular de origem não está 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 ter acesso ao diretório raiz, execute o seguinte comando. 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 workers da Web 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 não permitido na linha de execução principal. As APIs síncronas podem ser mais rápidas, porque evitam 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 lidar com o WebAssembly, pule para Usar o sistema de arquivos particular de origem em um Web Worker. Caso contrário, continue 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á 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 os arquivos e pastas criados anteriormente chamando os métodos getFileHandle() ou getDirectoryHandle(), 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 acessar 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 que um Blob possa. Em particular, FileReader, URL.createObjectURL(), createImageBitmap() e XMLHttpRequest.send() aceitam Blobs e Files. Se quiser, receber um File de um FileSystemFileHandle "libere" os dados para que eles possam ser acessados e disponibilizados 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 para você e depois write() para o conteúdo. No final, você precisa 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 identificador de arquivo ou 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 que será excluído 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 movimentação e a renomeação podem acontecer juntas 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 acessar 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 recebido por navigator.storage.getDirectory().

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

Verificar se dois identificadores de arquivo ou pasta apontam para o mesmo arquivo ou pasta

Às vezes, você tem duas alças e não sabe se elas 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 oferece suporte aos métodos entries(), values() e keys(), entre os quais você pode escolher 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 todas as subpastas

É fácil errar ao lidar com loops e funções assíncronas combinadas 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 os tamanhos deles. Você pode simplificar a função se não precisar dos tamanhos de arquivo, onde está escrito directoryEntryPromises.push, não empurrando 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 worker da Web

Como descrito anteriormente, os Web Workers não podem bloquear a linha de execução principal. É por isso que, nesse contexto, 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 possíveis é um FileSystemSyncAccessHandle, obtido 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 in-place síncronos

Depois de ter um identificador de acesso síncrono, você terá 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. A verificação do 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 por 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 particular 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 particular de origem para o sistema de arquivos visível para o usuário, mas você pode copiar arquivos. Como showSaveFilePicker() só é exposto na linha de execução principal, mas não na linha de execução de 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);
}

Fazer a depuração do 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 Criar novos arquivos e pastas foi tirada diretamente da extensão.

A extensão OPFS Explorer para o 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

Confira 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 um 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 particular de origem (porque o iframe é de origem cruzada), mas quando você abre a demonstração em uma guia separada, ele é usado.

Conclusões

O sistema de arquivos particular de origem, conforme especificado pelo WHATWG, moldou a maneira como usamos e interagimos com os 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, como Apple, Mozilla e Google, estão envolvidos 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. À medida que continuamos a refinar e melhorar o padrão, aceitamos feedback sobre o repositório whatwg/fs na forma de problemas ou solicitações de pull.

Agradecimentos

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