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 assises sur une table simple autour d'un ordinateur portable, avec une chaise en plastique sur la gauche. Le contexte ressemble à une école dans un pays en développement.

Cette étude de cas explore comment Kiwix, une organisation à but non lucratif, utilise la technologie d'application Web progressive et l'API File System Access pour permettre aux utilisateurs de télécharger et de stocker de grandes archives Internet à utiliser 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 dans la PWA Kiwix qui améliore la gestion des fichiers et offre un accès amélioré aux archives sans invites d'autorisation. L'article aborde les défis et met en avant 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 visant à mettre les connaissances à la disposition des personnes ayant un accès limité à Internet ou n'y ayant pas accès du tout. 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 ultérieure hors connexion. De nombreux sites essentiels, comme Wikipedia, Project Gutenberg, Stack Exchange ou même les conférences TED, peuvent désormais être convertis en archives hautement compressées, appelées fichiers ZIM, et lus instantanément par le navigateur Kiwix.

Les archives ZIM utilisent une compression Zstandard (ZSTD) très efficace (les versions antérieures 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 titres. La compression est essentielle ici, car l'intégralité de Wikipédia en anglais (6,4 millions d'articles, plus des images) est compressée à 97 Go après conversion au format ZIM, ce qui semble beaucoup jusqu'à ce que vous réalisiez 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 destinées aux ordinateurs (Windows/Linux/macOS) et aux mobiles (iOS/Android). Cette étude de cas se concentre toutefois sur la progressive web app (PWA), qui vise à offrir une solution simple et universelle pour tout appareil équipé d'un navigateur récent.

Nous examinerons les défis posés par le développement d'une application Web universelle qui doit fournir un accès rapide à des archives de contenu volumineuses hors connexion, ainsi que certaines API JavaScript modernes, en particulier l'API File System Access et Origin Private File System, qui fournissent des solutions innovantes et passionnantes à ces défis.

Une application Web pour une utilisation hors connexion ?

Les utilisateurs de Kiwix sont un groupe éclectique qui a de nombreux besoins différents, et Kiwix n'a peu ou pas de contrôle sur les appareils et les systèmes d'exploitation sur lesquels ils accéderont à son contenu. Certains de ces appareils peuvent être lents ou obsolètes, en particulier dans les régions à faible revenu du monde. Bien que Kiwix essaie de couvrir autant de cas d'utilisation que possible, l'organisation a également réalisé qu'elle pouvait toucher encore plus d'utilisateurs en utilisant le logiciel le plus universel sur n'importe quel appareil : le navigateur Web. Ainsi, 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 de Kiwix, il y a environ 10 ans, ont décidé de porter le logiciel Kiwix de C++ à JavaScript.

La première version de ce port, appelée Kiwix HTML5, était destinée à l'OS Firefox (désormais disparu) et aux extensions de navigateur. Au cœur de WebAssembly se trouvait (et se trouve) un moteur de décompression C++ (XZ et ZSTD) compilé dans le langage JavaScript intermédiaire ASM.js, puis Wasm ou WebAssembly, à l'aide du compilateur Emscripten. Renommées plus tard Kiwix JS, les extensions de navigateur sont toujours en cours de développement.

Navigateur hors connexion Kiwix JS

Saisissez la progressive web app (PWA). Conscients du potentiel de cette technologie, les développeurs de Kiwix ont créé une version PWA dédiée de Kiwix JS. Ils ont entrepris d'ajouter des intégrations d'OS pour permettre à l'application d'offrir des fonctionnalités de type natif, en particulier dans les domaines de l'utilisation hors connexion, de l'installation, de la gestion de fichiers et de l'accès aux systèmes de fichiers.

Les PWA axées sur le mode hors connexion sont extrêmement légères et sont donc parfaites pour les contextes où la connexion Internet mobile est intermittente ou coûteuse. La technologie sous-jacente est 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 ou de l'article principal consultés, et en les redirigeant vers le backend (JS) pour extraire et construire une réponse à partir de l'archive ZIM.

Stockage, stockage partout

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

C'est l'API File qui a initialement permis à Kiwix JS de lire d'énormes archives de centaines de Go (l'une de nos archives ZIM fait 166 Go !), même sur des appareils à faible mémoire. Cette API est compatible avec tous les navigateurs, même les très anciens. Elle sert donc de solution de secours universelle lorsque les API plus récentes ne sont pas prises en charge. C'est aussi simple que 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 faisant référence aux données sous-jacentes stockées. Techniquement, le backend orienté objet de Kiwix, écrit en JavaScript pur côté client, lit si nécessaire de petites portions de la grande archive. Si ces tranches doivent être décompressées, le backend les transmet au décompresseur Wasm et obtient 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 la grande archive n'a jamais besoin d'être entièrement lue dans la mémoire.

Bien qu'elle soit universelle, l'API File présente un inconvénient qui fait que les applications Kiwix JS semblent lourdes et démodées par rapport aux applications natives : l'utilisateur doit sélectionner des archives à l'aide d'un sélecteur de fichiers ou faire glisser un fichier dans l'application à chaque fois qu'il lance l'application, car avec cette API, il n'est pas possible 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 Kiwix JS ont d'abord opté pour Electron. ElectronJS est un framework incroyable qui offre des fonctionnalités puissantes, y compris un accès complet au système de fichiers à l'aide des API Node. Cependant, cette méthode présente des inconvénients bien connus:

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

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

Kiwix pouvait-il améliorer la situation pour les utilisateurs de la PWA ?

L'API File System Access à la rescousse

Vers 2019, Kiwix a pris connaissance d'une API émergente qui était en cours d'évaluation dans Chrome 78, sous le nom d'API Native File System. Il promettait de pouvoir obtenir un gestionnaire de fichiers pour un fichier ou un dossier, et de le stocker dans une base de données IndexedDB. Surtout, ce handle persiste entre les sessions de l'application. L'utilisateur n'est donc pas obligé de sélectionner à nouveau le fichier ou le dossier lors du redémarrage de l'application (mais il doit répondre à une invite d'autorisation rapide). Au moment de sa mise en production, elle a été rebaptisée API File System Access, et les parties principales ont été normalisées par le WHATWG sous le nom d'API File System (FSA).

Comment fonctionne la partie File System Access de l'API ? Voici quelques points importants à retenir :

  • Il s'agit d'une API asynchrone (à l'exception des fonctions spécialisées dans les Web Workers).
  • Les sélecteurs de fichiers ou de répertoires doivent être lancés de manière programmatique 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 sélectionné précédemment (dans une nouvelle session), un geste de l'utilisateur est également nécessaire. En fait, le navigateur refusera d'afficher l'invite d'autorisation s'il n'est pas initié par un geste de l'utilisateur.

Le code est relativement simple, à l'exception de l'utilisation de l'API IndexedDB peu pratique pour stocker les poignées de fichiers et de répertoires. La bonne nouvelle est qu'il existe quelques bibliothèques qui effectuent une grande partie du travail pour vous, comme browser-fs-access. Chez Kiwix JS, nous avons décidé de travailler directement avec les API, qui sont très bien documentées.

Ouvrir des sélecteurs de fichiers et de répertoires

L'ouverture d'un sélecteur de fichiers se présente comme suit (ici à l'aide de Promises, mais si vous préférez le sucre async/await, consultez le tutoriel Chrome pour les développeurs) :

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 poignée 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);
)};

Toutefois, il est préférable de demander à l'utilisateur de sélectionner le répertoire contenant ces fichiers plutôt que les fichiers individuels qu'il contient, en particulier parce 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 identique à celui ci-dessus, sauf que vous utilisez window.showDirectoryPicker.then(function (dirHandle) { … });.

