Système de fichiers privé d'origine

La norme de système de fichiers introduit un système de fichiers privé d'origine (OPFS) en tant que point de terminaison de stockage privé de 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é d'origine est compatible avec les navigateurs modernes et est standardisé par le WHATWG (Web Hypertext Application Technology Working Group) dans le File System Living Standard.

Navigateurs pris en charge

  • Chrome: 86.
  • Edge: 86.
  • Firefox: 111.
  • Safari: 15.2

Source

Motivation

Lorsque vous pensez aux fichiers de votre ordinateur, vous pensez probablement à une hiérarchie de fichiers: des fichiers organisés dans des 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 de fichier, et Users, Tom et Documents sont des noms de dossiers. Sous Windows, "C:" représente le répertoire racine du disque.

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

Pour modifier la liste de 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 apporte ses modifications, puis télécharge le fichier obtenu avec un <a download="ToDo.txt> injecté que vous click() de manière programmatique via JavaScript.
  3. Pour ouvrir des dossiers, vous utilisez un attribut spécial dans <input type="file" webkitdirectory>, qui, malgré son nom propriétaire, est compatible avec pratiquement tous les navigateurs.

Une méthode moderne de travailler avec des fichiers sur le Web

Ce parcours ne reflète pas la façon dont les utilisateurs pensent à modifier des fichiers. Par conséquent, ils se retrouvent avec des copies téléchargées de leurs fichiers d'entrée. Par conséquent, l'API File System Access a introduit trois méthodes de sélecteur : showOpenFilePicker(), showSaveFilePicker() et showDirectoryPicker(), qui font exactement ce que leur nom suggère. Elles permettent un flux comme suit:

  1. Ouvrez ToDo.txt avec showOpenFilePicker() et obtenez un objet FileSystemFileHandle.
  2. À partir de l'objet FileSystemFileHandle, obtenez un File en appelant la méthode getFile() du gestionnaire de fichiers.
  3. Modifiez le fichier, puis appelez requestPermission({mode: 'readwrite'}) sur la poignée.
  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 le gestionnaire de fichiers afin de ne pas avoir à afficher à nouveau la boîte de dialogue d'enregistrement du fichier.

Restrictions liées à l'utilisation de fichiers sur le Web

Les fichiers et les dossiers accessibles via ces méthodes se trouvent dans ce que l'on peut appeler le système de fichiers visible par l'utilisateur. Les fichiers enregistrés à partir du Web, et en particulier les fichiers exécutables, sont marqués du signe du Web. Le système d'exploitation peut donc afficher un avertissement supplémentaire avant l'exécution d'un fichier potentiellement dangereux. Pour renforcer la sécurité, les fichiers obtenus sur le Web sont également protégés par la navigation sécurisée, que vous pouvez considérer comme une analyse antivirus dans le cloud, par souci de simplicité et dans le contexte de cet article. Lorsque vous écrivez des données dans un fichier à l'aide de l'API File System Access, les écritures ne sont pas in situ, mais utilisent un fichier temporaire. Le fichier lui-même n'est pas modifié, sauf s'il passe toutes ces vérifications de sécurité. Comme vous pouvez l'imaginer, ce travail rend les opérations de fichiers relativement lentes, malgré les améliorations appliquées dans la mesure du possible, par exemple sous macOS. Chaque appel write() est autonome. Par conséquent, en interne, il ouvre le fichier, recherche le décalage donné et écrit finalement les données.

Les fichiers comme base du traitement

En même temps, 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 utilisés dans le traitement des images sont un autre exemple. Les mipmaps sont des séquences d'images précalculées et optimisées, chacune étant une représentation de résolution progressivement inférieure à la précédente, ce qui accélère de nombreuses opérations telles que le zoom. Comment les applications Web peuvent-elles bénéficier des avantages des fichiers, sans les coûts de performances du 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 par rapport au système de fichiers privé d'origine

Contrairement au système de fichiers visible par l'utilisateur qui est 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 son nom l'indique, les fichiers et les dossiers du système de fichiers privé d'origine sont privés, et plus précisément, privés 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 n'est pas l'origine). Pour en savoir plus sur la théorie des origines, consultez Comprendre les concepts "same-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é d'origine. https://developer.chrome.com/docs/extensions/mv3/getstarted/extensions-101/ peut donc voir les mêmes informations que dans l'exemple précédent. Chaque origine possède son propre système de fichiers privé indépendant, ce qui signifie que le système de fichiers privé de l'origine https://developer.chrome.com est complètement distinct de celui de, par exemple, https://web.dev. 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 par 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 diagramme suivant. Le diagramme montre que, à l'exception du répertoire racine, tout le reste est conceptuellement identique, avec une hiérarchie de fichiers et de dossiers à organiser et à organiser selon vos besoins en données et en 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, tandis que le point d&#39;entrée du système de fichiers privé d&#39;origine appelle la méthode &quot;navigator.storage.getDirectory&quot;.

Spécificités 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 du site, le système de fichiers privé d'origine est également supprimé. Appelez navigator.storage.estimate() et dans l'objet de réponse qui en résulte, consultez l'entrée usage pour connaître la quantité de stockage que votre application consomme déjà, qui est ventilée par mécanisme de stockage dans l'objet usageDetails, où vous devez examiner spécifiquement l'entrée fileSystem. Comme l'utilisateur ne voit pas le système de fichiers privé d'origine, aucune invite d'autorisation ni aucune vérification du navigateur sécurisé ne s'affichent.

