Système de fichiers privé d'origine

La norme de système de fichiers introduit un système de fichiers privé d'origine (OPFS, Origin Private File System) en tant que point de terminaison de stockage réservé à l'origine de la page et non visible par l'utilisateur. Il fournit un accès facultatif à un type de fichier spécial, hautement optimisé pour les performances.

Prise en charge des navigateurs

Le système de fichiers privés d'origine est compatible avec les navigateurs récents. Il est standardisé par le WhatWG (Web Hypertext Application Technology Working Group) dans la norme de vie du système de fichiers.

Navigateurs pris en charge

  • Chrome: 86 <ph type="x-smartling-placeholder">
  • Edge: 86 <ph type="x-smartling-placeholder">
  • Firefox: 111 <ph type="x-smartling-placeholder">
  • Safari: 15.2. <ph type="x-smartling-placeholder">

Source

Motivation

Lorsque vous pensez aux fichiers sur votre ordinateur, vous pensez probablement à une hiérarchie de fichiers, c’est-à-dire à des fichiers organisés en dossiers que vous pouvez explorer avec l’explorateur de fichiers de votre système d’exploitation. Par exemple, sous Windows, la liste de tâches d'un utilisateur appelé Tom peut se trouver dans C:\Users\Tom\Documents\ToDo.txt. Dans cet exemple, ToDo.txt est le nom du fichier, et Users, Tom et Documents sont des noms de dossiers. "C:" sous Windows représente le répertoire racine du lecteur.

Méthode traditionnelle de travail avec des fichiers sur le Web

Pour modifier la liste des tâches dans une application Web, procédez comme suit:

  1. L'utilisateur importe le fichier sur un serveur ou l'ouvre sur le client avec <input type="file">.
  2. L'utilisateur effectue ses modifications, puis télécharge le fichier obtenu avec un <a download="ToDo.txt> injecté que vous click() programmatiquez via JavaScript.
  3. Pour ouvrir des dossiers, vous utilisez un attribut spécial dans <input type="file" webkitdirectory> qui, malgré son nom propriétaire, offre une compatibilité pratiquement universelle avec les navigateurs.

Une façon moderne de travailler avec des fichiers sur le Web

Ce flux n'est pas représentatif de la façon dont les utilisateurs pensent de modifier des fichiers et signifie qu'ils se retrouvent avec des copies téléchargées de leurs fichiers d'entrée. L'API File System Access a donc introduit trois méthodes de sélection (showOpenFilePicker(), showSaveFilePicker() et showDirectoryPicker()) qui font exactement ce que leur nom suggère. Ils activent un flux comme suit:

  1. Ouvrez ToDo.txt avec showOpenFilePicker(), puis obtenez un objet FileSystemFileHandle.
  2. À partir de l'objet FileSystemFileHandle, obtenez un File en appelant la méthode getFile() du gestionnaire de fichier.
  3. Modifiez le fichier, puis appelez requestPermission({mode: 'readwrite'}) sur le handle.
  4. Si l'utilisateur accepte la demande d'autorisation, enregistrez les modifications dans le fichier d'origine.
  5. Vous pouvez également appeler showSaveFilePicker() et laisser l'utilisateur choisir un nouveau fichier. (Si l'utilisateur sélectionne un fichier déjà ouvert, son contenu sera écrasé.) Pour les enregistrements répétés, vous pouvez conserver la poignée du fichier. Vous n'avez donc pas besoin d'afficher à nouveau la boîte de dialogue d'enregistrement de fichier.

Restrictions concernant l'utilisation de fichiers sur le Web

