O sistema de arquivos privados de origem

O padrão do sistema de arquivos introduz um sistema de arquivos 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 fornece acesso opcional a um tipo especial de arquivo altamente otimizado para desempenho.

Suporte ao navegador

O sistema de arquivos privados de origem é compatível com navegadores mais recentes e é padronizado pelo grupo de trabalho de tecnologia de aplicativos de hipertexto da Web (WhatWG, na sigla em inglês) do File System Living Standard (link em inglês).

Compatibilidade com navegadores

  • 86
  • 86
  • 111
  • 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 podem ser exploradas com o explorador de arquivos do seu sistema operacional. Por exemplo, no Windows, para um usuário chamado Tom, a lista de tarefas pode ficar em C:\Users\Tom\Documents\ToDo.txt. Nesse exemplo, ToDo.txt é o nome do arquivo, e Users, Tom e Documents são os nomes das 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, siga este fluxo normal:

  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, em seguida, faz o download do arquivo resultante com um <a download="ToDo.txt> injetado que você click() de maneira programática via JavaScript.
  3. Para abrir pastas, use um atributo especial em <input type="file" webkitdirectory> que, apesar do nome reservado, é praticamente universal.

Modo moderno de trabalhar com arquivos na Web

Esse fluxo não representa o que os usuários pensam sobre a edição de arquivos, e isso significa que eles vão ter cópias transferidas por download dos arquivos de entrada. A API File System Access introduziu 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, acesse 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 escolher um arquivo aberto anteriormente, o conteúdo dele será substituído. Para salvamentos repetidos, você pode manter a alça de arquivo ao redor, de modo que não precise mostrar a caixa de diálogo para salvar arquivos novamente.

Restrições ao trabalhar com arquivos na Web

Os arquivos e as pastas que podem ser acessados por esses métodos residem no que pode ser chamado de sistema de arquivos visível para o usuário. Os arquivos salvos da Web, e os executáveis, especificamente, são marcados com a marca da Web. Por isso, 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 obtidos na 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 estão no local, mas usam um arquivo temporário. O arquivo em si não é modificado, a menos que passe em todas essas verificações de segurança. Como você pode imaginar, esse trabalho deixa 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 os 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 sendo uma representação progressivamente menor da anterior, o que torna muitas operações, como aumentar o zoom. 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 privados de origem.

Comparação entre as opções visíveis ao usuário e o sistema de arquivos particular de origem

Ao contrário do sistema de arquivos visível ao usuário que é navegado pelo 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 foi projetado para ser visto pelos usuários. Os arquivos e as pastas no sistema de arquivos de origem, como o nome sugere, são particulares e, de forma mais concreta, têm relação com a origin 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 Noções básicas sobre "mesmo site" e "mesma origem". Todas as páginas que compartilham a mesma origem podem ver os mesmos dados do sistema de arquivos privados de origem, de modo que o https://developer.chrome.com/docs/extensions/mv3/getstarted/extensions-101/ possa ver os mesmos detalhes do exemplo anterior. Cada origem tem o próprio sistema de arquivos privados de origem independente, o que significa que o sistema de arquivos privados de origem de https://developer.chrome.com é completamente diferente, 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 privados de origem é um diretório raiz inicialmente vazio por origem acessada 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, exceto o diretório raiz, todo o restante é conceitualmente o mesmo, com uma hierarquia de arquivos e pastas para organizar e organizar conforme necessário para seus dados e 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 exemplares. O ponto de entrada do sistema de arquivos visível para o usuário é um disco rígido simbólico. O ponto de entrada para o sistema de arquivos particular de origem é chamando o método &#39;navigator.storage.getDirectory&#39;.

Especificações 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 privados de origem também é excluído. Chame navigator.storage.estimate() e, no objeto de resposta resultante, consulte a entrada usage para conferir a quantidade de armazenamento que seu app já consome, dividida pelo mecanismo de armazenamento no objeto usageDetails, em que você quer analisar a entrada fileSystem especificamente. Como o sistema de arquivos privados 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 ter acesso ao diretório raiz

Para acessar o diretório raiz, execute o comando a seguir. Você vai acabar 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 privados 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 não permitido no thread principal. As APIs síncronas podem ser mais rápidas, já que não precisam lidar com promessas. Além disso, as operações de arquivos costumam ser 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 arquivo mais rápidas possíveis ou se tiver problemas com o WebAssembly, pule para Usar o sistema de arquivos privados de origem em um Web Worker. Caso contrário, você pode continuar a ler.

Usar o sistema de arquivos privados 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 pasta será criado, se não existir. 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 existentes

Se você souber o nome da pessoa, acesse 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');

Receber o arquivo associado a um identificador de arquivo para leitura

Um FileSystemFileHandle representa um arquivo no sistema de arquivos. Para conseguir 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 uma Blob puder. Especificamente, FileReader, URL.createObjectURL(), createImageBitmap() e XMLHttpRequest.send() aceitam Blobs e Files. Caso você queira, a obtenção de um File de um FileSystemFileHandle "livre" 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 um FileSystemWritableFileStream para você e, em seguida, write() o conteúdo. Ao 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 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í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 renomeação podem acontecer em conjunto 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 uma 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 uma pasta no sistema de arquivos particulares 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 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 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()) {}

Lista de forma recursiva o conteúdo de uma pasta e todas as subpastas

É fácil lidar com loops e funções assíncronos em conjunto 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 os respectivos tamanhos. Você pode simplificar a função se não precisar dos tamanhos de arquivo, como directoryEntryPromises.push, sem enviar a promessa handle.getFile(), mas a 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 privados de origem em um Web Worker

Conforme descrito anteriormente, os Web Workers não podem bloquear o thread principal. É por isso que, nesse contexto, os métodos síncronos são permitidos.

Como conseguir um identificador de acesso síncrono

O ponto de entrada para as operações de arquivo mais rápidas possíveis é um FileSystemSyncAccessHandle, recebido 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 arquivos 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 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.

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 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 ao usuário, mas você pode copiar arquivos. Como o 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 particulares 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 é tirada 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 esteja tudo pronto para inspecionar a hierarquia de arquivos. Salve arquivos do sistema de arquivos particular de origem no sistema de arquivos visível para o usuário clicando no nome do arquivo e exclua arquivos e pastas clicando no ícone da lixeira.

Demonstração

Confira o sistema de arquivos privados 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. A versão incorporada abaixo não usa o back-end do sistema de arquivos 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 privados de origem, conforme especificado pelo WHWG, moldou a maneira como usamos e interagimos com os arquivos na Web. Ele possibilitou novos casos de uso que eram impossíveis de alcançar com o sistema de arquivos visível ao usuário. Todos os principais fornecedores de navegadores, Apple, Mozilla e Google, estão integrados e compartilham uma visão conjunta. O desenvolvimento do sistema de arquivos privados de origem é um esforço colaborativo, e o feedback de desenvolvedores e usuários é essencial para o progresso dele. À medida que refinamos e melhoramos 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.