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

Browser können schon lange mit Dateien und Verzeichnissen umgehen. Die File API bietet Funktionen zum Darstellen von Datei-Objekten in Webanwendungen sowie zum programmgesteuerten Auswählen und Zugreifen auf ihre Daten. Bei genauerem Hinsehen stellt sich jedoch heraus, dass nicht alles Gold ist, was glänzt.

Der herkömmliche Umgang mit 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 so aussehen wie im folgenden Codebeispiel. Das input-Objekt enthält ein FileList, das im folgenden Fall nur aus einem File besteht. Ein File ist eine spezielle Art von Blob und kann in jedem Kontext verwendet werden, in dem ein Blob verwendet werden kann.

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 beschrieben. Trotz des Anbietervorsatzes im Namen ist webkitdirectory nicht nur in Chromium- und WebKit-Browsern, sondern auch im alten Edge auf EdgeHTML-Basis sowie in Firefox verfügbar.

Dateien speichern (bzw. herunterladen)

Bisher konnten Sie eine Datei nur herunterladen, um sie zu speichern. Das funktioniert dank des Attributs <a download>. Sie können das Attribut href des Ankers für ein Blob auf eine blob:-URL festlegen, die Sie mit der 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, den klassischen Ablauf „Öffnen“ → „Bearbeiten“ → „Speichern“ zu nutzen. Das heißt, es gibt keine Möglichkeit, die Originaldatei zu überschreiben. Stattdessen wird bei jedem Speichern eine neue Kopie der Originaldatei im Standard-Downloadordner des Betriebssystems erstellt.

File System Access API

Die File System Access API vereinfacht sowohl das Öffnen als auch das Speichern von Dateien erheblich. Außerdem ist das tatsächliche Speichern möglich. Sie können also nicht nur auswählen, wo eine Datei gespeichert werden soll, sondern auch eine vorhandene Datei überschreiben.

Dateien öffnen

Mit der File System Access API ist das Öffnen einer Datei ein Aufruf der Methode window.showOpenFilePicker(). Dieser Aufruf gibt ein Dateihandle zurück, über das Sie mit der Methode getFile() die tatsächliche File 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 ein Verzeichnis, indem Sie window.showDirectoryPicker() aufrufen. Dadurch werden Verzeichnisse im Dateidialogfeld auswählbar.

Dateien speichern

Das Speichern von Dateien ist ähnlich einfach. Aus einem Dateihandle erstellen Sie mit createWritable() einen beschreibbaren Stream. Dann schreiben Sie die Blob-Daten, indem Sie die write()-Methode des Streams aufrufen. Schließlich 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);
  }
};

browser-fs-access

Die File System Access API ist zwar sehr gut, aber noch nicht weit verbreitet.

Tabelle zur Browserunterstützung für die File System Access API. Alle Browser sind mit „Keine Unterstützung“ oder „Hinter einem Flag“ gekennzeichnet.
Tabelle zur Browserunterstützung für die File System Access API. (Quelle)

Aus diesem Grund betrachte ich die File System Access API als progressive Verbesserung. Daher möchte ich es verwenden, wenn der Browser es unterstützt, und den herkömmlichen Ansatz, wenn nicht. Dabei soll der Nutzer nie durch unnötige Downloads von nicht unterstütztem JavaScript-Code belastet werden. Die browser-fs-access-Bibliothek ist meine Antwort auf diese Herausforderung.

Designphilosophie

Da sich die File System Access API in Zukunft wahrscheinlich noch ändern wird, ist die browser-fs-access API nicht daran angelehnt. Die Bibliothek ist also kein Polyfill, sondern ein Ponyfill. Sie können (statisch oder dynamisch) ausschließlich die Funktionen importieren, die Sie benötigen, um Ihre App so klein wie möglich zu halten. Die verfügbaren Methoden sind fileOpen(), directoryOpen() und fileSave(). Intern wird in der Bibliothek erkannt, ob die File System Access API unterstützt wird. Anschließend wird der entsprechende Codepfad importiert.

Browser-FS-Access-Bibliothek verwenden

