Lire et écrire des fichiers et des répertoires avec la bibliothèquebrowser-fs-access

Les navigateurs gèrent les fichiers et les répertoires depuis longtemps. L'API File fournit des fonctionnalités permettant de représenter des objets fichier dans des applications Web, ainsi que de les sélectionner de manière automatisée et d'accéder à leurs données. Mais au moment où vous regardez de plus près, tout ce qui brille n'est pas or.

La façon traditionnelle de traiter les fichiers

Ouvrir des fichiers

En tant que développeur, vous pouvez ouvrir et lire des fichiers via l'élément <input type="file">. Dans sa forme la plus simple, l'ouverture d'un fichier peut se présenter comme dans l'exemple de code ci-dessous. L'objet input vous donne un FileList, qui, dans le cas ci-dessous, ne comprend qu'un seul File. Un File est un type spécifique de Blob et peut être utilisé dans tous les contextes possibles d'un objet Blob.

const openFile = async () => {
  return new Promise((resolve) => {
    const input = document.createElement('input');
    input.type = 'file';
    input.addEventListener('change', () => {
      resolve(input.files[0]);
    });
    input.click();
  });
};

Ouverture de répertoires

Pour ouvrir des dossiers (ou répertoires), vous pouvez définir l'attribut <input webkitdirectory>. À part cela, tout le reste fonctionne de la même manière que ci-dessus. Malgré son nom avec le préfixe de fournisseur, webkitdirectory n'est pas seulement utilisable dans les navigateurs Chromium et WebKit, mais aussi dans l'ancien Edge EdgeHTML et dans Firefox.

Enregistrer (au lieu de télécharger) des fichiers

En règle générale, pour enregistrer un fichier, vous êtes limité à son téléchargement, ce qui fonctionne grâce à l'attribut <a download>. Pour un objet Blob, vous pouvez définir l'attribut href de l'ancre sur une URL blob: que vous pouvez obtenir à l'aide de la méthode URL.createObjectURL().

const saveFile = async (blob) => {
  const a = document.createElement('a');
  a.download = 'my-file.txt';
  a.href = URL.createObjectURL(blob);
  a.addEventListener('click', (e) => {
    setTimeout(() => URL.revokeObjectURL(a.href), 30 * 1000);
  });
  a.click();
};

Problème

L'un des inconvénients majeurs de l'approche de téléchargement est qu'il n'existe aucun moyen d'effectuer un processus classique d'ouverture → modification → enregistrement, c'est-à-dire qu'il n'existe aucun moyen d'écraser le fichier d'origine. À la place, vous obtenez une nouvelle copie du fichier d'origine dans le dossier "Téléchargements" par défaut du système d'exploitation à chaque enregistrement.

API File System Access

L'API File System Access simplifie considérablement les opérations d'ouverture et d'enregistrement. Elle active également le vrai enregistrement, c'est-à-dire que vous pouvez non seulement choisir où enregistrer un fichier, mais également écraser un fichier existant.

Ouvrir des fichiers

Avec l'API File System Access, l'ouverture d'un fichier consiste à appeler la méthode window.showOpenFilePicker(). Cet appel renvoie un handle de fichier, à partir duquel vous pouvez obtenir le File réel via la méthode getFile().

const openFile = async () => {
  try {
    // Always returns an array.
    const [handle] = await window.showOpenFilePicker();
    return handle.getFile();
  } catch (err) {
    console.error(err.name, err.message);
  }
};

Ouverture de répertoires

Ouvrez un répertoire en appelant window.showDirectoryPicker() qui rend les répertoires sélectionnables dans la boîte de dialogue de fichier.

Enregistrement de fichiers

L'enregistrement de fichiers est tout aussi simple. À partir d'un handle de fichier, vous créez un flux accessible en écriture via createWritable(), puis vous écrivez les données Blob en appelant la méthode write() du flux, puis vous fermez le flux en appelant sa méthode close().