Les fichiers et les dossiers accessibles par l'intermédiaire de ces méthodes résident dans ce que l'on appelle un système de fichiers visible par l'utilisateur. Les fichiers enregistrés à partir du Web, et plus particulièrement les fichiers exécutables, sont signalés par la marque Web. Un avertissement supplémentaire peut être affiché dans le système d'exploitation avant l'exécution d'un fichier potentiellement dangereux. Comme fonctionnalité de sécurité supplémentaire, les fichiers obtenus sur le Web sont également protégés par la navigation sécurisée, qui, dans un souci de simplicité et dans le contexte de cet article, est comparable à une analyse antivirus dans le cloud. Lorsque vous écrivez des données dans un fichier à l'aide de l'API File System Access, les écritures ne sont pas sur place, mais utilisent un fichier temporaire. Le fichier lui-même n'est modifié que s'il passe tous ces contrôles de sécurité. Comme vous pouvez l'imaginer, ce travail ralentit les opérations de fichiers, malgré les améliorations appliquées dans la mesure du possible, par exemple sur macOS. Pourtant, chaque appel write() est autonome. Par conséquent, en arrière-plan, il ouvre le fichier, recherche le décalage donné et, enfin, écrit des données.

Fichiers comme base du traitement

Parallèlement, les fichiers sont un excellent moyen d'enregistrer des données. Par exemple, SQLite stocke des bases de données entières dans un seul fichier. Les mipmaps sont également utilisés pour traiter des images. Les mipmaps sont des séquences d'images précalculées et optimisées, chacune représentant une résolution progressivement inférieure de l'image précédente, ce qui accélère de nombreuses opérations comme le zoom. Comment les applications Web peuvent-elles donc bénéficier des avantages des fichiers sans les coûts de performances liés au traitement de fichiers sur le Web ? La réponse est le système de fichiers privé d'origine.

Système de fichiers visible par l'utilisateur et système de fichiers privé d'origine

Contrairement au système de fichiers visible par l'utilisateur parcouru à l'aide de l'explorateur de fichiers du système d'exploitation, avec des fichiers et des dossiers que vous pouvez lire, écrire, déplacer et renommer, le système de fichiers privé d'origine n'est pas destiné à être vu par les utilisateurs. Comme leur nom l'indique, les fichiers et dossiers du système de fichiers privé d'origine sont privés et, plus concrètement, privés au niveau de l'origine d'un site. Découvrez l'origine d'une page en saisissant location.origin dans la console DevTools. Par exemple, l'origine de la page https://developer.chrome.com/articles/ est https://developer.chrome.com (c'est-à-dire que la partie /articles ne fait pas partie de l'origine). Pour en savoir plus sur la théorie des origines, consultez l'article Comprendre un même site et "same-origin". Toutes les pages qui partagent la même origine peuvent voir les mêmes données de système de fichiers privés d'origine. https://developer.chrome.com/docs/extensions/mv3/getstarted/extensions-101/ peut donc voir les mêmes détails que dans l'exemple précédent. Chaque origine a son propre système de fichiers privé d'origine indépendant, ce qui signifie que le système de fichiers privé d'origine de https://developer.chrome.com est complètement différent de celui de https://web.dev, par exemple. Sous Windows, le répertoire racine du système de fichiers visible par l'utilisateur est C:\\. L'équivalent du système de fichiers privé d'origine est un répertoire racine initialement vide pour chaque origine, auquel on accède en appelant la méthode asynchrone. navigator.storage.getDirectory() Pour comparer le système de fichiers visible par l'utilisateur et le système de fichiers privé d'origine, consultez le schéma suivant. Le schéma montre qu'en dehors du répertoire racine, tout le reste est conceptuellement le même, avec une hiérarchie de fichiers et de dossiers que vous pouvez organiser et organiser selon vos besoins en termes de données et de stockage.

Schéma du système de fichiers visible par l&#39;utilisateur et du système de fichiers privé d&#39;origine avec deux exemples de hiérarchies de fichiers Le point d&#39;entrée du système de fichiers visible par l&#39;utilisateur est un disque dur symbolique. Le point d&#39;entrée du système de fichiers privé d&#39;origine appelle la méthode &quot;navigator.storage.getDirectory&quot;.

Caractéristiques du système de fichiers privé d'origine