Accéder au répertoire racine

Pour accéder au répertoire racine, exécutez la commande suivante. Vous obtenez un descripteur 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: sur le thread principal ou dans un Web Worker. Les nœuds de calcul Web ne peuvent pas bloquer le thread principal. Par conséquent, 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 d'avoir à gérer des promesses. De plus, les opérations de fichiers 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 des opérations de fichiers les plus rapides possibles ou si vous utilisez WebAssembly, passez à la section Utiliser le système de fichiers privé d'origine dans un Web Worker. Sinon, vous pouvez continuer à lire.

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

Créer des fichiers et des dossiers

Une fois que vous avez 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 sera 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 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 descripteur de fichier pour la lecture

Un FileSystemFileHandle représente un fichier dans le système de fichiers. Pour obtenir l'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 où un Blob peut l'être. En particulier, FileReader, URL.createObjectURL(), createImageBitmap() et XMLHttpRequest.send() acceptent à la fois Blobs et Files. En d'autres termes, obtenir un File à partir d'un FileSystemFileHandle "libère" les données afin que vous puissiez y accéder et les mettre à la disposition du système de fichiers visible par l'utilisateur.

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

Écrire dans un fichier en streaming

Transmettez des données en flux dans un fichier en appelant createWritable(), ce qui crée un FileSystemWritableFileStream auquel vous write() ensuite 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 poignée 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, vous pouvez également utiliser 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 renommage peuvent être effectués ensemble ou séparément.

// 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 d'un fichier ou d'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 gestionnaires de fichiers ou de dossiers pointent vers le même fichier ou dossier

Il arrive que vous disposiez de deux poignées et que vous ne sachiez pas si elles pointent vers le même fichier ou le même dossier. Pour vérifier si c'est le cas, utilisez la méthode isSameEntry().

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

Lister le contenu d'un dossier

FileSystemDirectoryHandle est un itérateur asynchrone que vous itérez à l'aide d'une boucle for await…of. En tant qu'itérateur asynchrone, il est également compatible avec les méthodes entries(), values() et keys(), que 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()) {}

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

Il est facile de se tromper lorsqu'on gère des boucles et des fonctions asynchrones associées à une récursion. La fonction ci-dessous peut servir de point de départ pour lister le contenu d'un dossier et de tous ses sous-dossiers, y compris tous les fichiers et leurs tailles. Vous pouvez simplifier la fonction si vous n'avez pas besoin des tailles de fichier. À la place de directoryEntryPromises.push, ne transmettez pas la promesse handle.getFile(), mais directement 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é de l'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 gestionnaire d'accès synchrone

Le point d'entrée des opérations de fichiers les plus rapides possibles 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 in situ synchrones

Une fois que vous disposez d'un gestionnaire d'accès synchrone, vous pouvez accéder à des méthodes de fichiers rapides en place, qui sont 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. Vérifier le nombre d'octets écrits 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 indiquée.
  • flush(): garantit que le contenu du fichier contient toutes les modifications effectuées via write().
  • close(): ferme le gestionnaire 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 du système de fichiers privé d'origine vers le système de fichiers visible par l'utilisateur, mais vous pouvez les copier. Étant donné que showSaveFilePicker() n'est exposé que sur le thread principal, mais pas sur le thread de travail, 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

Tant que la prise en charge intégrée de DevTools n'est pas ajoutée (voir crbug/1284595), utilisez l'extension Chrome OPFS Explorer pour déboguer le système de fichiers privé d'origine. La capture d'écran ci-dessus de la section Créer des fichiers et des dossiers est directement tirée de l'extension.

Extension des outils pour les développeurs Chrome OPFS Explorer sur le Chrome Web Store.

Après avoir installé l'extension, ouvrez les outils pour les développeurs Chrome, sélectionnez l'onglet OPFS Explorer, puis inspectez la hiérarchie des fichiers. Enregistrez des fichiers du système de fichiers privé d'origine dans le système de fichiers visible par l'utilisateur en cliquant sur le nom de fichier, et supprimez des fichiers et des dossiers en cliquant sur l'icône en forme de 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émo qui l'utilise comme backend pour une base de données SQLite compilée en WebAssembly. N'oubliez pas de 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), mais que c'est le cas lorsque vous ouvrez la démonstration dans un onglet distinct.

Conclusions

Le système de fichiers privé d'origine, tel que spécifié par le WHATWG, a façonné la façon dont nous utilisons et interagissons avec les fichiers sur le Web. Il a permis de créer de nouveaux cas d'utilisation qui étaient impossibles à réaliser avec le système de fichiers visible par l'utilisateur. Tous les principaux fournisseurs de navigateurs (Apple, Mozilla et Google) sont impliqués et partagent une vision commune. Le développement du système de fichiers privés d'origine est un effort collaboratif, et les commentaires des développeurs et des utilisateurs sont essentiels à sa progression. Nous continuons à affiner et à améliorer la norme. Nous vous invitons à nous faire part de vos commentaires sur le dépôt whatwg/fs sous la forme de problèmes ou de requêtes pull.

Remerciements

Cet article a été relu par Austin Sully, Étienne Noël et Rachel Andrew. Image héros par Christina Rumpf sur Unsplash.