Système de fichiers privé d'origine

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

Browser Support

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

Source

Motivation

Lorsque vous pensez aux fichiers sur 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, pour un utilisateur nommé Tom, sa liste de tâches 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 classique d'utilisation des fichiers sur le Web

Pour modifier la liste de tâches dans une application Web, voici le flux habituel :

  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 résultant avec un injecté <a download="ToDo.txt> que vous click() par programmation avec 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.

Méthode moderne d'utilisation des fichiers sur le Web

Ce flux ne correspond pas à la façon dont les utilisateurs pensent à modifier des fichiers, et signifie que les utilisateurs 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 :

Elles permettent un flux comme suit :

  1. Ouvrez ToDo.txt avec showOpenFilePicker() et obtenez un FileSystemFileHandle objet.
  2. À partir de l'objet FileSystemFileHandle, obtenez un File en appelant la méthode getFile() du descripteur de fichier.
  3. Modifiez le fichier, puis appelez requestPermission({mode: 'readwrite'}) sur le descripteur.
  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 choisit un fichier précédemment ouvert, son contenu sera écrasé.) Pour les enregistrements répétés, vous pouvez conserver le descripteur de fichier afin de ne pas avoir à afficher à nouveau la boîte de dialogue d'enregistrement de fichier.

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

Les fichiers et dossiers accessibles avec 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 plus particulièrement les fichiers exécutables, sont marqués avec la marque du Web, Un avertissement supplémentaire peut donc s'afficher 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 à partir du Web sont également protégés par la navigation sécurisée, que vous pouvez considérer comme une analyse antivirus basée sur le cloud, par souci de simplicité et dans le contexte de ce document. Lorsque vous écrivez des données dans un fichier à l'aide de l'API File System Access, les écritures ne sont pas effectuées sur place, mais utilisent un fichier temporaire. Le fichier lui-même n'est pas modifié, sauf s'il réussit tous ces contrôles de sécurité.

Cette opération rend les opérations sur les fichiers relativement lentes, malgré les améliorations appliquées lorsque cela est possible, par exemple, sur macOS. Toutefois, chaque write() appel est autonome. Il ouvre donc le fichier, recherche le décalage donné et écrit enfin 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 en 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 comme le zoom. Alors, comment les applications Web peuvent-elles bénéficier des avantages des fichiers, mais sans les coûts de performances du traitement des 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, 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 précisément, privés à 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. 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. Par conséquent, https://developer.chrome.com/docs/extensions/mv3/getstarted/extensions-101/ peut voir les mêmes détails que l'exemple précédent. Chaque origine possède 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 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 pour le système de fichiers privé d'origine est un répertoire racine initialement vide par origine, auquel vous accédez 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 à organiser et à disposer selon vos besoins en matière de données et de stockage.

Schéma du système de fichiers visible par l'utilisateur et du système de fichiers privé d'origine avec deux hiérarchies de fichiers exemplaires.
Le point d'entrée du système de fichiers visible par l'utilisateur est un disque dur symbolique, tandis que le point d'entrée du système de fichiers privé d'origine est l'appel de la méthode navigator.storage.getDirectory.

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

Tout comme les autres mécanismes de stockage dans le 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 résultant, consultez l' usage entrée pour voir la quantité de stockage déjà consommée par votre application, qui est répartie par mécanisme de stockage dans l'objet usageDetails , où vous devez examiner l'entrée fileSystem en particulier. Étant donné que le système de fichiers privé 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 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 Web Workers 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 d'avoir à gérer les promesses, et les opérations sur les fichiers sont généralement synchrones dans des langages comme 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 sur les fichiers les plus rapides possible ou si vous utilisez WebAssembly, passez à Utiliser le système de fichiers privé d'origine dans un Web Worker.

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 getFileHandle() et des getDirectoryHandle() méthodes, 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 à l'aide d'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'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 le associé File, utilisez la méthode getFile(). Un objet File est un type spécifique de Blob et peut être utilisé dans n'importe quel contexte dans lequel un Blob peut l'être.

En particulier, FileReader, URL.createObjectURL(), createImageBitmap(), et XMLHttpRequest.send() acceptent à la fois les Blobs et les Files. L'obtention d'un File à partir d'un FileSystemFileHandle "libère" les données, ce qui vous permet d'y accéder et de les rendre disponibles pour le système de fichiers visible par l'utilisateur.

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

Écrire dans un fichier par streaming

Diffusez des données dans un fichier en appelant createWritable() qui crée un FileSystemWritableFileStream dans lequel vous 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 descripteur de fichier ou de répertoire. Pour supprimer un dossier, y compris tous les sous-dossiers, transmettez l' {recursive: true} option.

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

Vous pouvez également utiliser la méthode removeEntry() si vous connaissez le nom du fichier ou du dossier à supprimer dans un répertoire.

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 move() méthode. Le déplacement et le renommage 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 resolve() méthode, 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 à l'aide de navigator.storage.getDirectory().

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

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

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

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

Lister le contenu d'un dossier

FileSystemDirectoryHandle est un itérateur asynchrone sur lequel vous itérez avec une for await... of boucle. En tant qu'itérateur asynchrone, il est également compatible avec les entries(), les values(), et les keys() méthodes, 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 les sous-dossiers

Il est facile de se tromper lorsqu'il s'agit de gérer des boucles et des fonctions asynchrones associées à la récursivité. La fonction suivante 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 leur taille. Vous pouvez simplifier la fonction si vous n'avez pas besoin de la taille des fichiers en ne transmettant pas la promesse à l'endroit où il est indiqué directoryEntryPromises.push, mais en transmettant directement le handle.handle.getFile()

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 les méthodes synchrones sont autorisées dans ce contexte.

Obtenir un descripteur d'accès synchrone

Le point d'entrée des opérations sur les fichiers les plus rapides possible 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 synchrones sur place

Une fois que vous disposez d'un descripteur d'accès synchrone, vous avez accès à des méthodes de fichiers rapides sur 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. La vérification du nombre d'octets écrits renvoyé 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(): s'assure que le contenu du fichier contient toutes les modifications apportées via write().
  • close(): ferme le descripteur d'accès.

Voici un exemple qui utilise chaque méthode.

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, et non sur le thread Worker, 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 l'ajout de la compatibilité intégrée avec les outils de développement (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 est d'ailleurs tirée directement de l'extension.

Extension OPFS Explorer Outils pour les développeurs Chrome 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), puis vous êtes prêt à 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, et supprimez les fichiers et 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é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 GitHub. Notez que la version intégrée n'utilise pas le backend du système de fichiers privé d'origine (car l'iframe est inter-origine), mais que c'est le cas lorsque vous ouvrez la démo dans un onglet distinct.

Conclusion

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 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 à bord et partagent une vision commune. Le développement du système de fichiers privé d'origine est un effort de collaboration, et les commentaires des développeurs et des utilisateurs sont essentiels à sa progression.

À mesure que nous continuons à affiner et à améliorer la norme, les commentaires sur le dépôt whatwg/fs sous forme de problèmes ou de requêtes d'extraction sont les bienvenus.

Remerciements

Ce document a été examiné par Austin Sully, Etienne Noël et Rachel Andrew.