const saveFile = async (blob) => {
  try {
    const handle = await window.showSaveFilePicker({
      types: [{
        accept: {
          // Omitted
        },
      }],
    });
    const writable = await handle.createWritable();
    await writable.write(blob);
    await writable.close();
    return handle;
  } catch (err) {
    console.error(err.name, err.message);
  }
};

Présentation de browser-fs-access

Aussi bonne que l'API File System Access, elle n'est pas encore disponible pour tous.

Tableau des navigateurs compatibles avec l&#39;API File System Access. Tous les navigateurs sont marqués comme &quot;sans assistance&quot; ou &quot;derrière un indicateur&quot;.
Tableau des navigateurs compatibles avec l'API File System Access. (Source)

C'est pourquoi je considère l'API File System Access comme une amélioration progressive. Par conséquent, je souhaite l'utiliser lorsque le navigateur le permet, et utiliser l'approche traditionnelle si ce n'est pas le cas, tout en ne punissant jamais l'utilisateur de téléchargements inutiles de code JavaScript non compatible. La bibliothèque browser-fs-access est ma réponse à ce défi.

Philosophie de conception

Étant donné que l'API File System Access est susceptible de changer à l'avenir, l'API browser-fs-access n'est pas modélisée sur celle-ci. La bibliothèque n'est pas un polyfill, mais plutôt un ponyfill. Vous pouvez importer (de manière statique ou dynamique) exclusivement les fonctionnalités dont vous avez besoin pour réduire au maximum la taille de votre application. Les méthodes disponibles sont les suivantes : fileOpen(), directoryOpen() et fileSave(). En interne, la fonctionnalité de bibliothèque détecte si l'API File System Access est compatible, puis importe le chemin de code correspondant.

Utiliser la bibliothèque browser-fs-access

L'utilisation de ces trois méthodes est intuitive. Vous pouvez spécifier le mimeTypes ou le fichier extensions accepté par votre application, et définir un indicateur multiple pour autoriser ou interdire la sélection de plusieurs fichiers ou répertoires. Pour plus d'informations, consultez la documentation de l'API browser-fs-access. L'exemple de code ci-dessous montre comment ouvrir et enregistrer des fichiers image.

// The imported methods will use the File
// System Access API or a fallback implementation.
import {
  fileOpen,
  directoryOpen,
  fileSave,
} from 'https://unpkg.com/browser-fs-access';

(async () => {
  // Open an image file.
  const blob = await fileOpen({
    mimeTypes: ['image/*'],
  });

  // Open multiple image files.
  const blobs = await fileOpen({
    mimeTypes: ['image/*'],
    multiple: true,
  });

  // Open all files in a directory,
  // recursively including subdirectories.
  const blobsInDirectory = await directoryOpen({
    recursive: true
  });

  // Save a file.
  await fileSave(blob, {
    fileName: 'Untitled.png',
  });
})();

Démonstration

Vous pouvez voir le code ci-dessus en action dans une démonstration sur Glitch. Son code source y est également disponible. Pour des raisons de sécurité, les sous-frames multi-origines ne sont pas autorisés à afficher un sélecteur de fichier. La démo ne peut donc pas être intégrée dans cet article.

La bibliothèque browser-fs-access dans le monde réel

Pendant mon temps libre, je contribue brièvement à une PWA installable appelée Excalidraw, un outil pour tableau blanc qui vous permet de dessiner facilement des diagrammes à la main. Il est entièrement réactif et fonctionne bien sur de nombreux appareils, des petits téléphones mobiles aux ordinateurs dotés de grands écrans. Cela signifie qu'il doit traiter les fichiers sur toutes les différentes plates-formes, qu'elles soient compatibles ou non avec l'API File System Access. Il s'agit donc d'un candidat idéal pour la bibliothèque browser-fs-access.

Je peux, par exemple, commencer un dessin sur mon iPhone, l'enregistrer (techniquement: le télécharger, car Safari ne prend pas en charge l'API File System Access) dans le dossier des téléchargements de mon iPhone, ouvrir le fichier sur mon bureau (après l'avoir transféré depuis mon téléphone), le modifier et l'écraser avec mes modifications, ou même l'enregistrer en tant que nouveau fichier.

Dessin Excalidraw sur un iPhone.
Vous pouvez lancer un dessin Excalidraw sur un iPhone sur lequel l'API File System Access n'est pas compatible, mais sur lequel un fichier peut être enregistré (téléchargé) dans le dossier "Téléchargements".
Le dessin Excalidraw modifié dans Chrome sur le bureau.
Ouvrir et modifier le dessin Excalidraw sur un ordinateur de bureau où l'API File System Access est compatible avec l'API pour y accéder
Écraser le fichier original avec les modifications.
Remplacement du fichier d'origine par les modifications apportées au fichier de dessin Excalidraw. Le navigateur affiche une boîte de dialogue me demandant si cela convient.
Enregistrement des modifications dans un nouveau fichier de dessin Excalidraw.
Enregistrement des modifications dans un nouveau fichier Excalidraw Le fichier d'origine reste intact.

Exemple de code réel

Vous trouverez ci-dessous un exemple concret d'accès "browser-fs-access" tel qu'il est utilisé dans Excalidraw. Cet extrait est issu de /src/data/json.ts. Il est particulièrement intéressant de voir comment la méthode saveAsJSON() transmet un handle de fichier ou null à la méthode fileSave() de browser-fs-access, qui entraîne son écrasement lorsqu'un handle est fourni, ou son enregistrement dans un nouveau fichier si ce n'est pas le cas.

export const saveAsJSON = async (
  elements: readonly ExcalidrawElement[],
  appState: AppState,
  fileHandle: any,
) => {
  const serialized = serializeAsJSON(elements, appState);
  const blob = new Blob([serialized], {
    type: "application/json",
  });
  const name = `${appState.name}.excalidraw`;
  (window as any).handle = await fileSave(
    blob,
    {
      fileName: name,
      description: "Excalidraw file",
      extensions: ["excalidraw"],
    },
    fileHandle || null,
  );
};

export const loadFromJSON = async () => {
  const blob = await fileOpen({
    description: "Excalidraw files",
    extensions: ["json", "excalidraw"],
    mimeTypes: ["application/json"],
  });
  return loadFromBlob(blob);
};

Remarques sur l'interface utilisateur

Que ce soit dans Excalidraw ou dans votre application, l'UI doit s'adapter à la situation d'assistance du navigateur. Si l'API File System Access est compatible (if ('showOpenFilePicker' in window) {}), vous pouvez afficher un bouton Save As (Enregistrer sous) en plus du bouton Save (Enregistrer). Les captures d'écran ci-dessous montrent la différence entre la barre d'outils réactive de l'application principale d'Excalidraw sur iPhone et sur Chrome pour ordinateur. Notez que sur iPhone, le bouton Save As (Enregistrer sous) est manquant.

la barre d&#39;outils de l&#39;application Excalidraw sur iPhone, grâce à un simple bouton &quot;Enregistrer&quot;.
Barre d'outils de l'application Excalidraw sur iPhone, avec un simple bouton Enregistrer
Barre d&#39;outils de l&#39;application Excalidraw sur le bureau Chrome avec les boutons &quot;Enregistrer&quot; et &quot;Enregistrer sous&quot;.
Barre d'outils de l'application Excalidraw dans Chrome avec le bouton Enregistrer et le bouton Enregistrer sous sélectionné.

Conclusions

L'utilisation des fichiers système fonctionne techniquement sur tous les navigateurs récents. Dans les navigateurs compatibles avec l'API File System Access, vous pouvez améliorer l'expérience en autorisant l'enregistrement et l'écrasement réel des fichiers, et en permettant aux utilisateurs de créer des fichiers où qu'ils le souhaitent, tout en restant fonctionnel sur les navigateurs non compatibles avec cette API. browser-fs-access vous facilite la vie en gérant les subtilités de l'amélioration progressive et en simplifiant au maximum votre code.

Remerciements

Cet article a été lu par Joe Medley et Kayce Basques. Merci aux contributeurs d'Excalidraw pour leur travail sur le projet et pour avoir examiné mes demandes d'extraction. Image héros d'Ilya Pavlov sur Unsplash.