Die drei Methoden sind intuitiv zu verwenden. Sie können die akzeptierte mimeTypes oder Datei extensions Ihrer App angeben und ein multiple-Flag festlegen, um die Auswahl mehrerer Dateien oder Verzeichnisse zuzulassen oder zu verbieten. Vollständige Informationen finden Sie in der API-Dokumentation für den Browser-Dateisystemzugriff. Das folgende Codebeispiel zeigt, wie Sie Bilddateien öffnen und speichern können.

// 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

Demo auf Glitch Dort ist auch der Quellcode verfügbar. Aus Sicherheitsgründen darf in untergeordneten Frames, die nicht von derselben Quelle stammen, keine Dateiauswahl angezeigt werden. Daher kann die Demo nicht in diesen Artikel eingebettet werden.

Die Browser-FS-Access-Bibliothek in der Praxis

In meiner Freizeit trage ich ein wenig zu einer installierbaren PWA namens Excalidraw bei, einem Whiteboard-Tool, mit dem Sie ganz einfach Diagramme mit einem handgezeichneten Look erstellen können. Es ist vollständig responsiv und funktioniert auf einer Vielzahl von Geräten, von kleinen Smartphones bis hin zu Computern mit großen Bildschirmen. Das bedeutet, dass die App Dateien auf allen verschiedenen Plattformen verarbeiten muss, unabhängig davon, ob sie die File System Access API unterstützen. Daher eignet sie sich hervorragend für die browser-fs-access-Bibliothek.

Ich kann beispielsweise eine Zeichnung auf meinem iPhone beginnen, sie speichern (technisch: herunterladen, da Safari die File System Access API nicht unterstützt) und in den Downloadordner auf meinem iPhone verschieben, die Datei auf meinem Computer öffnen (nachdem ich sie von meinem Smartphone übertragen habe), die Datei ändern und mit meinen Änderungen überschreiben oder sogar als neue Datei speichern.

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

Codebeispiel aus der Praxis

Unten sehen Sie ein echtes Beispiel für browser-fs-access, wie es in Excalidraw verwendet wird. Dieser Auszug stammt aus /src/data/json.ts. Besonders interessant ist, wie die Methode saveAsJSON() entweder ein Dateihandle oder null an die Methode fileSave() von browser-fs-access übergibt. Dadurch wird die Datei überschrieben, wenn ein Handle angegeben wird, oder in einer neuen Datei gespeichert, wenn 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

Die Benutzeroberfläche sollte sich in Excalidraw oder Ihrer App an die Unterstützung 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 auch die Schaltfläche Speichern unter anzeigen. Die Screenshots unten zeigen den Unterschied zwischen der responsiven Hauptsymbolleiste der Excalidraw-App auf dem iPhone und auf Chrome für den Desktop. Beachten Sie, dass die Schaltfläche Speichern unter auf dem iPhone fehlt.

Symbolleiste der Excalidraw App auf einem iPhone mit nur der Schaltfläche „Speichern“.
Symbolleiste der Excalidraw App auf dem iPhone mit nur der Schaltfläche Speichern.
Die Symbolleiste der Excalidraw-App in der Desktopversion von Chrome mit den Schaltflächen „Speichern“ und „Speichern unter“.
Excalidraw-App-Symbolleiste in Chrome mit den Schaltflächen Speichern und Speichern unter.

Zusammenfassung

Die Arbeit mit Systemdateien ist technisch in allen modernen Browsern möglich. In Browsern, die die File System Access API unterstützen, können Sie die Nutzerfreundlichkeit verbessern, indem Sie das tatsächliche Speichern und Überschreiben (nicht nur das Herunterladen) von Dateien ermöglichen und Nutzern erlauben, neue Dateien zu erstellen, wo immer sie möchten. Gleichzeitig bleibt die Funktionalität in Browsern erhalten, die die File System Access API nicht unterstützen. Die browser-fs-access-Bibliothek erleichtert Ihnen die Arbeit, da sie sich um die Feinheiten der progressiven Verbesserung kümmert und Ihren Code so einfach wie möglich macht.

Danksagungen

Dieser Artikel wurde von Joe Medley und Kayce Basques geprüft. Vielen Dank an die Mitwirkenden von Excalidraw für ihre Arbeit am Projekt und für die Überprüfung meiner Pull Requests. Hero-Image von Ilya Pavlov auf Unsplash.