Tout comme les autres mécanismes de stockage du navigateur (par exemple, localStorage ou IndexedDB), le système de fichiers privé d'origine est soumis aux restrictions de quota du navigateur. Lorsqu'un utilisateur efface toutes les données de navigation ou toutes les données de site, le système de fichiers d'origine est également supprimé. Appelez navigator.storage.estimate() et, dans l'objet de réponse obtenu, consultez l'entrée usage pour voir la quantité d'espace de stockage déjà utilisée par votre application, qui est répartie par mécanisme de stockage dans l'objet usageDetails, où vous souhaitez examiner spécifiquement l'entrée fileSystem. Étant donné que le système de fichiers privé d'origine n'est pas visible par l'utilisateur, il n'y a aucune invite d'autorisation ni vérification de la navigation sécurisée.

Accéder au répertoire racine

Pour accéder au répertoire racine, exécutez la commande suivante. Vous vous retrouvez avec un handle de répertoire vide, plus précisément, un FileSystemDirectoryHandle.

const opfsRoot = await navigator.storage.getDirectory();
// A FileSystemDirectoryHandle whose type is "directory"
// and whose name is "".
console.log(opfsRoot);

Thread principal ou Web worker

Il existe deux façons d'utiliser le système de fichiers privé d'origine: dans le thread principal ou dans un web worker. Les nœuds de calcul Web ne peuvent pas bloquer le thread principal, ce qui signifie que dans ce contexte, les API peuvent être synchrones, un modèle généralement interdit sur le thread principal. Les API synchrones peuvent être plus rapides, car elles évitent de devoir gérer les promesses. De plus, les opérations de fichier sont généralement synchrones dans des langages tels que C, qui peuvent être compilés en WebAssembly.

// This is synchronous C code.
FILE *f;
f = fopen("example.txt", "w+");
fputs("Some text\n", f);
fclose(f);

Si vous avez besoin d'opérations de fichiers les plus rapides possible ou si vous gérez WebAssembly, passez directement à la section Utiliser le système de fichiers d'origine dans un nœud de calcul Web. Sinon, poursuivez votre lecture.

Utiliser le système de fichiers privé d'origine sur le thread principal

Créer des fichiers et des dossiers

Une fois que vous disposez d'un dossier racine, créez des fichiers et des dossiers à l'aide des méthodes getFileHandle() et getDirectoryHandle(), respectivement. Si vous transmettez {create: true}, le fichier ou le dossier est créé s'il n'existe pas. Créez une hiérarchie de fichiers en appelant ces fonctions en utilisant un répertoire nouvellement créé comme point de départ.

const fileHandle = await opfsRoot
    .getFileHandle('my first file', {create: true});
const directoryHandle = await opfsRoot
    .getDirectoryHandle('my first folder', {create: true});
const nestedFileHandle = await directoryHandle
    .getFileHandle('my first nested file', {create: true});
const nestedDirectoryHandle = await directoryHandle
    .getDirectoryHandle('my first nested folder', {create: true});

Hiérarchie de fichiers résultant de l&#39;exemple de code précédent.

Accéder aux fichiers et dossiers existants

Si vous connaissez leur nom, accédez aux fichiers et aux dossiers créés précédemment en appelant les méthodes getFileHandle() ou getDirectoryHandle(), en transmettant le nom du fichier ou du dossier.

const existingFileHandle = await opfsRoot.getFileHandle('my first file');
const existingDirectoryHandle = await opfsRoot
    .getDirectoryHandle('my first folder');

Obtenir le fichier associé à un handle de fichier pour la lecture

Un FileSystemFileHandle représente un fichier dans le système de fichiers. Pour obtenir le File associé, utilisez la méthode getFile(). Un objet File est un type spécifique de Blob et peut être utilisé dans n'importe quel contexte possible par un Blob. En particulier, FileReader, URL.createObjectURL(), createImageBitmap() et XMLHttpRequest.send() acceptent à la fois Blobs et Files. Si vous le souhaitez, obtenir un File à partir d'un FileSystemFileHandle "sans frais" les données, ce qui vous permet d'y accéder et de les rendre accessibles au système de fichiers visible par l'utilisateur.

const file = await fileHandle.getFile();
console.log(await file.text());

Écrire dans un fichier par flux

Diffusez des données dans un fichier en appelant createWritable(), qui crée un FileSystemWritableFileStream auquel vous pouvez ensuite write() le contenu. À la fin, vous devez close() le flux.

const contents = 'Some text';
// Get a writable stream.
const writable = await fileHandle.createWritable();
// Write the contents of the file to the stream.
await writable.write(contents);
// Close the stream, which persists the contents.
await writable.close();

Supprimer des fichiers et des dossiers

Supprimez des fichiers et des dossiers en appelant la méthode remove() spécifique de leur gestionnaire de fichier ou de répertoire. Pour supprimer un dossier y compris tous ses sous-dossiers, transmettez l'option {recursive: true}.

await fileHandle.remove();
await directoryHandle.remove({recursive: true});

Si vous connaissez le nom du fichier ou du dossier à supprimer dans un répertoire, utilisez la méthode removeEntry().

directoryHandle.removeEntry('my first nested file');

Déplacer et renommer des fichiers et des dossiers

Renommez et déplacez des fichiers et des dossiers à l'aide de la méthode move(). Le déplacement et le changement de nom peuvent se faire ensemble ou de manière isolée.

// Rename a file.
await fileHandle.move('my first renamed file');
// Move a file to another directory.
await fileHandle.move(nestedDirectoryHandle);
// Move a file to another directory and rename it.
await fileHandle
    .move(nestedDirectoryHandle, 'my first renamed and now nested file');

Résoudre le chemin d'accès à un fichier ou à un dossier

Pour savoir où se trouve un fichier ou un dossier donné par rapport à un répertoire de référence, utilisez la méthode resolve(), en lui transmettant un FileSystemHandle comme argument. Pour obtenir le chemin d'accès complet d'un fichier ou d'un dossier dans le système de fichiers privé d'origine, utilisez le répertoire racine comme répertoire de référence obtenu via navigator.storage.getDirectory().

const relativePath = await opfsRoot.resolve(nestedDirectoryHandle);
// `relativePath` is `['my first folder', 'my first nested folder']`.

Vérifier si deux poignées de fichier ou de dossier pointent vers le même fichier ou dossier

Parfois, vous avez deux identifiants et vous ne savez pas s'ils pointent vers le même fichier ou le même dossier. Pour le vérifier, utilisez la méthode isSameEntry().

fileHandle.isSameEntry(nestedFileHandle);
// Returns `false`.

Lister le contenu d'un dossier

FileSystemDirectoryHandle est un itérateur asynchrone sur lequel vous effectuez une itération avec une boucle for await…of. En tant qu'itérateur asynchrone, il accepte également les méthodes entries(), values() et keys(), parmi lesquelles vous pouvez choisir en fonction des informations dont vous avez besoin:

for await (let [name, handle] of directoryHandle) {}
for await (let [name, handle] of directoryHandle.entries()) {}
for await (let handle of directoryHandle.values()) {}
for await (let name of directoryHandle.keys()) {}

Afficher de manière récursive le contenu d'un dossier et tous ses sous-dossiers

Il est facile de se tromper lorsque vous traitez des boucles et des fonctions asynchrones associées à une récursion. La fonction ci-dessous peut servir de point de départ pour répertorier le contenu d'un dossier et de tous ses sous-dossiers, y compris l'ensemble des fichiers et leur taille. Si vous n'avez pas besoin de la taille du fichier, vous pouvez simplifier la fonction en lui indiquant directoryEntryPromises.push, non pas en transférant la promesse handle.getFile(), mais directement dans handle.

  const getDirectoryEntriesRecursive = async (
    directoryHandle,
    relativePath = '.',
  ) => {
    const fileHandles = [];
    const directoryHandles = [];
    const entries = {};
    // Get an iterator of the files and folders in the directory.
    const directoryIterator = directoryHandle.values();
    const directoryEntryPromises = [];
    for await (const handle of directoryIterator) {
      const nestedPath = `${relativePath}/${handle.name}`;
      if (handle.kind === 'file') {
        fileHandles.push({ handle, nestedPath });
        directoryEntryPromises.push(
          handle.getFile().then((file) => {
            return {
              name: handle.name,
              kind: handle.kind,
              size: file.size,
              type: file.type,
              lastModified: file.lastModified,
              relativePath: nestedPath,
              handle
            };
          }),
        );
      } else if (handle.kind === 'directory') {
        directoryHandles.push({ handle, nestedPath });
        directoryEntryPromises.push(
          (async () => {
            return {
              name: handle.name,
              kind: handle.kind,
              relativePath: nestedPath,
              entries:
                  await getDirectoryEntriesRecursive(handle, nestedPath),
              handle,
            };
          })(),
        );
      }
    }
    const directoryEntries = await Promise.all(directoryEntryPromises);
    directoryEntries.forEach((directoryEntry) => {
      entries[directoryEntry.name] = directoryEntry;
    });
    return entries;
  };

Utiliser le système de fichiers privé d'origine dans un Web Worker

Comme indiqué précédemment, les Web workers ne peuvent pas bloquer le thread principal. C'est pourquoi, dans ce contexte, les méthodes synchrones sont autorisées.

Obtenir un handle d'accès synchrone

Le point d'entrée des opérations de fichier les plus rapides est un FileSystemSyncAccessHandle, obtenu à partir d'un FileSystemFileHandle standard en appelant createSyncAccessHandle().

const fileHandle = await opfsRoot
    .getFileHandle('my highspeed file.txt', {create: true});
const syncAccessHandle = await fileHandle.createSyncAccessHandle();

Méthodes de fichiers sur place synchrones

Une fois que vous disposez d'un handle d'accès synchrone, vous avez accès à des méthodes de fichiers sur place rapides et toutes synchrones.

  • getSize(): renvoie la taille du fichier en octets.
  • write(): écrit le contenu d'un tampon dans le fichier, éventuellement à un décalage donné, et renvoie le nombre d'octets écrits. La vérification du nombre d'octets écrits renvoyés permet aux appelants de détecter et de gérer les erreurs et les écritures partielles.
  • read(): lit le contenu du fichier dans un tampon, éventuellement à un décalage donné.
  • truncate(): redimensionne le fichier à la taille donnée.
  • flush(): vérifie que le contenu du fichier contient toutes les modifications effectuées via write().
  • close(): ferme le handle d'accès.

Voici un exemple qui utilise toutes les méthodes mentionnées ci-dessus.

const opfsRoot = await navigator.storage.getDirectory();
const fileHandle = await opfsRoot.getFileHandle('fast', {create: true});
const accessHandle = await fileHandle.createSyncAccessHandle();

const textEncoder = new TextEncoder();
const textDecoder = new TextDecoder();

// Initialize this variable for the size of the file.
let size;
// The current size of the file, initially `0`.
size = accessHandle.getSize();
// Encode content to write to the file.
const content = textEncoder.encode('Some text');
// Write the content at the beginning of the file.
accessHandle.write(content, {at: size});
// Flush the changes.
accessHandle.flush();
// The current size of the file, now `9` (the length of "Some text").
size = accessHandle.getSize();

// Encode more content to write to the file.
const moreContent = textEncoder.encode('More content');
// Write the content at the end of the file.
accessHandle.write(moreContent, {at: size});
// Flush the changes.
accessHandle.flush();
// The current size of the file, now `21` (the length of
// "Some textMore content").
size = accessHandle.getSize();

// Prepare a data view of the length of the file.
const dataView = new DataView(new ArrayBuffer(size));

// Read the entire file into the data view.
accessHandle.read(dataView);
// Logs `"Some textMore content"`.
console.log(textDecoder.decode(dataView));

// Read starting at offset 9 into the data view.
accessHandle.read(dataView, {at: 9});
// Logs `"More content"`.
console.log(textDecoder.decode(dataView));

// Truncate the file after 4 bytes.
accessHandle.truncate(4);

Copier un fichier du système de fichiers privé d'origine vers le système de fichiers visible par l'utilisateur

Comme indiqué ci-dessus, il n'est pas possible de déplacer des fichiers depuis le système de fichiers privé d'origine vers le système de fichiers visible par l'utilisateur, mais vous pouvez copier des fichiers. Comme showSaveFilePicker() n'est exposé que sur le thread principal, mais pas dans le thread de nœud de calcul, veillez à y exécuter le code.

// On the main thread, not in the Worker. This assumes
// `fileHandle` is the `FileSystemFileHandle` you obtained
// the `FileSystemSyncAccessHandle` from in the Worker
// thread. Be sure to close the file in the Worker thread first.
const fileHandle = await opfsRoot.getFileHandle('fast');
try {
  // Obtain a file handle to a new file in the user-visible file system
  // with the same name as the file in the origin private file system.
  const saveHandle = await showSaveFilePicker({
    suggestedName: fileHandle.name || ''
  });
  const writable = await saveHandle.createWritable();
  await writable.write(await fileHandle.getFile());
  await writable.close();
} catch (err) {
  console.error(err.name, err.message);
}

Déboguer le système de fichiers privé d'origine

En attendant que les outils de développement soient pris en charge (voir crbug/1284595), utilisez l'extension Chrome OPFS Explorer pour déboguer le système de fichiers privé d'origine. La capture d'écran de la section Créer des fichiers et des dossiers ci-dessus provient directement de l'extension.

Extension Chrome DevTools pour OPFS Explorer dans le Chrome Web Store.

Une fois l'extension installée, ouvrez les outils pour les développeurs Chrome et sélectionnez l'onglet OPFS Explorer. Vous pouvez alors inspecter la hiérarchie des fichiers. Enregistrez les fichiers du système de fichiers privé d'origine dans le système de fichiers visible par l'utilisateur en cliquant sur le nom du fichier, puis supprimez les fichiers et les dossiers en cliquant sur l'icône de la corbeille.

Démo

Découvrez le système de fichiers privé d'origine en action (si vous installez l'extension OPFS Explorer) dans une démonstration qui l'utilise comme backend pour une base de données SQLite compilée sur WebAssembly. Veillez à consulter le code source sur Glitch. Notez que la version intégrée ci-dessous n'utilise pas le backend du système de fichiers privé d'origine (car l'iFrame est multi-origine). Toutefois, c'est le cas lorsque vous ouvrez la version de démonstration dans un onglet distinct.

Conclusions

Le système de fichiers privés d'origine, tel que spécifié par le WHATWG, a façonné notre façon d'utiliser les fichiers et d'interagir avec eux sur le Web. Elle a permis de créer de nouveaux cas d'utilisation impossibles à atteindre avec un système de fichiers visible par l'utilisateur. Tous les principaux fournisseurs de navigateurs (Apple, Mozilla et Google) participent à l'intégration et partagent une vision commune. Le développement du système de fichiers privés d'origine est un travail de grande collaboration, et les commentaires des développeurs et des utilisateurs sont essentiels à sa progression. À mesure que nous continuons d'affiner et d'améliorer la norme, n'hésitez pas à nous faire part de vos commentaires concernant le dépôt whatwg/fs, sous forme de problèmes ou de demandes d'extraction.

Remerciements

Cet article a été lu par Austin Sully, Etienne Noël et Rachel Andrew. Image héros de Christina Rumpf sur Unsplash.