Dateien und Verzeichnisse mit der Bibliothek „browser-fs-access“ lesen und schreiben

Browser können schon seit Langem mit Dateien und Verzeichnissen umgehen. Die File API bietet Funktionen zur Darstellung von Dateiobjekten in Webanwendungen sowie zur programmatischen Auswahl von Dateiobjekten und für den Zugriff auf ihre Daten. Wenn man jedoch genau hinsieht, ist nicht alles Gold glänzend.

Die traditionelle Art der Verarbeitung von Dateien

Dateien öffnen

Als Entwickler können Sie Dateien über das Element <input type="file"> öffnen und lesen. In der einfachsten Form kann das Öffnen einer Datei in etwa wie im folgenden Codebeispiel aussehen. Das input-Objekt liefert ein FileList, das im folgenden Fall aus nur einem File besteht. Ein File ist eine bestimmte Art von Blob und kann in jedem Kontext verwendet werden, in dem ein Blob möglich ist.

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

Verzeichnisse öffnen

Zum Öffnen von Ordnern (oder Verzeichnissen) können Sie das Attribut <input webkitdirectory> festlegen. Ansonsten funktioniert alles wie oben. Trotz des Anbieternamens kann webkitdirectory nicht nur in Chromium- und WebKit-Browsern verwendet werden, sondern auch im Legacy-EdgeHTML-basierten Edge und in Firefox.

Speichern (statt Herunterladen) von Dateien

Wenn Sie eine Datei speichern möchten, müssen Sie sie normalerweise nur herunterladen. Dies funktioniert dank des Attributs <a download>. Bei einem Blob können Sie das Attribut href des Ankers auf eine blob:-URL festlegen, die Sie über die Methode URL.createObjectURL() abrufen können.

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();
};

Das Problem

Ein großer Nachteil des Download-Ansatzes besteht darin, dass es keine Möglichkeit gibt, einen klassischen Ablauf des Typs Öffnen → Bearbeiten → Speichern durchzuführen. Es gibt also keine Möglichkeit, die Originaldatei zu überschreiben. Stattdessen erhalten Sie bei jedem "Speichern" eine neue Kopie der Originaldatei im Standard-Downloadordner des Betriebssystems.

File System Access API

Die File System Access API vereinfacht sowohl das Öffnen als auch das Speichern von Dateien. Außerdem wird das echte Speichern ermöglicht. Das bedeutet, dass Sie nicht nur auswählen können, wo eine Datei gespeichert werden soll, sondern dass Sie auch eine vorhandene Datei überschreiben können.

Dateien öffnen

Mit der File System Access API muss eine Datei mit einem Aufruf der window.showOpenFilePicker()-Methode geöffnet werden. Dieser Aufruf gibt ein Datei-Handle zurück, mit dem Sie die tatsächliche File über die getFile()-Methode abrufen können.

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

Verzeichnisse öffnen

Öffnen Sie durch Aufrufen von window.showDirectoryPicker() ein Verzeichnis. Dadurch werden Verzeichnisse im Dateidialogfeld auswählbar.

Dateien werden gespeichert

Das Speichern von Dateien ist ähnlich einfach. Von einem Datei-Handle erstellen Sie über createWritable() einen beschreibbaren Stream. Anschließend schreiben Sie die Blob-Daten, indem Sie die write()-Methode des Streams aufrufen. Zum Schluss schließen Sie den Stream, indem Sie seine close()-Methode aufrufen.

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);
  }
};

Jetzt neu: browser-fs-access

Auch wenn die File System Access API so perfekt ist, ist sie noch nicht allgemein verfügbar.

Tabelle mit den Browserunterstützung für die File System Access API. Alle Browser sind mit &quot;Keine Unterstützung&quot; oder &quot;hinter einer Markierung&quot; gekennzeichnet.
Tabelle mit den Browserunterstützung für die File System Access API. (Quelle)

Aus diesem Grund betrachte ich die File System Access API als fortschrittliche Verbesserung. Daher möchte ich es verwenden, wenn es vom Browser unterstützt wird, und ansonsten den traditionellen Ansatz, ohne den Nutzer mit unnötigen Downloads von nicht unterstütztem JavaScript-Code zu bestrafen. Die Bibliothek browser-fs-access ist meine Antwort auf diese Herausforderung.

Designphilosophie

Da sich die File System Access API in Zukunft wahrscheinlich noch ändern wird, wird die browser-fs-access API nicht entsprechend modelliert. Das heißt, die Bibliothek ist kein polyfill, sondern ein Ponyfill. Sie können (statisch oder dynamisch) ausschließlich alle Funktionen importieren, die Sie benötigen, um Ihre App so klein wie möglich zu halten. Die verfügbaren Methoden sind die passend benannten Methoden fileOpen(), directoryOpen() und fileSave(). Intern erkennt die Bibliotheksfunktion, ob die File System Access API unterstützt wird, und importiert dann den entsprechenden Codepfad.

Bibliothek „browser-fs-access“ verwenden

Die drei Methoden sind intuitiv zu bedienen. Sie können die akzeptierte mimeTypes oder die Datei extensions Ihrer Anwendung angeben und ein multiple-Flag setzen, um die Auswahl mehrerer Dateien oder Verzeichnisse zuzulassen oder zu verbieten. Ausführliche Informationen finden Sie in der Dokumentation zur browser-fs-access API. Im Codebeispiel unten sehen Sie, wie Sie Bilddateien öffnen und speichern.

