Système de fichiers privé d'origine

Cette norme introduit un système de fichiers d'origine privé (OPFS) comme point de terminaison de stockage privé à l'origine de la page et non visible par l'utilisateur, qui fournit un accès facultatif à un type spécial de fichier hautement optimisé pour les performances.

Prise en charge des navigateurs

Le système de fichiers d'origine privé est compatible avec les navigateurs modernes et est standardisé par le WHATWG (Web Hypertext Application Technology Working Group) dans la norme File System Living Standard.

Navigateurs pris en charge

  • 86
  • 86
  • 111
  • 15.2

Source

Motivation

Lorsque vous pensez aux fichiers sur votre ordinateur, vous pensez probablement à une hiérarchie de fichiers: 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, pour un utilisateur appelé Tom, la liste de tâches 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.

Manière traditionnelle de travailler 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 une <a download="ToDo.txt> injectée que vous click() de façon automatisée via JavaScript.
  3. Pour ouvrir des dossiers, utilisez un attribut spécial dans <input type="file" webkitdirectory> qui, malgré son nom propriétaire, est compatible avec un navigateur pratiquement universel.

Travailler de façon moderne avec des fichiers sur le Web

Ce processus n'est pas représentatif de la façon dont les utilisateurs envisagent la modification des fichiers. 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élection (showOpenFilePicker(), showSaveFilePicker() et showDirectoryPicker()) qui fonctionnent exactement comme leur nom l'indique. Ils permettent d'activer 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 qu'il a déjà ouvert, son contenu sera écrasé. Pour les enregistrements répétés, vous pouvez conserver le handle de fichier afin de ne pas avoir à afficher à nouveau la boîte de dialogue d'enregistrement de fichier.

Restrictions concernant l'utilisation de fichiers sur le Web

Les fichiers et dossiers accessibles via ces méthodes résident dans ce que l'on peut appeler le système de fichiers visible par l'utilisateur. Les fichiers enregistrés sur le Web, ainsi que les fichiers exécutables en particulier, sont marqués de la marque du Web. Le système d'exploitation peut donc afficher un avertissement supplémentaire 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. Par souci de simplicité et dans le contexte de cet article, cette fonctionnalité 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 les contrôles de sécurité. Comme vous pouvez l'imaginer, ce travail ralentit les opérations sur les fichiers, malgré les améliorations appliquées dans la mesure du possible (par exemple, sur macOS). Toujours, chaque appel write() est autonome. En arrière-plan, il ouvre donc le fichier, recherche le décalage donné, puis écrit des données.

Fichiers comme base du traitement

En même temps, les fichiers constituent un excellent moyen d’enregistrer des données. Par exemple, SQLite stocke des bases de données entières dans un seul fichier. Autre exemple : les mipmaps sont utilisés dans le traitement des images. Les mipmaps sont des séquences d'images optimisées et précalculées. Chacune de ces séquences correspond à une résolution progressivement inférieure de la précédente, ce qui accélère de nombreuses opérations, comme le zoom. Comment les applications Web peuvent-elles tirer parti des avantages des fichiers sans les coûts de performances du traitement des fichiers en ligne ? La réponse est le système de fichiers d'origine privé.

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 naviguant à 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 d'origine privé n'est pas destiné à être vu par les utilisateurs. Comme son nom l'indique, les fichiers et dossiers du système de fichiers privés d'origine sont privés et, plus concrètement, réservés à l'origine d'un site. Découvrez l'origine d'une page en saisissant location.origin dans la console des outils de développement. 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 les termes "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ées 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 possède son propre système de fichiers privés d'origine indépendant, ce qui signifie que le système de fichiers d'origine privé de https://developer.chrome.com est complètement différent de celui d'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 d'origine privé est un répertoire racine initialement vide pour chaque origine accessible 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'à l'exception du répertoire racine, tout le reste est conceptuellement identique, avec une hiérarchie de fichiers et de dossiers que vous pouvez 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 d&#39;origine privé avec deux hiérarchies de fichiers d&#39;exemple. 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 est l&#39;appel de la méthode &quot;navigator.storage.getDirectory&quot;.

Spécificités du système de fichiers d'origine privé

Tout comme les autres mécanismes de stockage du navigateur (par exemple, localStorage ou IndexedDB), le système de fichiers d'origine privé 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 d'origine privé est également supprimé. Appelez navigator.storage.estimate(). 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és d'origine n'est pas visible par l'utilisateur, il n'y a pas d'invite d'autorisation ni de 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, et 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 nœud de calcul Web

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 Web Workers ne peuvent pas bloquer le thread principal. Dans ce contexte, les API peuvent donc être synchrones, un modèle généralement non autorisé sur le thread principal. Les API synchrones peuvent être plus rapides, car elles évitent d'avoir à gérer des promesses, et les opérations de fichiers sont généralement synchrones dans des langages tels que C, qui peuvent être compilés dans 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 d'origine privé dans un worker 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 sera créé s'il n'existe pas. Créez une hiérarchie de fichiers en appelant ces fonctions en utilisant un nouveau répertoire 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() et 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 sur 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. Plus spécifiquement, FileReader, URL.createObjectURL(), createImageBitmap() et XMLHttpRequest.send() acceptent à la fois Blobs et Files. Si c'est le cas, l'obtention d'un File à partir d'un FileSystemFileHandle "libère" les données, afin que vous puissiez y accéder et 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

Transférez des données en flux continu dans un fichier en appelant createWritable(), qui crée un FileSystemWritableFileStream dans lequel vous pouvez 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 contenant tous les 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 produire 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 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 en tant qu'argument. Pour obtenir le chemin d'accès complet d'un fichier ou d'un dossier dans le système de fichiers d'origine privé, 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érifiez si deux poignées de fichiers ou de dossiers 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 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 que vous itérez avec une boucle for await…of. En tant qu'itérateur asynchrone, il est également compatible avec 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()) {}

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

Il est facile de se tromper lors de la gestion des boucles et fonctions asynchrones associées à la 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. Si vous n'avez pas besoin de la taille des fichiers, vous pouvez simplifier la fonction en indiquant directoryEntryPromises.push (et non en transmettant directement 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 d'origine privé dans un worker Web

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 vers les 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 rapides sur place 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(): s'assure que le contenu du fichier contient toutes les modifications effectuées via write().
  • close(): ferme la poignée 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 d'origine privé 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 copier des fichiers. Étant donné que 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 la prise en charge intégrée des outils de développement soit 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 de la section Créer des fichiers et des dossiers ci-dessus provient directement de l'extension.

L&#39;extension OPFS Explorer Chrome DevTools 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 (Explorateur OPFS), et vous pouvez 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 en forme de corbeille.

Démonstration

Découvrez le système de fichiers d'origine privé 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 dans WebAssembly. Pensez à 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). En revanche, lorsque vous ouvrez la version de démonstration dans un onglet distinct, c'est le cas.

Conclusions

Le système de fichiers privés d'origine, comme spécifié par le WHATWG, a façonné la façon dont nous utilisons les fichiers sur le Web et interagissons avec eux. Elle a permis de développer 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 présents et partagent une vision commune. Le développement du système de fichiers d'origine privé est un travail collaboratif, et les commentaires des développeurs et des utilisateurs sont essentiels à son évolution. Nous continuons d'affiner et d'améliorer la norme, mais n'hésitez pas à nous faire part de vos commentaires sur 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, Étienne Noël et Rachel Andrew. Image principale de Christina Rumpf sur Unsplash.