La norme File System introduit un système de fichiers privé d'origine (OPFS, Origin Private File System) en tant que point de stockage privé à l'origine de la page et non visible par l'utilisateur. Il offre 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és d'origine est compatible avec les navigateurs modernes et est normalisé par le Web Hypertext Application Technology Working Group (WHATWG) dans la norme File System Living Standard.
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, la liste de tâches d'un utilisateur nommé 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 lecteur.
Méthode traditionnelle pour travailler avec des fichiers sur le Web
Pour modifier la liste de tâches dans une application Web, la procédure habituelle est la suivante :
- L'utilisateur importe le fichier sur un serveur ou l'ouvre sur le client avec
<input type="file">
. - L'utilisateur apporte ses modifications, puis télécharge le fichier résultant avec un
<a download="ToDo.txt>
injecté que vousclick()
de manière programmatique via JavaScript. - Pour ouvrir des dossiers, vous utilisez un attribut spécial dans
<input type="file" webkitdirectory>
qui, malgré son nom propriétaire, est compatible avec la quasi-totalité des navigateurs.
Une façon moderne de travailler avec des fichiers sur le Web
Ce flux ne reflète pas la façon dont les utilisateurs envisagent la modification de fichiers. Il signifie que les utilisateurs se retrouvent avec des copies téléchargées de leurs fichiers d'entrée. C'est pourquoi 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 le flux suivant :
- Ouvrez
ToDo.txt
avecshowOpenFilePicker()
et obtenez un objetFileSystemFileHandle
. - À partir de l'objet
FileSystemFileHandle
, obtenez unFile
en appelant la méthodegetFile()
du gestionnaire de fichiers. - Modifiez le fichier, puis appelez
requestPermission({mode: 'readwrite'})
sur le handle. - Si l'utilisateur accepte la demande d'autorisation, enregistrez les modifications dans le fichier d'origine.
- Vous pouvez également appeler
showSaveFilePicker()
et laisser l'utilisateur choisir un nouveau fichier. (Si l'utilisateur choisit un fichier 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 liées à l'utilisation de fichiers sur le Web
Les fichiers et 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 sur le Web, et plus particulièrement les fichiers exécutables, sont marqués avec la marque du Web. Ainsi, le système d'exploitation peut 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 un antivirus basé dans le cloud, pour simplifier 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 effectuées sur place, 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 sur les fichiers relativement lentes, malgré les améliorations apportées lorsque cela est possible, par exemple sur macOS. Toutefois, chaque appel write()
est autonome. En coulisses, il ouvre le fichier, recherche le décalage donné et écrit enfin les données.
Les fichiers comme base du traitement
Les fichiers sont également 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 représentant une 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 système de fichiers privés d'origine.
Système de fichiers privés d'origine et système de fichiers privés visibles par l'utilisateur
Contrairement au système de fichiers visible par l'utilisateur et 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és d'origine sont privés, plus précisément pour 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 Comprendre "same-site" et "same-origin". Toutes les pages qui partagent la même origine peuvent voir les mêmes données du 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 dans l'exemple précédent. Chaque origine possède son propre système de fichiers privé d'origine indépendant. Cela signifie que le système de fichiers privé d'origine de https://developer.chrome.com
est complètement distinct 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 pour le système de fichiers privé d'origine est un répertoire racine initialement vide par 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 diagramme 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 à structurer selon vos besoins en matière de données et de stockage.
Spécificités du système de fichiers privés 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 de site, le système de fichiers privés d'origine est également supprimé. Appelez navigator.storage.estimate()
et, dans l'objet de réponse obtenu, consultez l'entrée usage
pour connaître la quantité de stockage déjà utilisée par votre application. Cette quantité est ventilée par mécanisme de stockage dans l'objet usageDetails
, où vous devez examiner plus particulièrement l'entrée fileSystem
. Comme 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 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és 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. De plus, les opérations sur les fichiers sont généralement synchrones dans les 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 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. Sinon, vous pouvez continuer à lire.
Utiliser le système de fichiers privés 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 à 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});
Accéder aux fichiers et dossiers existants
Si vous connaissez le nom d'un fichier ou d'un dossier, accédez-y 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 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 où un Blob
peut l'être. En particulier, FileReader
, URL.createObjectURL()
, createImageBitmap()
et XMLHttpRequest.send()
acceptent à la fois Blobs
et Files
. Si vous obtenez un File
à partir d'un FileSystemFileHandle
, les données sont "libérées". Vous pouvez ainsi y accéder et 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 flux
Diffusez les données dans un fichier en appelant createWritable()
, ce qui crée un FileSystemWritableFileStream
dans lequel vous write()
le contenu. Enfin, 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 au fichier ou au répertoire. Pour supprimer un dossier, y compris 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, 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()
. Vous pouvez déplacer et renommer des fichiers en même temps 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 à 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és 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 handles de fichier ou de dossier pointent vers le même fichier ou dossier
Il arrive que vous ayez deux identifiants et que vous ne sachiez 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 sur lequel vous itérez avec une boucle for await…of
. En tant qu'itérateur asynchrone, il prend également en charge 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 les sous-dossiers
Il est facile de se tromper lorsqu'on traite des boucles et des fonctions asynchrones associées à la récursivité. 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 leur taille. Vous pouvez simplifier la fonction si vous n'avez pas besoin de la taille des fichiers en ne transmettant pas la promesse handle.getFile()
, mais directement handle
à l'endroit où directoryEntryPromises.push
est indiqué.
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és 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 handle d'accès synchrone
Le point d'entrée des opérations sur les 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 fichier synchrones sur place
Une fois que vous disposez d'un handle d'accès synchrone, vous avez accès à des méthodes de fichier 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é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 indiquée.flush()
: s'assure que le contenu du fichier contient toutes les modifications effectuées viawrite()
.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 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 dans 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és d'origine
En attendant l'ajout de la prise en charge des outils de développement intégrés (voir crbug/1284595), utilisez l'extension Chrome OPFS Explorer pour déboguer le système de fichiers privés d'origine. La capture d'écran ci-dessus de la section Créer des fichiers et des dossiers est extraite directement de l'extension.
Après avoir installé l'extension, ouvrez les outils de développement Chrome, sélectionnez l'onglet OPFS Explorer, puis vous êtes prêt à inspecter la hiérarchie des fichiers. Enregistrez des fichiers du système de fichiers privés d'origine dans le système de fichiers visible par l'utilisateur en cliquant sur le nom du 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és 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 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és d'origine (car l'iFrame est cross-origin), mais que c'est le cas lorsque vous ouvrez la démo dans un onglet distinct.
Conclusions
Le système de fichiers privés d'origine, tel que spécifié par le WHATWG, a façonné la façon dont nous utilisons les fichiers sur le Web et interagissons avec eux. 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és d'origine est un effort de collaboration. Les commentaires des développeurs et des utilisateurs sont essentiels à sa progression. Nous continuons à affiner et à améliorer la norme. 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.
Liens associés
- Spécification standard du système de fichiers
- Dépôt standard du système de fichiers
- Post WebKit sur l'API File System avec le système de fichiers privés d'origine
- Extension OPFS Explorer
Remerciements
Cet article a été examiné par Austin Sully, Etienne Noël et Rachel Andrew. Image héros de Christina Rumpf sur Unsplash.