Traitement du gestionnaire de fichier ou de répertoire

Une fois que vous avez le handle, vous devez le traiter. La fonction processFileHandle peut 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 pour stocker le gestionnaire de fichiers. Il n'existe aucune méthode pratique pour cela, sauf si vous utilisez une bibliothèque d'abstraction. L'implémentation de cette fonctionnalité par Kiwix est visible dans le fichier cache.js, mais elle pourrait être considérablement simplifiée s'il n'est utilisé 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 entries.next() asynchrone pour trouver les fichiers ou les types de fichiers que vous souhaitez. Il existe plusieurs façons de procéder, mais voici le code utilisé dans la PWA Kiwix, en résumé :

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 plus tard obtenir le fichier avec entry.getFile().then(function (file) { … }) lorsque vous en aurez besoin, ou l'é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 déclenchée par un geste utilisateur lors des lancements ultérieurs de l'application ajoute une légère friction à l'ouverture (ré)ouverte des fichiers et des dossiers, mais elle est toujours beaucoup plus fluide que d'être obligé de choisir à nouveau un fichier. Les développeurs Chromium finalisent actuellement le code permettant d'accorder des autorisations persistantes aux PWA installées. C'est une fonctionnalité que de nombreux développeurs de PWA réclament depuis longtemps et qui est très attendue.

Mais que faire si nous n'avons pas besoin d'attendre ? Les développeurs de Kiwix ont récemment constaté qu'il était possible de supprimer toutes les invites d'autorisation dès maintenant grâce à une toute nouvelle fonctionnalité de l'API File Access, compatible avec les navigateurs Chromium et Firefox (et partiellement compatible avec Safari, mais manquante de FileSystemWritableFileStream). Cette nouvelle fonctionnalité est le système de fichiers privés d'origine.

Passer au natif complet : le système de fichiers privé Origin

Le système de fichiers privé d'origine (OPFS) est toujours une fonctionnalité expérimentale dans la PWA Kiwix, mais l'équipe est ravie d'encourager les utilisateurs à l'essayer, car il comble en grande partie l'écart entre les applications natives et les applications Web. Voici les principaux avantages :

  • Les archives de l'OPFS sont accessibles sans invite d'autorisation, même au lancement. Les utilisateurs peuvent reprendre la lecture d'un article et la navigation dans une archive à partir de l'endroit où ils s'étaient arrêtés lors d'une session précédente, sans aucune friction.
  • Il offre un accès hautement optimisé aux fichiers qui y sont stockés : sur Android, nous constatons une amélioration de la vitesse de cinq à dix fois.

L'accès aux fichiers standard dans Android à l'aide de l'API File est extrêmement lent, en particulier (comme c'est souvent le cas pour les utilisateurs de Kiwix) si de grandes archives 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 l'OPFS (qui consomme l'espace de stockage de l'appareil, et non celui de la carte microSD), il est parfait pour stocker des archives de petite à moyenne taille. Vous voulez consulter l'encyclopédie médicale la plus complète de WikiProject Medicine ? Pas de problème, avec 1,7 Go, il se glisse aisément dans l'OPFS ! (Conseil : recherchez Autre → mdwiki_en_all_maxi dans la bibliothèque de l'application.)

Fonctionnement de l'OPFS

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

Pour utiliser OPFS, la première étape consiste à demander l'accès à celui-ci à l'aide de navigator.storage.getDirectory() (là encore, si vous préférez voir le code en utilisant await, consultez L'origine du système de fichiers privé):

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

Le handle que vous obtenez à partir de là est le même type de FileSystemDirectoryHandle que celui que vous obtenez avec 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 quand vous en avez besoin). Supposons que vous ayez déjà des fichiers dans OPFS et que vous souhaitiez les utiliser. Vous pouvez alors utiliser la fonction iterateAsyncDirEntries() présentée précédemment:

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 toutes les entrées du tableau archiveList que vous souhaitez utiliser.

Importer des fichiers dans OPFS

Comment insérer des fichiers dans l'OPFS ? 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'importer un fichier de 97 Go s'il ne rentre pas.

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

Panneau affichant l&#39;espace de stockage utilisé en pourcentage et l&#39;espace de stockage restant disponible 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 constater, il existe également un bouton permettant 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 l'objet (ou les objets) File nécessaires à l'importation. Il est même important de ne pas utiliser window.showOpenFilePicker(), car cette méthode n'est pas compatible avec Firefox, contrairement à OPFS .

Le bouton Ajouter un ou plusieurs fichiers visible dans la capture d'écran ci-dessus n'est pas un ancien sélecteur de fichiers, mais il click() un ancien sélecteur masqué (élément <input type="file" multiple … />) lorsqu'il est cliqué ou appuyé. L'application ne fait alors que capturer l'événement change de l'entrée de fichier masquée, vérifier la taille des fichiers et les rejeter 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 privé d&#39;origine.

Comme l'importation d'archives n'est pas l'opération la plus rapide sur certains systèmes d'exploitation, comme Android, Kiwix affiche également une bannière et une petite icône de chargement pendant l'importation des archives. L'équipe n'a pas réussi à ajouter un indicateur de progression à cette opération. Si vous trouvez la solution, envoyez-nous la réponse sur une carte postale.

Comment Kiwix a-t-il implémenté la fonction importOPFSEntries() ? Pour ce faire, vous devez utiliser la méthode fileHandle.createWriteable(), qui permet de diffuser chaque fichier dans l'OPFS. Tout le travail est géré par le navigateur. (Kiwix utilise des promesses ici pour des raisons liées à notre ancien codebase, mais il convient de noter que, dans ce cas, await produit une syntaxe plus simple et évite l'effet pyramidal de la mort.)

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 OPFS

Une variante de cette méthode consiste à diffuser un fichier depuis Internet directement dans l'OPFS ou dans n'importe quel répertoire pour lequel vous disposez d'un gestionnaire de répertoires (c'est-à-dire les répertoires sélectionnés avec window.showDirectoryPicker()). Il utilise les mêmes principes que le code ci-dessus, mais crée un Response composé d'un ReadableStream et d'un contrôleur qui met en file d'attente les octets lus à partir du fichier distant. Le Response.body obtenu est ensuite transmis à l'écrivain du nouveau fichier dans l'OPFS.

Dans ce cas, Kiwix est en mesure de compter les octets qui transitent par ReadableStream. Il fournit donc un indicateur de progression à l'utilisateur et l'invite également à ne pas quitter l'application pendant le téléchargement. Le code est un peu trop complexe pour être présenté ici, mais comme notre application est une application Open Source, vous pouvez consulter le code source si vous souhaitez faire quelque chose de similaire. Voici à quoi ressemble l'interface utilisateur de Kiwix (les différentes valeurs de progression affichées ci-dessous sont dues au fait qu'elle ne met à jour la bannière que lorsque le pourcentage change, mais qu'elle 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 s'assure que la bannière est toujours affichée pour 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 la PWA Kiwix ont réalisé qu'il ne suffisait pas d'ajouter des fichiers à l'OPFS. L'application devait également permettre aux utilisateurs de supprimer les fichiers dont ils n'avaient plus besoin de cette zone de stockage et, dans l'idéal, d'exporter les fichiers verrouillés dans l'OPFS vers le système de fichiers visible par l'utilisateur. En fait, il a fallu implémenter un mini système de gestion de fichiers dans l'application.

Je voudrais faire un clin d'œil à l'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 qui se trouve dans l'OPFS, et de supprimer également les fichiers indésirables ou ayant échoué. Cet outil a été très utile pour vérifier si le code fonctionnait, pour surveiller le comportement des téléchargements et, en général, pour nettoyer nos tests de développement.

L'exportation de 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 donc que dans les contextes où il peut utiliser la méthode window.showSaveFilePicker(). Si les fichiers Kiwix étaient inférieurs à plusieurs Go, nous pourrions créer 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 l'exportation est prise en charge, elle est relativement simple : elle est pratiquement identique, à l'inverse, à l'enregistrement d'un fichier dans l'OPFS (obtenez un gestionnaire du fichier à enregistrer, demandez à l'utilisateur de choisir un emplacement où l'enregistrer avec window.showSaveFilePicker(), puis utilisez createWriteable() sur saveHandle). Vous pouvez consulter le code dans le dépôt.

La suppression de fichiers est prise en charge par tous les navigateurs et peut être effectuée avec un simple dirHandle.removeEntry('filename'). 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 confirmation, mais cela n'est peut-être pas nécessaire pour tout le monde. Encore une fois, vous pouvez examiner notre code si vous le souhaitez.

Il a été décidé de ne pas encombrer l'interface utilisateur de Kiwix avec des boutons proposant ces options, mais plutôt de placer de petites icônes directement sous la liste des archives. Si l'utilisateur appuie sur l'une de ces icônes, la couleur de la liste des archives change, ce qui lui indique visuellement ce qu'il va faire. L'utilisateur 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 en écran partagé 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 dans l'OPFS, suppression d'un fichier et exportation vers le système de fichiers visible par l'utilisateur.

Le travail d'un développeur ne s'arrête jamais

L'OPFS est une excellente innovation pour les développeurs de PWA, car il offre des fonctionnalités de gestion de fichiers très puissantes qui permettent de combler l'écart entre les applications natives et les applications Web. Mais les développeurs sont une misérable, ils ne sont jamais très satisfaits ! L'OPFS est presque parfait, mais pas tout à fait… C'est formidable 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 implémenté dans Safari et iOS. Les problèmes suivants sont toujours en cours :

  • Dans Firefox, le quota OPFS est actuellement limité à 10 Go, quel que soit l'espace disque sous-jacent. Bien que la plupart des auteurs de PWA puissent disposer d'un tel quota, cela peut être suffisant pour Kiwix, mais c'est assez restrictif pour Kiwix. Heureusement, les navigateurs Chromium sont beaucoup plus généreux.
  • Il n'est actuellement pas possible d'exporter de gros fichiers de l'OPFS vers le système de fichiers visible par l'utilisateur dans les navigateurs mobiles ou Firefox pour ordinateur, car window.showSaveFilePicker() n'est pas implémenté. Dans ces navigateurs, les fichiers de grande taille sont effectivement piégés dans l'OPFS. Cela va à l'encontre de l'esprit de Kiwix, qui est de permettre un accès ouvert aux contenus et de partager des archives entre utilisateurs, en particulier dans les zones où la connectivité Internet est intermittente ou coûteuse.
  • L'utilisateur ne peut pas contrôler le 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 de grandes quantités d'espace sur une carte microSD, mais d'une quantité très faible sur l'espace de stockage de l'appareil.

Mais dans l'ensemble, il s'agit de problèmes mineurs dans ce qui constitue un grand pas en avant pour l'accès aux fichiers dans les PWA. L'équipe Kiwix PWA est très reconnaissante aux développeurs et défenseurs de Chromium qui ont proposé et conçu pour la première fois l'API File System Access, ainsi que pour le travail acharné qu'ils ont fourni pour parvenir à un consensus entre les fournisseurs de navigateurs sur l'importance du système de fichiers privé d'origine. Pour la PWA Kiwix JS, elle a résolu un grand nombre des problèmes d'expérience utilisateur qui ont entravé l'application par le passé et nous aide dans notre quête d'amélioration de l'accessibilité des contenus Kiwix pour tous. Veuillez essayer la PWA Kiwix et en faire part aux développeurs.

Vous trouverez des ressources utiles sur les fonctionnalités des PWA sur les sites suivants: