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 ao usuário, que fornece acesso opcional a um tipo especial de arquivo altamente otimizado para performance.

Suporte ao navegador

O sistema de arquivos privado de origem é compatível com navegadores modernos e é padronizado pelo grupo de trabalho da tecnologia de aplicativos de hipertexto da Web (WHATWG) no padrão de sistema de arquivos dinâmico.

Browser Support

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

Source

Motivação

Ao pensar em arquivos no computador, você provavelmente pensa em uma hierarquia de arquivos: arquivos organizados em pastas que podem ser exploradas com o explorador 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 típica 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 baixa o arquivo resultante com um injetado <a download="ToDo.txt> que você programa click() com 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.

Maneira moderna de trabalhar com arquivos na Web

Esse fluxo não é representativo de como os usuários pensam em editar arquivos e significa que os usuários acabam com cópias baixadas dos arquivos de entrada. Portanto, a API File System Access introduziu três métodos de seletor:

Eles ativam um fluxo da seguinte maneira:

  1. Abra ToDo.txt com showOpenFilePicker() e receba um FileSystemFileHandle objeto.
  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 mudanças 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 de arquivo por perto para não precisar mostrar a caixa de diálogo de salvamento de arquivo novamente.

Restrições de trabalho com arquivos na Web

Os arquivos e pastas acessíveis com esses métodos ficam no que pode ser chamado de sistema de arquivos visível ao usuário. Os arquivos salvos da Web e os arquivos executáveis especificamente são marcados com a marca da Web, assim, 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 documento, pode ser considerada uma verificação de vírus baseada na nuvem. Ao gravar 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 em si não é modificado, a menos que passe em todas essas verificações de segurança.

Esse trabalho torna as operações de arquivo relativamente lentas, apesar das melhorias aplicadas sempre que possível, por exemplo, no macOS. Ainda assim, cada write() chamada é independente. Portanto, nos bastidores, ela abre o arquivo, busca o deslocamento fornecido 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, 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 de resolução progressivamente mais baixa da anterior, o que torna muitas operações, como o zoom, mais rápidas. Então, como os aplicativos da Web podem aproveitar os benefícios dos arquivos, mas 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 visível ao usuário versus o sistema de arquivos privado 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 podem ser lidos, gravados, movidos e renomeados, o sistema de arquivos privado de origem não foi criado para ser visto pelos usuários. Os arquivos e pastas no sistema de arquivos privado de origem, como o nome sugere, são privados e, mais concretamente, privados 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. 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 privado de origem. Assim, 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 de origem independente, o que significa que o sistema de arquivos privado de origem de https://developer.chrome.com é completamente diferente do de, digamos, 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 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 do sistema de arquivos visível ao usuário e do 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 o mesmo, 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 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. O ponto de entrada para o sistema de arquivos privado de origem é a chamada do método navigator.storage.getDirectory.

Especificidades do sistema de arquivos privado 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, consulte a usage entrada para saber quanto armazenamento seu app já consome, que é detalhado pelo mecanismo de armazenamento no usageDetails objeto, em que você quer analisar a fileSystem entrada especificamente. Como o sistema de arquivos privado de origem não está visível para o usuário, não há solicitações de permissão nem verificações de Navegação Segura.

Como acessar o diretório raiz

Para acessar o diretório raiz, execute o comando a seguir. 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, pois 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.

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

Criar novos arquivos e pastas

Depois de ter uma pasta raiz, crie arquivos e pastas usando os getFileHandle() e os getDirectoryHandle() métodos, 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 atuais

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

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

Um FileSystemFileHandle representa um arquivo no sistema de arquivos. Para receber o associado File, use o getFile() método. Um objeto File é um tipo específico de Blob e pode ser usado em qualquer contexto em que um Blob possa.

Em particular, FileReader, URL.createObjectURL(), createImageBitmap(), e XMLHttpRequest.send() aceitam Blobs e Files. A obtenção de um File de um FileSystemFileHandle "libera" 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

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

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 {recursive: true} opção.

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 removeEntry() método.

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

Mover e renomear arquivos e pastas

Renomeie e mova arquivos e pastas usando o move() método. A movimentação e a renomeação podem ocorrer 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 resolve() método, transmitindo um FileSystemHandle como argumento. Para receber o caminho completo de um arquivo ou pasta no sistema de arquivos privado de origem, use o diretório raiz como o diretório de referência obtido usando 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 dois identificadores e não sabe se eles apontam para o mesmo arquivo ou pasta. Para verificar se esse é o caso, use o isSameEntry() método.

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

Listar o conteúdo de uma pasta

FileSystemDirectoryHandle é um iterador assíncrono que você itera com um for await... of loop. Como um iterador assíncrono, ele também oferece suporte aos entries(), aos values(), e aos keys() métodos, 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 todas as subpastas

É fácil errar ao lidar com loops e funções assíncronas pareadas com recursão. A função a seguir pode servir como ponto de partida para listar o conteúdo de uma pasta e todas as subpastas, incluindo todos os arquivos e tamanhos. Você pode simplificar a função se não precisar dos tamanhos de arquivo, em que diz directoryEntryPromises.push, não transmitindo a handle.getFile() promessa, 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 privado de origem em um Web Worker

Como descrito anteriormente, os Web Workers não podem bloquear a linha de execução principal. É por isso que, 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 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

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 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 contenha todas as modificações feitas por write().
  • close(): fecha o identificador de acesso.

Confira um exemplo que usa cada método.

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 ao 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 ao 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 OPFS Explorer do Chrome para depurar o sistema de arquivos privado de origem. A captura de tela da seção Criar novos arquivos e pastas é 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 você estará pronto para inspecionar a hierarquia de arquivos. Salve arquivos do sistema de arquivos privado de origem para o sistema de arquivos visível ao usuário clicando no nome do arquivo e exclua arquivos e pastas clicando no ícone da lixeira.

Demonstração

Confira o sistema de arquivos privado de origem em ação (se você instalar a extensão do 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 GitHub. Observe como a versão incorporada 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 é.

Conclusão

O sistema de arquivos privado de origem, conforme especificado pelo WHATWG, moldou a maneira como usamos e interagimos com arquivos na Web. Ele ativou 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 a bordo e compartilham uma visão conjunta. O desenvolvimento do sistema de arquivos privado 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 sobre o repositório whatwg/fs na forma de problemas ou solicitações de envio é bem-vindo.

Agradecimentos

Este documento foi revisado por Austin Sully, Etienne Noël e Rachel Andrew.