// 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',
  });
})();

Demo

Du kannst dir den obigen Code in einer Demo auf Glitch ansehen. Auch der Quellcode ist dort verfügbar. Da aus Sicherheitsgründen ursprungsübergreifende Subframes keine Dateiauswahl anzeigen dürfen, kann die Demo nicht in diesen Artikel eingebettet werden.

Die neue Browser-FS-Access-Bibliothek

In meiner Freizeit trage ich ein kleines bisschen zu einer installierbaren PWA namens Excalidraw bei, einem Whiteboard-Tool, mit dem sich Diagramme ganz einfach handgezeichnet lassen können. Sie ist vollständig responsiv und funktioniert gut auf einer Reihe von Geräten von kleinen Mobiltelefonen bis hin zu Computern mit großen Bildschirmen. Das bedeutet, dass es mit Dateien auf den verschiedenen Plattformen umgehen muss, unabhängig davon, ob sie die File System Access API unterstützen oder nicht. Dies macht sie zu einem hervorragenden Kandidat für die Bibliothek „browser-fs-access“.

Ich kann zum Beispiel eine Zeichnung auf meinem iPhone starten, sie herunterladen (technisch gesehen: Download, da Safari die File System Access API nicht unterstützt) in meinem iPhone-Downloadordner, öffne die Datei auf meinem Desktop (nach der Übertragung von meinem Smartphone), bearbeite sie und überschreibe sie mit meinen Änderungen oder speichere sie sogar als neue Datei.

Eine Excalidraw-Zeichnung auf einem iPhone.
Excalidraw-Zeichnung auf einem iPhone starten, auf dem die File System Access API nicht unterstützt wird, aber eine Datei im Ordner „Downloads“ gespeichert (heruntergeladen) werden kann
Die geänderte Excalidraw-Zeichnung in Chrome auf dem Desktop.
Excalidraw-Zeichnung auf dem Computer öffnen und ändern, auf dem die File System Access API unterstützt wird, sodass über die API auf die Datei zugegriffen werden kann
Die Originaldatei mit den Änderungen wird überschrieben.
Originaldatei mit den Änderungen in der ursprünglichen Excalidraw-Zeichnungsdatei überschreiben. Der Browser zeigt ein Dialogfeld an, in dem ich gefragt werde, ob das in Ordnung ist.
Speichern der Änderungen in einer neuen Excalidraw-Zeichnungsdatei
Die Änderungen werden in einer neuen Excalidraw-Datei gespeichert. Die Originaldatei bleibt unverändert.

Codebeispiel aus der Praxis

Unten sehen Sie ein Praxisbeispiel für browser-fs-access, wie er in Excalidraw verwendet wird. Dieser Auszug stammt aus /src/data/json.ts. Von besonderem Interesse ist die Art und Weise, wie die Methode saveAsJSON() entweder ein Datei-Handle oder null an die Methode fileSave() von browser-fs-access übergibt. Dadurch wird sie überschrieben, wenn ein Handle angegeben ist, oder dass sie in einer neuen Datei gespeichert wird, falls dies nicht der Fall ist.

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);
};

Hinweise zur Benutzeroberfläche

Unabhängig davon, ob in Excalidraw oder Ihrer Anwendung, sollte sich die Benutzeroberfläche an die Supportsituation des Browsers anpassen. Wenn die File System Access API unterstützt wird (if ('showOpenFilePicker' in window) {}), können Sie zusätzlich zur Schaltfläche Speichern die Schaltfläche Speichern unter anzeigen lassen. Die folgenden Screenshots zeigen den Unterschied zwischen der Symbolleiste der responsiven Haupt-App von Excalidraw auf dem iPhone und auf der Chrome-Desktop-App. Wie Sie sehen, fehlt auf dem iPhone die Schaltfläche Speichern unter.

Symbolleiste der Excalidraw-App auf dem iPhone mit nur einer Schaltfläche &quot;Speichern&quot;.
Symbolleiste der Excalidraw-App auf dem iPhone über die Schaltfläche Save (Speichern).
Symbolleiste der Excalidraw-App auf der Chrome-Desktop-App mit den Schaltflächen „Speichern“ und „Speichern unter“.
Symbolleiste der Excalidraw-App in Chrome mit der Schaltfläche Speichern und der hervorgehobenen Schaltfläche Speichern unter

Ergebnisse

Die Arbeit mit Systemdateien funktioniert technisch in allen modernen Browsern. Bei Browsern, die die File System Access API unterstützen, können Sie die Abläufe optimieren, indem Sie Dateien richtig speichern und überschreiben (nicht nur herunterladen) und Ihre Nutzer jederzeit neue Dateien erstellen können. In Browsern, die die File System Access API nicht unterstützen, bleiben die Funktionen erhalten. Der browser-fs-access erleichtert Ihnen das Leben, da er die Feinheiten Progressive Enhancement angeht und Ihren Code so einfach wie möglich gestaltet.

Danksagungen

Dieser Artikel wurde von Joe Medley und Kayce Basques geprüft. vielen Dank an die Mitwirkenden zu Excalidraw für ihre Arbeit an dem Projekt und für die Prüfung meiner Pull-Anfragen. Hero-Image von Ilya Pavlov auf Unsplash.