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 em volta de um laptop em uma mesa simples com uma cadeira de plástico à esquerda. O plano de fundo parece uma escola em um país em desenvolvimento.

Este estudo de caso explora como a Kiwix, uma organização sem fins lucrativos, usa a tecnologia de app da Web progressivo 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 do navegador no PWA Kiwix que aprimora o gerenciamento de arquivos, oferecendo acesso aprimorado aos arquivos sem solicitações de permissão. O artigo aborda os desafios e destaca possíveis desenvolvimentos futuros nesse novo sistema de arquivos.

Sobre o Kiwix

Mais de 30 anos após o nascimento da Web, um terço da população mundial ainda espera por 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. A equipe da Kiwix, uma organização sem fins lucrativos da Suíça, desenvolveu um ecossistema de apps e conteúdo de código aberto que visa disponibilizar conhecimento para pessoas com acesso limitado ou nenhum acesso à Internet. A ideia é que, se você não conseguir acessar a Internet com facilidade, alguém poderá fazer o download de recursos importantes para você, onde e quando a conectividade estiver disponível, e armazená-los localmente para uso off-line posterior. Muitos sites importantes, como a Wikipedia, o Project Gutenberg, o Stack Exchange ou até mesmo as palestras do TED, agora podem ser convertidos em arquivos altamente compactados, chamados de arquivos ZIM, e lidos em tempo real pelo navegador Kiwix.

Os arquivos ZIM usam a compactação Zstandard (ZSTD) altamente eficiente (as versões mais antigas usavam XZ), principalmente para armazenar HTML, JavaScript e CSS, enquanto as imagens geralmente são convertidas para o formato compactado WebP. Cada ZIM também inclui um URL e um índice de títulos. A compactação é fundamental, já que todo o conteúdo da Wikipedia em inglês (6, 4 milhões de artigos e imagens) é compactado para 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 de gama média. Muitos recursos menores também são oferecidos, incluindo versões temáticas da Wikipédia, como matemática, medicina etc.

O Kiwix oferece uma gama de apps nativos voltados para uso em computadores (Windows/Linux/macOS) e dispositivos móveis (iOS/Android). No entanto, este estudo de caso vai se concentrar no App Web Progressivo (PWA), que tem como objetivo ser uma solução universal e simples para qualquer dispositivo com um navegador moderno.

Vamos analisar os desafios apresentados no 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, principalmente a API File System Access e o Origin Private File System, que oferecem soluções inovadoras e interessantes para esses desafios.

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 eles vão acessar o conteúdo. Alguns desses dispositivos podem ser lentos ou desatualizados, especialmente em áreas de baixa renda do mundo. 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. Assim, inspirados pela Lei de Atwood, que afirma que qualquer aplicativo que possa ser escrito em JavaScript será escrito em JavaScript, alguns desenvolvedores do Kiwix, há cerca de 10 anos, começaram a portar o software do Kiwix de C++ para JavaScript.

A primeira versão dessa porta, chamada Kiwix HTML5, era para o Firefox OS, que não existe mais, e para extensões de navegador. O núcleo era (e é) um mecanismo de descompressão C++ (XZ e ZSTD) compilado para a linguagem JavaScript intermediária do ASM.js e, mais tarde, Wasm ou WebAssembly, usando o compilador Emscripten. Mais tarde, renomeadas como Kiwix JS, as extensões de navegador ainda estão sendo desenvolvidas.

Kiwix JS Offline Browser

Entre no Progressive Web App (PWA). Percebendo o potencial dessa tecnologia, os desenvolvedores do Kiwix criaram uma versão PWA dedicada do Kiwix JS e começaram a adicionar integrações de SO que permitiam que o app oferecesse recursos semelhantes aos nativos, principalmente nas áreas de uso off-line, instalação, manuseio de arquivos e acesso ao sistema de arquivos.

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

Armazenamento em todos os lugares

Considerando o grande tamanho dos arquivos ZIM, o armazenamento e o acesso a eles, especialmente em dispositivos móveis, provavelmente é a maior dor de cabeça dos 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. Alguns trocam conteúdo em pen drives ou discos rígidos portáteis em áreas com Internet móvel instável ou cara. Todas essas maneiras de acessar conteúdo de locais arbitrários acessíveis ao usuário precisam ter suporte ao Kiwix JS e ao Kiwix PWA.

O que inicialmente permitiu que o Kiwix JS lesse arquivos enormes, de centenas de GB (um dos nossos arquivos ZIM tem 166 GB!), mesmo em dispositivos com pouca memória, é a API File. Essa API tem suporte universal em qualquer navegador, até mesmo muito antigos, e funciona como substituto universal quando as APIs mais recentes não têm suporte. É 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 armazena os objetos de arquivo, que são essencialmente metadados que fazem referência aos dados subjacentes no armazenamento. Tecnicamente, o back-end orientado a objetos do Kiwix, escrito em JavaScript puro do lado do cliente, lê pequenas fatias do grande arquivo conforme necessário. Se essas fatias precisarem ser descompactadas, o back-end as transmitirá ao descompressor Wasm, recebendo mais fatias, se solicitado, até que um blob completo seja descompactado (geralmente um artigo ou um recurso). Isso significa que o grande arquivo nunca precisa ser lido totalmente na memória.

A API File é universal, mas tem uma desvantagem que faz com que os apps Kiwix JS pareçam desajeitados e antiquados em comparação com os apps nativos: ela exige que o usuário selecione arquivos usando um seletor de arquivos ou arrastar e soltar um arquivo no app, sempre que o app é iniciado, porque com essa API, não há como manter as permissões de acesso de uma sessão para a próxima.

Para reduzir essa experiência ruim, como muitos desenvolvedores, os desenvolvedores do Kiwix JS inicialmente seguiram a rota do Electron. O ElectronJS é um framework incrível que oferece recursos poderosos, incluindo acesso total ao sistema de arquivos usando APIs do Node. No entanto, ele tem algumas desvantagens bem conhecidas:

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

O tamanho dos apps do Electron, devido ao fato de que uma cópia completa do Chromium é incluída em cada app, é muito menor do que apenas 5,1 MB para o PWA minimizado e agrupado.

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 ficou sabendo de uma API emergente que estava passando por um teste de origem no Chrome 78, chamada de API Native File System. Ele prometia a capacidade de receber um identificador de arquivo para um arquivo ou uma pasta e armazená-lo em um banco de dados IndexedDB. É importante ressaltar que esse identificador persiste entre as sessões do app. Assim, o usuário não precisa escolher o arquivo ou a pasta novamente ao iniciar o app novamente (embora ele precise responder a uma solicitação de permissão rápida). Quando chegou à produção, foi renomeada como API File System Access, e as partes principais foram padronizadas pelo WHATWG como a API File System (FSA).

Como funciona a parte File System Access da API? Alguns pontos importantes:

  • Ela é uma API assíncrona (exceto para funções especializadas em Web Workers).
  • Os seletores de arquivo ou diretório precisam ser iniciados programaticamente capturando um gesto do usuário (clique ou toque em um elemento da interface).
  • Para que o usuário conceda 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 recusa a mostrar a solicitação de permissão se não for iniciada por um gesto do usuário.

O código é relativamente simples, exceto pelo uso da API IndexedDB para armazenar identificadores de arquivos e diretórios. A boa notícia é que há algumas bibliotecas que fazem grande parte do trabalho pesado para você, como browser-fs-access. No 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 é semelhante a esta (aqui usando promessas, mas se você preferir async/await sugar, consulte o tutorial do Chrome para desenvolvedores):

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
,
   
);
 
});

Para simplificar, esse código processa apenas o primeiro arquivo selecionado e não permite selecionar mais de um. Caso você queira permitir a seleção de vários arquivos com { multiple: true }, basta agrupar 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, é melhor escolher vários arquivos pedindo ao usuário que selecione o diretório que contém esses arquivos, em vez de cada um deles, 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 que o acima, exceto pelo uso de window.showDirectoryPicker.then(function (dirHandle) { … });.

Processar o identificador de arquivo ou diretório

Depois de ter o identificador, é necessário processá-lo. Assim, a função processFileHandle pode ficar assim:

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 de 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 simplificada consideravelmente se for usada apenas para armazenar e recuperar um identificador de arquivo ou pasta.

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

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);
   
});
}

Para cada entrada no entryList, você 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 ir mais longe?

A exigência de que o usuário conceda permissão iniciada com um gesto do usuário em lançamentos subsequentes do app adiciona uma pequena quantidade de atrito à (re)abertura de arquivos e pastas, mas ainda é muito mais fluido do que ser forçado a escolher um arquivo novamente. Os desenvolvedores do Chromium estão finalizando o código que permitiria permissões persistentes para PWAs instalados. Isso é algo que muitos desenvolvedores de PWA têm solicitado e é muito esperado.

Mas e se não precisarmos esperar?! Recentemente, os desenvolvedores do Kiwix descobriram que é possível eliminar todos os avisos de permissão usando um novo recurso da API File Access que é compatível com os navegadores Chromium e Firefox (e parcialmente compatível com o Safari, mas ainda falta FileSystemWritableFileStream). Esse novo recurso é o Origin Private File System.

Como usar o sistema de arquivos particular da origem: a versão totalmente nativa

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

  • Os arquivos na OPFS podem ser acessados sem solicitações de permissão, mesmo na inicialização. Os usuários podem continuar lendo um artigo e navegando em um arquivo de onde pararam em uma sessão anterior, sem nenhuma dificuldade.
  • Ele oferece acesso altamente otimizado aos arquivos armazenados nele: no Android, notamos melhorias de velocidade entre cinco e dez vezes mais rápidas.

O acesso padrão a arquivos no Android usando a API File é muito lento, especialmente (como costuma ser o caso dos usuários do Kiwix) se grandes arquivos forem armazenados em um cartão microSD em vez de no armazenamento do dispositivo. Tudo isso muda com essa nova API. Embora a maioria dos usuários não consiga armazenar um arquivo de 97 GB no OPFS, que consome o armazenamento do dispositivo, e não do cartão microSD, ele é perfeito para armazenar arquivos de tamanho pequeno a médio. Você quer a enciclopédia médica mais completa do WikiProject Medicine? Sem problemas. Com 1,7 GB, cabe 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 no escopo do app no Android. Os arquivos podem ser importados para o OPFS do sistema de arquivos visível para o usuário ou podem ser transferidos por download diretamente para ele. A API também permite criar arquivos no OPFS. Quando estão no OPFS, eles são isolados do restante do dispositivo. Em computadores com navegadores baseados no Chromium, também é possível exportar arquivos do OPFS para o sistema de arquivos visível para o usuário.

Para usar o OPFS, a primeira etapa é solicitar acesso a ele usando navigator.storage.getDirectory(). Se você preferir ver o código usando await, leia O sistema de arquivos particular do Origin:

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

O handle que você recebe é do mesmo tipo de FileSystemDirectoryHandle que você recebe do window.showDirectoryPicker() mencionado acima, o que significa que você pode reutilizar o código que lida com isso. Felizmente, não é necessário armazenar isso em indexedDB. Basta receber quando precisar. Digamos que você já tenha alguns arquivos no OPFS e queira usá-los. Usando a função iterateAsyncDirEntries() mostrada anteriormente, você pode 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 que queira trabalhar com a matriz archiveList.

Como importar arquivos para o OPFS

Como você coloca arquivos no OPFS? Mas não é bem assim. Primeiro, você precisa estimar a quantidade de armazenamento com que vai trabalhar e garantir que os usuários não tentem colocar um arquivo de 97 GB se ele não couber.

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

Painel mostrando o armazenamento usado em porcentagem 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 ver, também há um botão que permite que os usuários adicionem arquivos ao OPFS no sistema de arquivos visível para o usuário. A boa notícia é que você pode usar a API File para receber o objeto de arquivo necessário que será importado. Na verdade, é importante não usar window.showOpenFilePicker() porque esse método não tem suporte do Firefox, enquanto o OPFS tem.

O botão Add file(s) 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 clicado. O app captura o evento change da entrada de arquivo oculta, verifica o tamanho dos arquivos e os rejeita se eles forem muito grandes para a cota. Se tudo estiver bem, pergunte ao usuário se ele quer adicionar:

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 giratório 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 usar o método fileHandle.createWriteable(), que permite que cada arquivo seja transmitido para o OPFS. Todo o trabalho pesado é feito pelo navegador. O Kiwix está usando promessas aqui por motivos relacionados ao nosso repositório de código legado, mas é preciso dizer que, neste caso, await produz uma sintaxe mais simples e evita o efeito pirâmide da morte.

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);
   
});
}

Fazer o download de um fluxo de arquivos diretamente no OPFS

Uma variação disso é a capacidade de transmitir um arquivo da Internet diretamente para o OPFS ou para qualquer diretório para o qual você tenha um identificador de diretório (ou seja, diretórios escolhidos com window.showDirectoryPicker()). Ele 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 é então encaminhado para o gravador do novo arquivo dentro do OPFS.

Nesse caso, o Kiwix pode contar os bytes que passam pelo ReadableStream e, assim, fornecer um indicador de progresso ao usuário e também alertá-lo para que não saia do app durante o download. O código é um pouco complicado para mostrar aqui, mas, como nosso app é um app FOSS, você pode consultar a origem se tiver interesse em fazer algo semelhante. A interface do Kiwix tem esta aparência. Os diferentes valores de progresso mostrados abaixo são porque ele só atualiza o banner quando a porcentagem muda, mas atualiza o painel Download progress com mais frequência:

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

Como o download pode ser uma operação 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 não fechem o app até que a operação de download seja concluída.

Como implementar um minigerenciador de arquivos no app

Nesse ponto, os desenvolvedores da PWA do Kiwix perceberam que não bastava 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 nessa área de armazenamento e, idealmente, exportar arquivos bloqueados no OPFS de volta para o sistema de arquivos visível para o usuário. Na prática, foi necessário implementar um mini sistema de gerenciamento de arquivos no app.

Um breve comentário sobre a maravilhosa extensão OPFS Explorer para Chrome, que 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 maliciosos ou com falhas. Foi muito útil para verificar se o código estava funcionando, monitorar o comportamento dos downloads e, de modo geral, limpar nossos experimentos de desenvolvimento.

A export depende da capacidade de receber um identificador de arquivo em um arquivo ou diretório escolhido em que o Kiwix vai salvar o arquivo exportado. Portanto, isso só funciona em contextos em que é possível usar o método window.showSaveFilePicker(). Se os arquivos do Kiwix fossem menores que vários GB, poderíamos construir um blob na memória, atribuir um URL a ele e fazer o download para o sistema de arquivos visível para o usuário. Infelizmente, isso não é possível com arquivos tão grandes. Se houver suporte, a exportação é bastante simples: praticamente a mesma coisa, ao contrário, como salvar um arquivo no OPFS (pegar um identificador do arquivo a ser salvo, pedir ao usuário para escolher um local para salvar com window.showSaveFilePicker() e, em seguida, usar createWriteable() no saveHandle). Você pode ver o código no repositório.

A exclusão de arquivos é aceita por todos os navegadores e pode ser feita com um dirHandle.removeEntry('filename') simples. No caso do Kiwix, preferimos iterar as entradas do 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 examinar nosso código se tiver interesse.

Decidimos 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. Tocar em um desses ícones muda a cor da lista de arquivos, 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 (exportar ou excluir) é realizada após a confirmação.

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

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

O trabalho de um desenvolvedor nunca acaba

O OPFS é uma grande inovação para desenvolvedores de PWAs, oferecendo recursos de gerenciamento de arquivos muito eficientes que ajudam a fechar a lacuna entre apps nativos e da Web. Mas os desenvolvedores são um grupo infeliz. Eles nunca ficam satisfeitos. O OPFS é quase perfeito, mas não totalmente… É ótimo que os principais recursos funcionem nos navegadores Chromium e Firefox e que 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 ainda existem:

  • Atualmente, o Firefox coloca um limite de 10 GB na cota do OPFS, não importa quanto espaço em disco exista. 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 para o usuário em navegadores para dispositivos móveis ou no Firefox para computador, porque window.showSaveFilePicker() não está implementado. Nesses navegadores, os arquivos grandes são efetivamente armazenados no OPFS. Isso vai contra o espírito do Kiwix de acesso aberto ao conteúdo e a capacidade de compartilhar arquivos entre usuários, especialmente em áreas com conectividade de Internet intermitente ou cara.
  • O usuário não pode controlar qual armazenamento o sistema de arquivos virtual do OPFS vai consumir. Isso é particularmente problemático em dispositivos móveis, em que os usuários podem ter grandes quantidades de espaço em um cartão microSD, mas uma quantidade muito pequena no armazenamento do dispositivo.

No geral, essas são pequenas falhas em um grande avanço para o acesso a arquivos em PWAs. A equipe da PWA do Kiwix é muito grata aos desenvolvedores e defensores do Chromium que propuseram e projetaram a API de acesso ao sistema de arquivos e pelo trabalho árduo de alcançar um consenso entre os fornecedores de navegadores sobre a importância do sistema de arquivos particular de origem. Para a PWA do Kiwix JS, ela resolveu muitos dos problemas de UX que prejudicaram o app no passado e nos ajuda na nossa busca para melhorar a acessibilidade do conteúdo do Kiwix para todos. Teste a PWA do Kiwix e diga aos desenvolvedores o que você acha.

Para conferir alguns recursos excelentes sobre as capacidades de PWA, acesse estes sites: