Comment la PWA Kiwix permet aux utilisateurs de stocker des gigaoctets de données sur Internet pour une utilisation hors connexion

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

Personnes rassemblées autour d'un ordinateur portable, debout sur une table simple, avec une chaise en plastique sur la gauche. L'arrière-plan ressemble à une école dans un pays en voie de développement.

Cette étude de cas explique comment Kiwix, une organisation à but non lucratif, utilise la technologie Progressive Web App et l'API File System Access pour permettre aux utilisateurs de télécharger et de stocker des archives Internet volumineuses pour une utilisation hors connexion. Découvrez l'implémentation technique du code traitant du système de fichiers privés d'origine (OPFS, Origin Private File System), une nouvelle fonctionnalité de navigateur de la PWA Kiwix qui améliore la gestion des fichiers en améliorant l'accès aux archives sans invite d'autorisation. Cet article aborde les défis et met en évidence les futurs développements potentiels de ce nouveau système de fichiers.

À propos de Kiwix

Plus de 30 ans après la naissance du Web, un tiers de la population mondiale attend toujours un accès fiable à Internet selon l'Union internationale des télécommunications. C'est là que l'histoire se termine ? Bien sûr que non. Les membres de Kiwix, une organisation à but non lucratif basée en Suisse, ont développé un écosystème d'applications et de contenus Open Source dans le but de mettre les connaissances à la disposition des personnes ayant un accès Internet limité ou inexistant. Son idée est que si vous ne pouvez pas accéder facilement à Internet, quelqu'un peut télécharger des ressources clés pour vous, où et quand la connectivité est disponible, et les stocker localement pour une utilisation hors connexion ultérieure. De nombreux sites essentiels, tels que Wikipédia, Project Gutenberg, Stack Exchange ou même des conférences TED, peuvent désormais être convertis en archives hautement compressées, appelées fichiers ZIM, et lus à la volée par le navigateur Kiwix.

Les archives ZIM utilisent la compression Zstandard (ZSTD) très efficace (les anciennes versions utilisaient XZ), principalement pour stocker du code HTML, JavaScript et CSS, tandis que les images sont généralement converties au format WebP compressé. Chaque ZIM comprend également une URL et un index de titre. La compression est essentielle ici, car l'intégralité de Wikipédia en anglais (6,4 millions d'articles, plus les images) est compressée à 97 Go après la conversion au format ZIM, ce qui semble considérable jusqu'à ce que vous vous rendiez compte que la somme de toutes les connaissances humaines peut désormais tenir sur un téléphone Android de milieu de gamme. De nombreuses ressources plus petites sont également proposées, y compris des versions thématiques de Wikipédia, telles que les mathématiques, la médecine, etc.

Kiwix propose une gamme d'applications natives ciblant les ordinateurs (Windows/Linux/macOS) et les appareils mobiles (iOS/Android). Cependant, cette étude de cas se concentrera sur la progressive web app (PWA), qui se veut une solution universelle et simple pour tous les appareils disposant d'un navigateur récent.

Nous allons examiner les difficultés que représente le développement d'une application Web universelle qui doit fournir un accès rapide aux archives de contenu volumineuses et entièrement hors connexion, ainsi que certaines API JavaScript modernes, en particulier l'API File System Access et l'Origin Private File System, qui fournissent des solutions innovantes et intéressantes à ces défis.

Une application Web pour une utilisation hors connexion ?

Les utilisateurs de Kiwix sont un groupe éclectique ayant des besoins très différents, et Kiwix n'a que peu ou pas de contrôle sur les appareils et les systèmes d'exploitation sur lesquels ils accèdent à son contenu. Certains de ces appareils peuvent être lents ou obsolètes, en particulier dans les régions à faibles revenus du monde. Bien que Kiwix tente de couvrir autant de cas d'utilisation que possible, l'entreprise a également réalisé qu'elle pouvait toucher encore plus d'utilisateurs en utilisant le logiciel le plus universel de n'importe quel appareil: le navigateur Web. Ainsi, en s'inspirant de la loi d'Atwood, qui stipule que toute application pouvant être écrite en JavaScript, sera à terme écrite en JavaScript, certains développeurs Kiwix, il y a environ 10 ans, se sont mis à passer du logiciel Kiwix de C++ à JavaScript.

La première version de ce port, appelée Kiwix HTML5, était destinée au système d'exploitation Firefox, désormais obsolète, et aux extensions du navigateur. Un moteur de décompression C++ (XZ et ZSTD) était à l'origine un moteur de décompression C++ (XZ et ZSTD) compilé pour le langage JavaScript intermédiaire d'ASM.js, puis pour Wasm (ou WebAssembly) basé sur le compilateur Emscripten. Rebaptisées Kiwix JS par la suite, les extensions de navigateur sont toujours développées de manière active.

Navigateur hors connexion Kiwix JS

Choisissez la Progressive Web App (PWA). Conscients du potentiel de cette technologie, les développeurs Kiwix ont créé une version PWA dédiée de Kiwix JS et ont commencé à ajouter des intégrations de système d'exploitation permettant à l'application d'offrir des fonctionnalités natives, en particulier dans les domaines de l'utilisation hors connexion, de l'installation, de la gestion des fichiers et de l'accès au système de fichiers.

Les PWA orientées hors connexion sont extrêmement légères. Elles conviennent donc parfaitement aux contextes où l'Internet mobile est intermittent ou coûteux. Cette technologie repose sur l'API Service Worker et l'API Cache associée, utilisées par toutes les applications basées sur Kiwix JS. Ces API permettent aux applications d'agir en tant que serveur, en interceptant les requêtes de récupération du document principal ou de l'article consulté, et en les redirigeant vers le backend (JS) pour extraire et construire une réponse à partir de l'archive ZIM.

Un stockage à portée de main

Compte tenu de la taille importante des archives ZIM, le stockage et l'accès à ceux-ci, en particulier sur les appareils mobiles, constituent probablement le plus gros problème pour les développeurs Kiwix. De nombreux utilisateurs finaux Kiwix téléchargent du contenu dans l'application, lorsque Internet est disponible, pour une utilisation ultérieure hors connexion. D'autres utilisateurs les téléchargent sur un PC à l'aide d'un torrent, puis les transfèrent vers un appareil mobile ou une tablette. Certains échangent des contenus sur des clés USB ou des disques durs portables dans les zones où l'Internet mobile est coûteux ou disparate. Toutes ces méthodes d'accès au contenu à partir d'emplacements arbitraires accessibles aux utilisateurs doivent être compatibles avec Kiwix JS et Kiwix PWA.

Au départ, Kiwix JS a pu lire d'énormes archives, de plusieurs centaines de Go (l'une de nos archives ZIM s'élève à 166 Go !), même sur les appareils à faible mémoire, grâce à l'API File. Cette API est universellement compatible avec tous les navigateurs, même les navigateurs très anciens. Elle agit donc comme une solution de secours universelle, lorsque les API plus récentes ne sont pas prises en charge. Il suffit de définir un élément input en HTML, dans le cas de Kiwix:

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

Une fois sélectionné, l'élément d'entrée contient les objets File, qui sont essentiellement des métadonnées référençant les données sous-jacentes dans l'espace de stockage. Techniquement, le backend orienté objet de Kiwix, écrit en JavaScript pur côté client, lit de petites parties de l'archive volumineuse selon les besoins. Si ces tranches doivent être décompressées, le backend les transmet au décompresseur Wasm, en obtenant d'autres segments si nécessaire, jusqu'à ce qu'un blob complet soit décompressé (généralement un article ou un élément). Cela signifie que l'archive volumineuse n'a jamais besoin d'être entièrement lue en mémoire.

Universelle telle qu'elle est, l'API File présente un inconvénient : les applications Kiwix JS semblent plus complexes et obsolètes que les applications natives : elle oblige l'utilisateur à sélectionner des archives à l'aide d'un sélecteur de fichier ou à glisser-déposer un fichier dans l'application à chaque lancement de l'application, car cette API ne permet pas de conserver les autorisations d'accès d'une session à l'autre.

Pour atténuer cette mauvaise expérience utilisateur, comme de nombreux développeurs, les développeurs de Kiwix JS ont commencé par opter pour Electron. ElectronJS est un framework exceptionnel qui fournit des fonctionnalités puissantes, y compris un accès complet au système de fichiers à l'aide des API Node. Cependant, elle présente des inconvénients bien connus:

  • Il ne s'exécute que sur les systèmes d'exploitation de bureau.
  • Il est volumineux et lourd (de 70 à 100 Mo).

La taille des applications Electron, du fait qu'une copie complète de Chromium est incluse dans chaque application, est très défavorable par rapport à seulement 5,1 Mo pour la PWA réduite et intégrée.

Existe-t-il un moyen pour Kiwix d'améliorer la situation des utilisateurs de la PWA ?

L'API File System Access à la rescousse

Vers 2019, Kiwix a découvert l'existence d'une API émergente qui passait une phase d'évaluation dans Chrome 78, alors appelée API Native File System. Elle promettait de pouvoir obtenir un handle pour un fichier ou un dossier et le stocker dans une base de données IndexedDB. Il est important de noter que cet identifiant persiste entre les sessions d'application. L'utilisateur n'est donc pas obligé de sélectionner à nouveau le fichier ou le dossier lors du redémarrage de l'application (bien qu'il doive répondre à une invite d'autorisation rapide). À son arrivée en production, elle avait été renommée API File System Access, et les parties principales standardisées par le WHATWG sont l'API File System (FSA).

Comment la partie Accès au système de fichiers de l'API fonctionne-t-elle ? Quelques points importants:

  • Il s'agit d'une API asynchrone (à l'exception des fonctions spécialisées des workers Web).
  • Les sélecteurs de fichier ou de répertoire doivent être lancés par programmation en capturant un geste de l'utilisateur (cliquer ou appuyer sur un élément d'interface utilisateur).
  • Pour que l'utilisateur puisse de nouveau autoriser l'accès à un fichier précédemment sélectionné (dans une nouvelle session), un geste est également nécessaire. En fait, le navigateur refusera d'afficher l'invite d'autorisation s'il n'est pas déclenché par un geste de l'utilisateur.

Le code est relativement simple, à l'exception de la nécessité d'utiliser l'API IndexedDB, peu intuitive, pour stocker les identifiants de fichiers et de répertoires. La bonne nouvelle, c'est que deux bibliothèques, comme browser-fs-access, effectuent une grande partie des tâches fastidieuses à votre place. Chez Kiwix JS, nous avons décidé de travailler directement avec les API, qui sont très bien documentées.

Ouverture des outils de sélection de fichiers et de répertoires

L'ouverture d'un sélecteur de fichier ressemble à ceci (ici, à l'aide de promesses, mais si vous préférez le sucre async/await, consultez le tutoriel 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,
    );
  });

Notez que par souci de simplicité, ce code ne traite que le premier fichier sélectionné (et interdit d'en sélectionner plusieurs). Si vous souhaitez autoriser la sélection de plusieurs fichiers avec { multiple: true }, il vous suffit d'encapsuler toutes les promesses qui traitent chaque handle dans une instruction Promise.all().then(...), par exemple:

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

Cependant, il est sans doute préférable de demander à l'utilisateur de choisir le répertoire qui contient ces fichiers plutôt que les fichiers individuels qu'il contient, d'autant plus que les utilisateurs de Kiwix ont tendance à organiser tous leurs fichiers ZIM dans le même répertoire. Le code permettant de lancer le sélecteur de répertoire est presque le même que celui ci-dessus, à la différence que vous utilisez window.showDirectoryPicker.then(function (dirHandle) { … });.

Traiter le handle de fichier ou de répertoire

Une fois que vous disposez de l'identifiant, vous devez le traiter. La fonction processFileHandle pourrait donc se présenter comme suit:

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

Notez que vous devez fournir la fonction permettant de stocker le handle de fichier, il n'existe aucune méthode pratique pour cela, sauf si vous utilisez une bibliothèque d'abstractions. L'implémentation de cette fonctionnalité par Kiwix est visible dans le fichier cache.js, mais elle pourrait être considérablement simplifiée si elle n'était utilisée que pour stocker et récupérer un handle de fichier ou de dossier.

Le traitement des répertoires est un peu plus compliqué, car vous devez itérer les entrées du répertoire sélectionné avec une entries.next() asynchrone pour trouver les fichiers ou les types de fichiers souhaités. Il existe plusieurs façons de procéder, mais voici le code utilisé dans la PWA Kiwix:

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

Notez que pour chaque entrée de entryList, vous devrez ultérieurement obtenir le fichier avec entry.getFile().then(function (file) { … }) lorsque vous en aurez besoin, ou un fichier équivalent en utilisant const file = await entry.getFile() dans un async function.

Pouvons-nous aller plus loin ?

L'obligation pour l'utilisateur d'accorder une autorisation initiée par un geste de l'utilisateur lors des lancements ultérieurs de l'application ajoute un peu de friction à la réouverture des fichiers et des dossiers, mais c'est encore beaucoup plus fluide que de devoir choisir à nouveau un fichier. Les développeurs Chromium finalisent actuellement du code qui autoriserait des autorisations persistantes pour les PWA installées. De nombreux développeurs de PWA l'attendent et l'attendent avec impatience.

Mais que se passe-t-il si nous ne devons pas attendre ? Les développeurs Kiwix ont récemment constaté qu'il était possible d'éliminer toutes les invites d'autorisation pour le moment, en utilisant une toute nouvelle fonctionnalité de l'API File Access compatible avec les navigateurs Chromium et Firefox (et partiellement compatible avec Safari, mais toujours manquante avec FileSystemWritableFileStream). Cette nouvelle fonctionnalité est le système de fichiers privé d'origine.

Devenir entièrement natif: système de fichiers privés d'origine

Le système de fichiers privés d'origine (OPFS, Origin Private File System) est encore une fonctionnalité expérimentale de la PWA Kiwix, mais l'équipe est très enthousiaste à l'idée d'encourager les utilisateurs à l'essayer, car elle permet de combler le fossé entre les applications natives et Web. Voici les principaux avantages de cette fonctionnalité:

  • Les archives du service OPFS sont accessibles sans invite d'autorisation, même au lancement. Les utilisateurs peuvent reprendre la lecture d'un article et parcourir une archive, là où ils s'étaient arrêtés lors d'une session précédente, sans aucun problème.
  • Elle fournit un accès hautement optimisé aux fichiers qu'il contient: sur Android, nous constatons une amélioration de la vitesse cinq à dix fois plus rapide.

L'accès standard aux fichiers dans Android à l'aide de l'API File est particulièrement lent (comme c'est souvent le cas pour les utilisateurs de Kiwix) si des archives volumineuses sont stockées sur une carte microSD plutôt que dans l'espace de stockage de l'appareil. Tout cela change avec cette nouvelle API. Bien que la plupart des utilisateurs ne puissent pas stocker un fichier de 97 Go dans OPFS (qui consomme l'espace de stockage de l'appareil, et non le stockage de carte microSD), il est idéal pour stocker des archives de petite à moyenne taille. Vous souhaitez obtenir l'encyclopédie médicale la plus complète de WikiProject Medicine ? Pas de problème, avec 1,7 Go, il rentre facilement dans l'OPFS ! (Conseil : recherchez othermdwiki_en_all_maxi dans la bibliothèque de l'application.)

Fonctionnement du service OPFS

OPFS est un système de fichiers fourni par le navigateur, distinct pour chaque origine, qui peut être considéré comme semblable à l'espace de stockage à l'échelle de l'application sur Android. Les fichiers peuvent être importés dans l'OPFS à partir du système de fichiers visible par l'utilisateur ou y être téléchargés directement (l'API permet également de créer des fichiers dans l'OPFS). Une fois dans l'OPFS, ils sont isolés du reste de l'appareil. Sur les navigateurs pour ordinateur basés sur Chromium, il est également possible d'exporter des fichiers OPFS vers le système de fichiers visible par l'utilisateur.

Pour utiliser l'OPFS, la première étape consiste à en demander l'accès à l'aide de navigator.storage.getDirectory() (là encore, si vous préférez voir le code avec await, consultez The Origin Private File System):

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

L'identifiant que vous obtenez est le même type de FileSystemDirectoryHandle que celui de window.showDirectoryPicker() mentionné ci-dessus, ce qui signifie que vous pouvez réutiliser le code qui gère cela (et heureusement, il n'est pas nécessaire de le stocker dans indexedDB, il vous suffit de l'obtenir lorsque vous en avez besoin). Supposons que vous ayez déjà des fichiers dans le fichier OPFS et que vous souhaitiez les utiliser. Ensuite, à l'aide de la fonction iterateAsyncDirEntries() présentée précédemment, vous pouvez effectuer une opération comme celle-ci:

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'oubliez pas que vous devez toujours utiliser getFile() sur chaque entrée du tableau archiveList que vous souhaitez utiliser.

Importer des fichiers dans OPFS

Alors, comment importer des fichiers dans OPFS en premier lieu ? Pas si vite ! Tout d'abord, vous devez estimer la quantité d'espace de stockage dont vous disposez et vous assurer que les utilisateurs n'essaient pas d'insérer un fichier de 97 Go s'il ne convient pas.

Pour obtenir le quota estimé, rien de plus simple : navigator.storage.estimate().then(function (estimate) { … });. Un peu plus difficile est de trouver comment présenter cela à l'utilisateur. Dans l'application Kiwix, nous avons opté pour un petit panneau visible juste à côté de la case à cocher, qui permet aux utilisateurs de tester OPFS:

Panneau affichant l&#39;espace de stockage utilisé en pourcentage et l&#39;espace de stockage disponible restant en gigaoctets.

Le panneau est renseigné à l'aide de estimate.quota et estimate.usage, par exemple:

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

Comme vous pouvez le voir, il existe également un bouton qui permet aux utilisateurs d'ajouter des fichiers à l'OPFS à partir du système de fichiers visible par l'utilisateur. La bonne nouvelle est que vous pouvez simplement utiliser l'API File pour obtenir le ou les objets File nécessaires à importer. En fait, il est important de ne pas utiliser window.showOpenFilePicker(), car cette méthode n'est pas compatible avec Firefox, alors que OPFS est absolument compatible.

Le bouton visible Add file(s) (Ajouter un ou plusieurs fichiers) que vous voyez dans la capture d'écran ci-dessus n'est pas un ancien sélecteur de fichier, mais click() est un ancien sélecteur masqué (élément <input type="file" multiple … />) lorsque l'utilisateur clique ou appuie dessus. L'application enregistre ensuite simplement l'événement change de l'entrée de fichier masquée, vérifie la taille des fichiers et les rejette s'ils sont trop volumineux pour le quota. Si tout va bien, demandez à l'utilisateur s'il souhaite les ajouter:

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

Boîte de dialogue demandant à l&#39;utilisateur s&#39;il souhaite ajouter une liste de fichiers .zim au système de fichiers d&#39;origine privé.

Étant donné que sur certains systèmes d'exploitation, comme Android, l'importation d'archives n'est pas l'opération la plus rapide, Kiwix affiche également une bannière et une petite icône de chargement pendant l'importation des archives. L'équipe n'a pas trouvé comment ajouter un indicateur de progression pour cette opération. Si vous parvenez à le résoudre, envoyez-lui vos réponses sur une carte postale.

Comment Kiwix a-t-il implémenté la fonction importOPFSEntries() ? Cela implique d'utiliser la méthode fileHandle.createWriteable(), qui permet de diffuser chaque fichier en streaming dans l'OPFS. Le navigateur gère tout le travail acharné. (Kiwix utilise ici des promesses pour des raisons liées à notre ancien codebase, mais dans ce cas, await produit une syntaxe plus simple et évite la pyramide de l'effet de malheur).

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

Télécharger un flux de fichiers directement dans l'OPFS

Une variante possible est la possibilité de diffuser un fichier depuis Internet directement dans l'OPFS, ou dans n'importe quel répertoire dont vous avez un handle de répertoire (c'est-à-dire des répertoires sélectionnés avec window.showDirectoryPicker()). Il utilise les mêmes principes que le code ci-dessus, mais construit un Response composé d'un ReadableStream et d'un contrôleur qui met en file d'attente les octets lus à partir du fichier distant. L'objet Response.body résultant est ensuite transmis au rédacteur du nouveau fichier dans OPFS.

Dans ce cas, Kiwix est en mesure de compter les octets transmis via ReadableStream. Il fournit donc un indicateur de progression à l'utilisateur et l'avertit de ne pas quitter l'application pendant le téléchargement. Le code est un peu trop compliqué pour être affiché ici, mais comme notre application est une application FOSS, vous pouvez consulter la source si vous souhaitez faire quelque chose de similaire. Voici à quoi ressemble l'interface utilisateur de Kiwix (les différentes valeurs de progression indiquées ci-dessous sont affichées, car la bannière n'est mise à jour que lorsque le pourcentage change, mais met à jour le panneau Progression du téléchargement plus régulièrement):

Interface utilisateur de Kiwix avec une barre en bas indiquant à l&#39;utilisateur de ne pas quitter l&#39;application et indiquant la progression du téléchargement de l&#39;archive .zim

Étant donné que le téléchargement peut être une opération assez longue, Kiwix permet aux utilisateurs d'utiliser l'application librement pendant l'opération, mais garantit que la bannière est toujours affichée, afin de rappeler aux utilisateurs de ne pas fermer l'application tant que l'opération de téléchargement n'est pas terminée.

Implémenter un mini gestionnaire de fichiers dans l'application

À ce stade, les développeurs de PWA Kiwix ont réalisé qu'il ne suffit pas de pouvoir ajouter des fichiers à l'OPFS. L'application devait également donner aux utilisateurs un moyen de supprimer les fichiers dont ils n'ont plus besoin de cette zone de stockage et, idéalement, d'exporter tous les fichiers verrouillés dans OPFS vers le système de fichiers visible par l'utilisateur. En réalité, il est devenu nécessaire d'implémenter un mini système de gestion de fichiers dans l'application.

Remerciez rapidement la fabuleuse extension OPFS Explorer pour Chrome (elle fonctionne également dans Edge). Il ajoute un onglet dans les outils pour les développeurs qui vous permet de voir exactement ce qu'il y a dans OPFS, et de supprimer les fichiers non autorisés ou en échec. Elle a été inestimable pour vérifier si le code fonctionnait, surveiller le comportement des téléchargements et, de manière générale, nettoyer nos expériences de développement.

L'exportation des fichiers dépend de la possibilité d'obtenir un handle de fichier sur un fichier ou un répertoire sélectionné dans lequel Kiwix va enregistrer le fichier exporté. Cela ne fonctionne que dans les contextes où la méthode window.showSaveFilePicker() est autorisée. Si les fichiers Kiwix étaient inférieurs à plusieurs Go, nous pourrions construire un blob en mémoire, lui attribuer une URL, puis le télécharger dans le système de fichiers visible par l'utilisateur. Malheureusement, cela n'est pas possible avec des archives aussi volumineuses. Si elle est prise en charge, l'exportation est assez simple: pratiquement la même chose que l'enregistrement d'un fichier dans OPFS (obtenez un handle vers le fichier à enregistrer, demandez à l'utilisateur de choisir un emplacement pour l'enregistrer avec window.showSaveFilePicker(), puis utilisez createWriteable() sur saveHandle). Vous pouvez voir le code dans le dépôt.

Tous les navigateurs sont compatibles avec la suppression de fichiers, et il suffit d'une dirHandle.removeEntry('filename') pour effectuer cette opération. Dans le cas de Kiwix, nous avons préféré itérer les entrées OPFS comme nous l'avons fait ci-dessus, afin de pouvoir vérifier que le fichier sélectionné existe en premier et demander une confirmation, mais cela n'est peut-être pas nécessaire pour tout le monde. N'hésitez pas à examiner notre code si cela vous intéresse.

Il a été décidé de ne pas encombrer l'interface utilisateur de Kiwix avec des boutons offrant ces options, et de placer à la place de petites icônes directement sous la liste d'archives. En appuyant sur l'une de ces icônes, l'utilisateur peut modifier la couleur de la liste des archives et obtenir un indice visuel sur ce qu'il va faire. Il clique ou appuie ensuite sur l'une des archives, et l'opération correspondante (exportation ou suppression) est effectuée (après confirmation).

Boîte de dialogue demandant à l&#39;utilisateur s&#39;il souhaite supprimer un fichier .zim.

Enfin, voici une démonstration d'enregistrement d'écran de toutes les fonctionnalités de gestion de fichiers décrites ci-dessus : ajout d'un fichier à l'OPFS, téléchargement direct d'un fichier, suppression d'un fichier et exportation vers le système de fichiers visible par l'utilisateur.

Le travail d'un développeur n'est jamais terminé

OPFS est une excellente innovation pour les développeurs de PWA. Il fournit des fonctionnalités de gestion de fichiers très performantes qui contribuent grandement à réduire l'écart entre les applications natives et les applications Web. Mais les développeurs sont horribles : ils ne sont jamais très satisfaits ! L'OPFS est presque parfait, mais pas tout à fait. C'est génial que les principales fonctionnalités fonctionnent à la fois dans les navigateurs Chromium et Firefox, et qu'elles soient implémentées sur Android et sur ordinateur. Nous espérons que l'ensemble des fonctionnalités sera bientôt disponible dans Safari et iOS. Les problèmes suivants subsistent:

  • Firefox limite actuellement le quota OPFS de 10 Go, quelle que soit la quantité d'espace disque sous-jacent. Pour la plupart des auteurs de PWA, ce quota est assez restrictif pour Kiwix. Heureusement, les navigateurs Chromium sont beaucoup plus généreux.
  • Il n'est actuellement pas possible d'exporter des fichiers volumineux depuis OPFS vers le système de fichiers visible par l'utilisateur sur les navigateurs mobiles ou sur Firefox pour ordinateur, car window.showSaveFilePicker() n'est pas implémenté. Dans ces navigateurs, les fichiers volumineux sont effectivement piégés dans le fichier OPFS. Cela va à l'encontre de la philosophie Kiwix d'un accès ouvert au contenu, et de la possibilité de partager des archives entre utilisateurs, en particulier dans les domaines de connectivité Internet intermittente ou coûteuse.
  • L'utilisateur ne peut pas contrôler l'espace de stockage que le système de fichiers virtuel OPFS consommera. Cela est particulièrement problématique sur les appareils mobiles, où les utilisateurs peuvent disposer d'une grande quantité d'espace sur une carte microSD, mais d'une très petite quantité sur l'espace de stockage de l'appareil.

Mais dans l'ensemble, il s'agit là de petits problèmes qui, par ailleurs, représentent un énorme pas en avant pour l'accès aux fichiers dans les PWA. L'équipe PWA Kiwix est très reconnaissante envers les développeurs et les porte-parole de Chromium qui ont proposé et conçu l'API File System Access, ainsi que le travail acharné que les fournisseurs de navigateurs ont travaillé pour parvenir à un consensus sur l'importance du système de fichiers privés d'origine. Pour la PWA Kiwix JS, elle a résolu un grand nombre des problèmes d'expérience utilisateur qui entraînaient l'application dans le passé, et nous a aidés à améliorer l'accessibilité du contenu Kiwix pour tous. Veuillez essayer la PWA Kiwix et dire ce que vous en pensez aux développeurs.

Vous trouverez d'excellentes ressources sur les fonctionnalités des PWA sur les sites suivants: