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 surgimento da Web, um terço da população mundial ainda está esperando por 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 Wikipedia, 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 conhecer os desafios do desenvolvimento de um app da Web universal que precisa fornecer 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 a Kiwix tente abranger o maior número de casos de uso possível, 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 já não existe mais, e para extensões de navegador. Em sua essência, havia (e é) um mecanismo de descompactação C++ (XZ e ZSTD) compilado na linguagem JavaScript intermediária ASM.js, e mais tarde, o Wasm, ou WebAssembly, usando o compilador Emscripten (links em inglês). Mais tarde renomeado como JS Kiwix, as extensões do navegador ainda estão 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 as solicitações de busca do documento ou artigo principal que está sendo visualizado e redirecionando 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 para os desenvolvedores 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 formas de acessar conteúdo de locais arbitrários acessíveis ao usuário precisam ser compatíveis com o Kiwix JS e o 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 todos os navegadores, até mesmo nos navegadores muito antigos. Portanto, ela funciona como um substituto universal, para quando as APIs mais recentes não são aceitas. É 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 arquivo grande nunca precisa ser lido totalmente na memória.

Universal como é, a API File tem uma desvantagem que faz com que os apps JS do Kiwix pareçam complicados e antigos em comparação com apps nativos: ela exige que o usuário selecione arquivos usando um seletor de arquivos ou arraste e solte um arquivo no app sempre que o app for iniciado, porque essa API não pode 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 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 do PWA minimizado e empacotado.

Então, como a Kiwix poderia melhorar a situação para os usuários do PWA?

API File System Access ao resgate

Por volta de 2019, a Kiwix descobriu uma API emergente que estava passando por um teste de origem no Chrome 78, então chamada de API Native File System. Ele prometeu a capacidade de ter um identificador de arquivo para um arquivo ou 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 o trabalho pesado para você, como o 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 escolhido (e proíbe a seleção de mais de um). Se você quiser 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 do 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;
  });
}

Observe que você precisa fornecer a função para armazenar o identificador do arquivo, não há métodos convenientes 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 se for usada apenas para armazenar e extrair um gerenciador de arquivos ou pastas.

O processamento de 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 arquivos 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?! Os desenvolvedores da Kiwix descobriram recentemente que é possível eliminar todos os prompts de permissão no momento 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 não tem FileSystemWritableFileStream). Esse novo recurso é o Sistema de arquivos particular de origem.

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

O Sistema de arquivos particulares de origem (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.
  • Ela fornece acesso altamente otimizado aos arquivos armazenados nele. No Android, percebemos melhorias de velocidade de cinco a dez vezes mais rápido.

O acesso padrão a arquivos no Android usando a API File é muito lento, principalmente (como geralmente acontece para usuários do Kiwix) se arquivos grandes forem armazenados em um cartão microSD em vez de 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 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 da 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 a partir do sistema de arquivos visível ao usuário ou é possível fazer o download deles diretamente nele. A API também permite criar arquivos no OPFS. Depois de fazer isso, eles ficam isolados do restante do dispositivo. Em navegadores para computador baseados no Chromium, 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(). 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 na matriz archiveList.

Como importar arquivos para o OPFS

Como você coloca arquivos no OPFS? Não tão rápido! 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 visível ao lado da caixa de seleção, que permite aos usuários testar 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 notar, há também um botão que permite que os usuários adicionem arquivos ao OPFS do 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 de carregamento enquanto os arquivos estão sendo 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);
    });
}

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 para o qual você tenha um identificador de diretório (ou seja, diretórios selecionados com window.showDirectoryPicker()). Ele usa os mesmos princípios do código acima, mas cria 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 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

Nesse ponto, os desenvolvedores do 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. Efetivamente, tornou-se necessário implementar um mini sistema de gerenciamento de arquivos dentro do 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.

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 é alterada, como uma dica visual para o usuário sobre o que ele fará. Em seguida, 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, aqui está uma demonstração de screencast de todos os recursos de gerenciamento de arquivos discutidos acima: como adicionar um arquivo ao OPFS, fazer o download de um arquivo diretamente nele, 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:

  • O Firefox atualmente coloca um limite de 10 GB na cota OPFS, independentemente do espaço em disco subjacente. Embora para a maioria dos autores de PWAs 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 pelo usuário em navegadores para dispositivos móveis ou o 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: