Como o Kiwix PWA permite que os usuários armazenem gigabytes de dados da Internet para uso off-line

Geoffrey Kantaris
Geoffrey Kantaris
Stéphane Coillet-Matillon
Stéphane Coillet-Matillon

Pessoas reunidas ao redor de um laptop em pé sobre uma mesa simples com uma cadeira de plástico à esquerda. O plano de fundo parece uma escola de um país em desenvolvimento.

Este estudo de caso explora como a Kiwix, uma organização sem fins lucrativos, usa a tecnologia Progressive Web App e a API File System Access para permitir que os usuários façam o download e armazenem grandes arquivos da Internet para uso off-line. Saiba mais sobre a implementação técnica do código que lida com o Origin Private File System (OPFS), um novo recurso de navegador no Kiwix PWA que melhora o gerenciamento de arquivos, oferecendo acesso aprimorado a arquivos sem solicitações de permissão. O artigo aborda desafios e destaca possíveis desenvolvimentos futuros nesse novo sistema de arquivos.

Sobre o Kiwix

Mais de 30 anos após o surgimento da Web, um terço da população mundial ainda aguarda um acesso confiável à Internet, de acordo com a União Internacional de Telecomunicações. É aqui que a história termina? Claro que não. O pessoal da Kiwix, uma organização sem fins lucrativos sediada na Suíça, desenvolveram um ecossistema de apps e conteúdo de código aberto para disponibilizar o conhecimento a pessoas com acesso limitado ou sem acesso à Internet. A ideia é que, se você não conseguir acessar a Internet com facilidade, alguém poderá fazer o download dos principais recursos para você, onde e quando a conectividade estiver disponível, e armazená-los localmente para uso off-line posterior. Muitos sites vitais, como Wikipédia, Project Gutenberg, Stack Exchange ou até mesmo palestras do TED, agora podem ser convertidos em arquivos altamente compactados, chamados de arquivos ZIM, e lidos imediatamente pelo navegador Kiwix.

Os arquivos ZIM usam a compactação Zstandard (ZSTD) altamente eficiente (versões mais antigas usavam XZ), principalmente para armazenar HTML, JavaScript e CSS, enquanto as imagens geralmente são convertidas para o formato WebP compactado. Cada ZIM também inclui um URL e um índice de título. A compactação é fundamental, já que a Wikipédia em inglês (6,4 milhões de artigos, mais imagens) é compactada em 97 GB após a conversão para o formato ZIM, o que parece muito até você perceber que a soma de todo o conhecimento humano agora cabe em um smartphone Android intermediário. Muitos recursos menores também são oferecidos, incluindo versões temáticas da Wikipédia, como matemática, medicina e assim por diante.

O Kiwix oferece uma variedade de apps nativos voltados para computadores (Windows/Linux/macOS) e dispositivos móveis (iOS/Android). No entanto, este estudo de caso se concentrará no Progressive Web App (PWA), que pretende ser uma solução universal e simples para qualquer dispositivo que tenha um navegador moderno.

Vamos analisar os desafios impostos ao desenvolvimento de um app da Web universal que precisa oferecer acesso rápido a grandes arquivos de conteúdo totalmente off-line e algumas APIs JavaScript modernas, especialmente a API File System Access e o Origin Private File System, que oferecem soluções inovadoras e empolgantes.

Um app da Web para uso off-line?

Os usuários do Kiwix são um grupo eclético com muitas necessidades diferentes, e o Kiwix tem pouco ou nenhum controle sobre os dispositivos e sistemas operacionais em que vão acessar o conteúdo. Alguns desses dispositivos podem ser lentos ou desatualizados, especialmente em áreas do mundo de baixa renda. Embora o Kiwix tente abranger o maior número possível de casos de uso, a organização também percebeu que poderia alcançar ainda mais usuários usando o software mais universal em qualquer dispositivo: o navegador da Web. Por isso, inspirado na Lei de Atwood (link em inglês), que afirma que Qualquer aplicativo que possa ser escrito em JavaScript, em algum momento, será escrito em JavaScript. Alguns desenvolvedores Kiwix, há cerca de 10 anos, estabeleceram a portabilidade do software Kiwix de C++ para JavaScript.

A primeira versão dessa porta, chamada Kiwix HTML5, foi para o Firefox OS, atualmente extinto, e para as extensões do navegador. Na essência, havia um mecanismo de descompactação C++ (XZ e ZSTD) compilado para a linguagem JavaScript intermediária do ASM.js e, mais recente, o Wasm, ou WebAssembly, usando o compilador Emscripten. Posteriormente renomeadas como Kiwix JS, as extensões de navegador ainda são desenvolvidas.

Navegador Kiwix JS off-line

Acesse o Progressive Web App (PWA). Ao perceber o potencial dessa tecnologia, os desenvolvedores do Kiwix criaram uma versão PWA dedicada do Kiwix JS e definiram a adição de integrações de SO que permitiriam ao app oferecer recursos nativos, especialmente nas áreas de uso off-line, instalação, gerenciamento de arquivos e acesso ao sistema de arquivos.

Os PWAs que priorizam o modo off-line são extremamente leves e, por isso, são perfeitos para contextos em que há uma Internet móvel intermitente ou cara. A tecnologia por trás disso é a API Service Worker e a API Cache, usada por todos os apps baseados em JavaScript Kiwix. Essas APIs permitem que os apps atuem como um servidor, interceptando Solicitações de busca do documento principal ou artigo que está sendo visualizado e redirecionando-as ao back-end (JS) para extrair e construir uma Resposta do arquivo ZIM.

Armazenamento, armazenamento em qualquer lugar

Devido ao grande tamanho dos arquivos ZIM, o armazenamento e o acesso a eles, principalmente em dispositivos móveis, são provavelmente as maiores dores de cabeça para os desenvolvedores do Kiwix. Muitos usuários finais do Kiwix fazem o download de conteúdo no app, quando a Internet está disponível, para uso off-line posterior. Outros usuários fazem o download em um PC usando um torrent e, em seguida, transferem para um dispositivo móvel ou tablet, e alguns trocam conteúdo em pen drives ou discos rígidos portáteis em áreas com Internet móvel cara ou irregular. Todas essas formas de acessar conteúdo de locais arbitrários acessíveis ao usuário precisam ser compatíveis com Kiwix JS e Kiwix PWA.

O que inicialmente permitiu ao Kiwix JS ler arquivos enormes, de centenas de GB (um dos nossos arquivos ZIM é de 166 GB), mesmo em dispositivos com pouca memória, é a API File. Essa API tem suporte universal em qualquer navegador, mesmo em navegadores muito antigos. Portanto, ela funciona como substituto universal para quando APIs mais recentes não forem compatíveis. É tão fácil quanto definir um elemento input no HTML, no caso do Kiwix:

<input
  type="file"
  accept="application/octet-stream,.zim,.zimaa,.zimab,.zimac, ..."
  value="Select folder with ZIM files"
  id="archiveFilesLegacy"
  multiple
/>

Depois de selecionado, o elemento de entrada contém os objetos File, que são essencialmente metadados que referenciam os dados subjacentes no armazenamento. Tecnicamente, o back-end orientado a objetos do Kiwix, escrito em JavaScript puro no lado do cliente, lê pequenas partes do arquivo grande conforme necessário. Se essas frações precisarem ser descompactadas, o back-end as passará para o descompactador do Wasm, recebendo mais frações, se solicitado, até que um blob completo seja descompactado (geralmente um artigo ou um recurso). Isso significa que o arquivo grande nunca precisa ser lido por completo na memória.

Como é universal, a API File tem uma desvantagem que fez os apps Kiwix JS parecerem desajeitados e antiquados em comparação com os apps nativos: exige que o usuário escolha arquivos usando um seletor de arquivos ou arraste e solte um arquivo no app sempre que o app for iniciado. Isso acontece porque, com essa API, não é possível manter as permissões de acesso de uma sessão para outra.

Para reduzir essa UX ruim, como muitos desenvolvedores, os desenvolvedores do Kiwix JS inicialmente seguiram o Electron. O ElectronJS é um framework incrível que fornece recursos avançados, incluindo acesso total ao sistema de arquivos usando APIs do Node. No entanto, ela tem algumas desvantagens bem conhecidas:

  • Ele só é executado em sistemas operacionais de computadores.
  • Ser grande e pesado (70 MB a 100 MB).

O tamanho dos apps Electron, devido ao fato de que uma cópia completa do Chromium está incluída em todos os apps, se compara muito desfavoravelmente a apenas 5,1 MB para o PWA minimizado e empacotado.

Então, havia uma maneira de o Kiwix melhorar a situação para os usuários do PWA?

API File System Access para o resgate

Por volta de 2019, o Kiwix tomou conhecimento de uma API emergente que estava passando por um teste de origem no Chrome 78 e, em seguida, chamou a API Native File System. Ela prometeu a capacidade de conseguir um identificador de arquivo para um arquivo ou uma pasta e armazená-lo em um banco de dados IndexedDB. Esse identificador persiste entre as sessões do app, então o usuário não é forçado a selecionar o arquivo ou a pasta novamente ao reiniciar o app, embora seja necessário responder a uma solicitação de permissão rápida. No momento em que alcançou a produção, ela havia sido renomeada como API File System Access, e as partes principais padronizadas pelo WHWG como API File System (FSA).

Então, como a parte Acesso ao sistema de arquivos da API funciona? Alguns pontos importantes:

  • É uma API assíncrona (exceto para funções especializadas em Web Workers).
  • Os seletores de arquivo ou diretório precisam ser iniciados de maneira programática capturando um gesto do usuário (clique ou toque em um elemento da interface).
  • Para que o usuário dê permissão novamente para acessar um arquivo escolhido anteriormente (em uma nova sessão), um gesto do usuário também é necessário. Na verdade, o navegador se recusará a mostrar a solicitação de permissão se não for iniciado por um gesto do usuário.

O código é relativamente simples, além de precisar usar a API IndexedDB para armazenar identificadores de arquivo e diretório. A boa notícia é que há algumas bibliotecas que fazem a maior parte do trabalho pesado para você, como o browser-fs-access. Na Kiwix JS, decidimos trabalhar diretamente com as APIs, que são muito bem documentadas.

Como abrir seletores de arquivos e diretórios

A abertura de um seletor de arquivos tem a seguinte aparência (usando promessas, mas se você preferir açúcar async/await, consulte o tutorial do Chrome for Developers):

return window
  .showOpenFilePicker({ multiple: false })
  .then(function (fileHandles) {
    return processFileHandle(fileHandles[0]);
  })
  .catch(function (err) {
    // This is normal if app is launching
    console.warn(
      'User cancelled, or cannot access fs without user gesture',
      err,
    );
  });

Observe que, para simplificar, este código processa apenas o primeiro arquivo escolhido (e proíbe a escolha de mais de um). Caso você queira permitir a seleção de vários arquivos com { multiple: true }, basta unir todas as promessas que processam cada identificador em uma instrução Promise.all().then(...). Por exemplo:

let promisesForFiles = fileHandles.map(function (fileHandle) {
    return processFileHandle(fileHandle);
});
return Promise.all(promisesForFiles).then(function (arrayOfFiles) {
    // Do something with the files array
    console.log(arrayOfFiles);
}).catch(function (err) {
    // Handle any errors that occurred during processing
    console.error('Error processing file handles!', err);
)};

No entanto, escolher vários arquivos é possivelmente melhor fazer isso pedindo que o usuário escolha o diretório que contém esses arquivos em vez dos arquivos individuais nele, principalmente porque os usuários do Kiwix tendem a organizar todos os arquivos ZIM no mesmo diretório. O código para iniciar o seletor de diretório é quase o mesmo acima, exceto pelo uso de window.showDirectoryPicker.then(function (dirHandle) { … });.

Processando o identificador do arquivo ou diretório

Depois que você tiver o identificador, é preciso processá-lo para que a função processFileHandle possa ter esta aparência:

function processFileHandle(fileHandle) {
  // Serialize fileHandle to indexedDB
  serializeFSHandletoIdxDB('pickedFSHandle', fileHandle, function (val) {
    console.debug('IndexedDB responded with ' + val);
  });
  return fileHandle.getFile().then(function (file) {
    // Do something with the file
    return file;
  });
}

Você precisa fornecer a função para armazenar o identificador do arquivo. Não há métodos de conveniência para isso, a menos que você use uma biblioteca de abstração. A implementação do Kiwix pode ser vista no arquivo cache.js, mas pode ser bastante simplificada se for usado apenas para armazenar e recuperar um identificador de arquivo ou pasta.

Processar diretórios é um pouco mais complicado, porque você precisa iterar as entradas no diretório escolhido com entries.next() assíncrono para encontrar os arquivos ou tipos de arquivo que quer. Há várias maneiras de fazer isso, mas este é o código usado no Kiwix PWA:

let iterableEntryList = dirHandle.entries();
return iterateAsyncDirEntries(iterableEntryList, []).then(function (entryList) {
  // Do something with the entry list
  return entryList;
});

/**
 * Iterates FileSystemDirectoryHandle iterator and adds entries to an array
 * @param {Iterator} entries An asynchronous iterator of entries
 * @param {Array} archives An array to which to add the entries (may be empty)
 * @return {Promise<Array>} A Promise for an array of entries in the directory
 */
function iterateAsyncDirEntries(entries, archives) {
  return entries
    .next()
    .then(function (result) {
      if (!result.done) {
        let entry = result.value[1];
        // Filter for the files you want
        if (/\.zim(\w\w)?$/i.test(entry.name)) {
          archives.push(entry);
        }
        return iterateAsyncDirEntryArray(entries, archives);
      } else {
        // We've processed all the entries
        if (!archives.length) {
          console.warn('No archives found in the picked directory!');
        }
        return archives;
      }
    })
    .catch(function (err) {
      console.error('There was an error processing the directory!', err);
    });
}

Observe que, para cada entrada no entryList, mais tarde você vai precisar acessar o arquivo com entry.getFile().then(function (file) { … }) quando precisar usá-lo, ou o equivalente usando const file = await entry.getFile() em um async function.

Podemos avançar?

O requisito de conceder permissão, iniciado com um gesto do usuário em inicializações subsequentes do app, adiciona um pouco de atrito à reabertura de arquivos e pastas, mas ainda é muito mais fluido do que ser forçado a selecionar novamente um arquivo. Os desenvolvedores do Chromium estão finalizando o código que permite permissões persistentes para PWAs instalados. Isso é algo que muitos desenvolvedores de PWA têm chamado e é muito esperado.

Mas e se não precisarmos esperar? Recentemente, os desenvolvedores do Kiwix descobriram que é possível eliminar todas as solicitações de permissão no momento usando o novo recurso da API File Access que tem suporte dos navegadores Chromium e Firefox (e parcialmente compatível com o Safari, mas ainda sem FileSystemWritableFileStream). Esse novo recurso é o Sistema de arquivos privados de origem.

Transformação totalmente nativa: o sistema de arquivos particulares de origem

O Sistema de arquivos privados de origem (OPFS, na sigla em inglês) ainda é um recurso experimental no Kiwix PWA, mas a equipe está muito animada para incentivar os usuários a testá-lo, porque ele preenche a lacuna entre apps nativos e da Web. Estes são os principais benefícios:

  • Os arquivos no OPFS podem ser acessados sem solicitações de permissão, mesmo na inicialização. Os usuários podem retomar a leitura de um artigo e navegar por um arquivo de onde pararam em uma sessão anterior, sem qualquer atrito.
  • Ela fornece acesso altamente otimizado aos arquivos armazenados nela: no Android, conseguimos melhorias de velocidade entre cinco e dez vezes mais rápidas.

O acesso a arquivos padrão no Android usando a API File é muito lento, especialmente (como geralmente acontece com usuários do Kiwix) se arquivos grandes forem armazenados em um cartão microSD e não no armazenamento do dispositivo. Tudo isso muda com a nova API. Embora a maioria dos usuários não consiga armazenar um arquivo de 97 GB no OPFS (que consome armazenamento do dispositivo, e não no cartão microSD), ele é perfeito para armazenar arquivos de tamanho pequeno e médio. Você quer a enciclopédia médica mais completa da WikiProject Medicine? Sem problemas, com 1,7 GB, ela se encaixa facilmente no OPFS! Dica: procure othermdwiki_en_all_maxi na biblioteca no app.

Como o OPFS funciona

O OPFS é um sistema de arquivos fornecido pelo navegador, separado para cada origem, que pode ser considerado semelhante ao armazenamento com escopo de apps no Android. Os arquivos podem ser importados para o OPFS do sistema de arquivos visível ao usuário ou podem ser transferidos diretamente para ele. A API também permite criar arquivos no OPFS. Uma vez no OPFS, eles são isolados do restante do dispositivo. Em navegadores baseados no Chromium para computador, também é possível exportar arquivos de volta do OPFS para o sistema de arquivos visível ao usuário.

Para usar o OPFS, a primeira etapa é solicitar acesso a ele usando navigator.storage.getDirectory(). De novo, se você preferir ver o código usando await, leia O sistema de arquivos particulares de origem:

return navigator.storage
  .getDirectory()
  .then(function (handle) {
    return processDirHandle(handle);
  })
  .catch(function (err) {
    console.warn('Unable to get the OPFS directory entry', err);
  });

O identificador que você recebe é o mesmo tipo de FileSystemDirectoryHandle recebido do window.showDirectoryPicker() mencionado acima, o que significa que é possível reutilizar o código que processa isso. Felizmente, não é necessário armazená-lo em indexedDB. Basta adquiri-lo quando precisar. Vamos supor que você já tenha alguns arquivos no OPFS e queira usá-los. Em seguida, usando a função iterateAsyncDirEntries() mostrada anteriormente, é possível fazer algo como:

return navigator.storage.getDirectory().then(function (dirHandle) {
  let entries = dirHandle.entries();
  return iterateAsyncDirEntries(entries, [])
    .then(function (archiveList) {
      return archiveList;
    })
    .catch(function (err) {
      console.error('Unable to iterate OPFS entries', err);
    });
});

Não se esqueça de que você ainda precisa usar getFile() em qualquer entrada com que queira trabalhar da matriz archiveList.

Como importar arquivos para o OPFS

Como colocar arquivos no OPFS? Calma lá! Primeiro, você precisa estimar a quantidade de armazenamento com que precisa trabalhar e garantir que os usuários não tentem colocar um arquivo de 97 GB caso ele não sirva.

É fácil atingir a cota estimada: navigator.storage.estimate().then(function (estimate) { … });. Um pouco mais difícil é descobrir como exibir isso ao usuário. No app Kiwix, optamos por um pequeno painel no app visível ao lado da caixa de seleção, que permite aos usuários testar o OPFS:

Painel mostrando a porcentagem do armazenamento usado e o armazenamento disponível restante em gigabytes.

O painel é preenchido usando estimate.quota e estimate.usage, por exemplo:

let OPFSQuota; // Global variable, so we don't have to keep checking it
return navigator.storage.estimate().then(function (estimate) {
  const percent = ((estimate.usage / estimate.quota) * 100).toFixed(2);
  OPFSQuota = estimate.quota - estimate.usage;
  document.getElementById('OPFSQuota').innerHTML =
    '<b>OPFS storage quota:</b><br />Used:&nbsp;<b>' +
    percent +
    '%</b>; ' +
    'Remaining:&nbsp;<b>' +
    (OPFSQuota / 1024 / 1024 / 1024).toFixed(2) +
    '&nbsp;GB</b>';
});

Como você pode notar, também há um botão que permite que os usuários adicionem arquivos ao OPFS pelo sistema de arquivos visível ao usuário. A boa notícia é que basta usar a API File para acessar o objeto (ou objetos) de arquivo que será importado. Na verdade, é importante não usar window.showOpenFilePicker() porque esse método não é compatível com o Firefox, enquanto o OPFS é mais compatível.

O botão Adicionar arquivos visível na captura de tela acima não é um seletor de arquivos legado, mas click() um seletor legado oculto (elemento <input type="file" multiple … />) quando alguém clica ou toca nele. Em seguida, o app apenas captura o evento change da entrada de arquivo oculta, verifica o tamanho dos arquivos e os rejeita se forem muito grandes para a cota. Se tudo estiver bem, pergunte ao usuário se ele quer adicioná-las:

archiveFilesLegacy.addEventListener('change', function (files) {
  const filesArray = Array.from(files.target.files);
  // Abort if user didn't select any files
  if (filesArray.length === 0) return;
  // Calculate the size of the picked files
  let filesSize = 0;
  filesArray.forEach(function (file) {
    filesSize += file.size;
  });
  // Check the size of the files does not exceed the quota
  if (filesSize > OPFSQuota) {
    // Oh no, files are too big! Tell user...
    console.log('Files would exceed the OPFS quota!');
  } else {
    // Ask user if they're sure... if user said yes...
    return importOPFSEntries(filesArray)
      .then(function () {
        // Tell user we successfully imported the archives
      })
      .catch(function (err) {
        // Tell user there was an error (error catching is important!)
      });
  }
});

Caixa de diálogo perguntando ao usuário se ele quer adicionar uma lista de arquivos .zim ao sistema de arquivos particular de origem.

Como em alguns sistemas operacionais, como o Android, importar arquivos não é a operação mais rápida, o Kiwix também mostra um banner e um pequeno ícone de carregamento enquanto os arquivos são importados. A equipe não descobriu como adicionar um indicador de progresso para essa operação: se você descobrir, responda em um cartão-postal.

Como o Kiwix implementou a função importOPFSEntries()? Isso envolve o uso do método fileHandle.createWriteable(), que permite que cada arquivo seja transmitido para o OPFS. Todo o trabalho duro é feito pelo navegador. O Kiwix está usando promessas aqui por motivos a ver com nossa base de código legada. No entanto, é preciso dizer que, neste caso, await produz uma sintaxe mais simples e evita o efeito pirâmide do fim.

function importOPFSEntries(files) {
  // Get a handle on the OPFS directory
  return navigator.storage
    .getDirectory()
    .then(function (dir) {
      // Collect the promises for each file that we want to write
      let promises = files.map(function (file) {
        // Create the file and get a writeable handle on it
        return dir
          .getFileHandle(file.name, { create: true })
          .then(function (fileHandle) {
            // Get a writer for the file
            return fileHandle.createWritable().then(function (writer) {
              // Show a banner / spinner, then write the file
              return writer
                .write(file)
                .then(function () {
                  // Finished with this writer
                  return writer.close();
                })
                .catch(function (err) {
                  console.error('There was an error writing to the OPFS!', err);
                });
            });
          })
          .catch(function (err) {
            console.error('Unable to get file handle from OPFS!', err);
          });
      });
      // Return a promise that resolves when all the files have been written
      return Promise.all(promises);
    })
    .catch(function (err) {
      console.error('Unable to get a handle on the OPFS directory!', err);
    });
}

Como fazer o download de um stream de arquivos diretamente para o OPFS

Uma variação disso é a capacidade de transmitir um arquivo da Internet diretamente para o OPFS ou para qualquer diretório em que você tenha um identificador de diretório (ou seja, diretórios escolhidos com window.showDirectoryPicker()). Ela usa os mesmos princípios do código acima, mas constrói um Response que consiste em um ReadableStream e um controlador que enfileira os bytes lidos do arquivo remoto. O Response.body resultante é encaminhado para o gravador do novo arquivo dentro do OPFS.

Nesse caso, o Kiwix consegue contar os bytes que passam pelo ReadableStream e, portanto, fornecer um indicador de progresso ao usuário e também avisá-lo para não sair do app durante o download. O código é um pouco complicado demais para ser mostrado aqui, mas como nosso app é FOSS, você pode consultar a fonte se estiver interessado em fazer algo semelhante. A interface do Kiwix é assim. Os diferentes valores de progresso mostrados abaixo são porque ela só atualiza o banner quando a porcentagem muda, mas atualiza o painel Progresso do download com mais frequência:

Interface do usuário do Kiwix com uma barra na parte de baixo avisando que o usuário não precisa sair do app e mostrando o progresso do download do arquivo .zim.

Como o download pode ser uma operação bastante longa, o Kiwix permite que os usuários usem o app livremente durante a operação, mas garante que o banner seja sempre exibido para que os usuários sejam lembrados de não fechar o app até que a operação de download seja concluída.

Implementar um minigerenciador de arquivos no app

Neste ponto, os desenvolvedores do Kiwix PWA perceberam que não é suficiente adicionar arquivos ao OPFS. O app também precisava oferecer aos usuários uma maneira de excluir arquivos que não são mais necessários dessa área de armazenamento e, idealmente, também exportar os arquivos bloqueados no OPFS de volta para o sistema de arquivos visível para o usuário. Efetivamente, se tornou necessário implementar um mini sistema de gerenciamento de arquivos dentro do app.

Um agradecimento à fabulosa extensão OPFS Explorer para Chrome (ela também funciona no Edge). Ele adiciona uma guia nas ferramentas para desenvolvedores que permite ver exatamente o que está no OPFS e também excluir arquivos não autorizados ou com falha. Ele foi inestimável para verificar se o código estava funcionando, monitorar o comportamento dos downloads e limpar geralmente nossos experimentos de desenvolvimento.

A exportação de arquivos depende da capacidade de conseguir um identificador de um arquivo ou diretório escolhido em que o Kiwix salvará o arquivo exportado. Portanto, isso só funciona em contextos em que possa usar o método window.showSaveFilePicker(). Se os arquivos Kiwix fossem menores que vários GB, poderíamos construir um blob na memória, atribuir um URL a ele e fazer o download dele para o sistema de arquivos visível para o usuário. Infelizmente, isso não é possível com arquivos grandes. Se compatível, a exportação será bastante direta: praticamente da mesma forma, ao contrário, que salva um arquivo no OPFS. Receba um identificador no arquivo a ser salvo, peça ao usuário para escolher um local para salvá-lo com window.showSaveFilePicker() e, em seguida, use createWriteable() no saveHandle. É possível ver o código no repositório.

A exclusão de arquivos é compatível com todos os navegadores e pode ser feita com um dirHandle.removeEntry('filename') simples. No caso do Kiwix, preferimos iterar as entradas OPFS como fizemos acima, para verificar se o arquivo selecionado existe primeiro e pedir confirmação, mas isso pode não ser necessário para todos. Novamente, você pode analisar nosso código se tiver interesse.

Foi decidido não sobrecarregar a interface do Kiwix com botões que oferecem essas opções e, em vez disso, colocar pequenos ícones diretamente abaixo da lista de arquivos. Ao tocar em um desses ícones, a cor da lista de arquivos será alterada, como uma dica visual para o usuário sobre o que ele vai fazer. O usuário clica ou toca em um dos arquivos, e a operação correspondente (exportação ou exclusão) é realizada após a confirmação.

Caixa de diálogo perguntando ao usuário se ele quer excluir um arquivo .zim.

Por fim, aqui está uma demonstração de screencast de todos os recursos de gerenciamento de arquivos discutidos acima: adicionar um arquivo ao OPFS, fazer o download diretamente de um arquivo nele, excluir um arquivo e exportar para o sistema de arquivos visível para o usuário.

O trabalho de um desenvolvedor nunca termina

O OPFS é uma ótima inovação para desenvolvedores de PWAs, fornecendo recursos de gerenciamento de arquivos realmente avançados que ajudam a preencher a lacuna entre apps nativos e da Web. Mas os desenvolvedores são um grupo miserável, eles nunca estão tão satisfeitos! O OPFS é quase perfeito, mas não exatamente... É ótimo que os principais recursos funcionem nos navegadores Chromium e Firefox e que eles sejam implementados no Android e no computador. Esperamos que o conjunto completo de recursos também seja implementado no Safari e no iOS em breve. Os seguintes problemas permanecem:

  • Atualmente, o Firefox coloca um limite de 10 GB na cota do OPFS, independentemente do espaço em disco subjacente. Embora para a maioria dos autores de PWA isso possa ser amplo, para o Kiwix, isso é bastante restritivo. Felizmente, os navegadores Chromium são muito mais generosos.
  • No momento, não é possível exportar arquivos grandes do OPFS para o sistema de arquivos visível ao usuário em navegadores para dispositivos móveis ou o Firefox para computador, porque window.showSaveFilePicker() não está implementado. Nesses navegadores, arquivos grandes são efetivamente presos no OPFS. Isso vai contra o ethos do Kiwix de acesso aberto ao conteúdo e a capacidade de compartilhar arquivos entre usuários, especialmente em áreas de conectividade de Internet intermitente ou cara.
  • O usuário não consegue controlar qual armazenamento o sistema de arquivos virtual OPFS consumirá. Isso é particularmente problemático em dispositivos móveis, onde os usuários podem ter uma grande quantidade de espaço em um cartão microSD, mas uma quantidade muito pequena no armazenamento do dispositivo.

No geral, essas são pequenas correções no que seria um grande avanço para o acesso a arquivos nos PWAs. A equipe do Kiwix PWA agradece aos desenvolvedores e defensores do Chromium que primeiro propuseram e projetaram a API File System Access e pelo trabalho árduo para chegar a um consenso entre os fornecedores de navegadores sobre a importância do sistema de arquivos privados de origem. Para o Kiwix JS PWA, ele resolveu muitos dos problemas de UX que atrapalharam o app no passado e nos ajuda na nossa busca para melhorar a acessibilidade do conteúdo do Kiwix para todos. Teste o Kiwix PWA e diga aos desenvolvedores o que você acha.

Para encontrar ótimos recursos sobre recursos de PWA, dê uma olhada nestes sites: