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 Dateiobjekten in Webanwendungen sowie zum programmgesteuerten Auswählen und Abrufen ihrer Daten. In dem Moment, in dem du genauer hinsiehst, ist jedoch nicht alles Gold, was glänzt.

Die traditionelle Dateiverwaltung

Dateien werden geöffnet

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 so aussehen wie im folgenden Codebeispiel. Das input-Objekt gibt ein FileList zurück, das im folgenden Fall nur aus einem File besteht. Eine File ist eine bestimmte Art von Blob und kann in jedem Kontext verwendet werden, in dem auch 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 werden geöffnet

Zum Öffnen von Ordnern (oder Verzeichnissen) können Sie das Attribut <input webkitdirectory> festlegen. Abgesehen davon funktioniert alles wie oben beschrieben. Trotz des Namens mit dem Anbieterpräfix kann webkitdirectory nicht nur in Chromium- und WebKit-Browsern, sondern auch im alten EdgeHTML-basierten Edge und in Firefox verwendet werden.

Speichern (statt Herunterladen) von Dateien

Zum Speichern einer Datei sind Sie traditionell auf das Herunterladen einer Datei beschränkt. Das funktioniert dank des Attributs <a download>. Für einen Blob können Sie das href-Attribut des Ankers 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 Herunterladens ist, dass es keine Möglichkeit gibt, die klassische Abfolge „Öffnen → Bearbeiten → Speichern“ auszuführen. Das bedeutet, dass die Originaldatei nicht überschrieben werden kann. Stattdessen wird beim Speichern eine neue Kopie der ursprünglichen Datei im Standard-Downloadordner des Betriebssystems erstellt.

Die File System Access API

Die File System Access API vereinfacht sowohl das Öffnen als auch das Speichern erheblich. Außerdem wird echtes Speichern aktiviert, das heißt, Sie können nicht nur auswählen, wo eine Datei gespeichert werden soll, sondern auch eine vorhandene Datei überschreiben.

Dateien werden geöffnet

Mit der File System Access API ist das Öffnen einer Datei mit einem einzigen Aufruf der window.showOpenFilePicker()-Methode möglich. Dieser Aufruf gibt einen Dateihandle zurück, über den Sie die tatsächliche File über die Methode getFile() 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 durch Aufrufen von window.showDirectoryPicker(). Dadurch werden Verzeichnisse im Dialogfeld „Datei“ auswählbar.

Dateien speichern

Das Speichern von Dateien ist ebenfalls ganz einfach. Sie erstellen über createWritable() einen beschreibbaren Stream aus einem Dateihandle, schreiben dann die Blob-Daten durch Aufrufen der Methode write() des Streams und schließen den Stream schließlich durch Aufrufen der Methode 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);
  }
};

Jetzt neu: browser-fs-access

So gut die File System Access API auch ist, sie ist aber noch nicht allgemein verfügbar.

Tabelle zur Browserunterstützung der File System Access API Alle Browser sind als „Nicht unterstützt“ oder „Unter Vorbehalt“ gekennzeichnet.
Tabelle mit 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 traditionellen Ansatz verwenden, wenn nicht. Dabei möchte ich Nutzer nicht mit unnötigen Downloads von nicht unterstütztem JavaScript-Code bestrafen. Die Bibliothek browser-fs-access ist meine Antwort auf diese Herausforderung.

Designphilosophie

Da die File System Access API sich in Zukunft wahrscheinlich noch ändern wird, ist die browser-fs-access API nicht nach ihr modelliert. Die Bibliothek ist also kein Polyfill, sondern ein Ponyfill. Sie können (statisch oder dynamisch) exklusiv alle Funktionen importieren, die Sie benötigen, um Ihre App so klein wie möglich zu halten. Die verfügbaren Methoden sind die treffend benannten fileOpen(), directoryOpen() und fileSave(). Intern wird in der Bibliothek geprüft, ob die File System Access API unterstützt wird, und dann wird der entsprechende Codepfad importiert.

Bibliothek „browser-fs-access“ verwenden

Die drei Methoden sind intuitiv zu bedienen. Sie können die zulässige mimeTypes oder Datei extensions Ihrer App angeben und ein multiple-Flag setzen, um die Auswahl mehrerer Dateien oder Verzeichnisse zuzulassen oder zu verhindern. Weitere Informationen finden Sie in der API-Dokumentation für browser-fs-access. Im folgenden Codebeispiel wird gezeigt, 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

In einer Demo auf Glitch können Sie sich den Code in Aktion ansehen. Der Quellcode ist dort ebenfalls verfügbar. Da in untergeordneten Frames mit unterschiedlichen Ursprüngen aus Sicherheitsgründen keine Dateiauswahl angezeigt werden darf, kann die Demo nicht in diesen Artikel eingebettet werden.

Die browser-fs-access-Bibliothek in der Praxis

In meiner Freizeit arbeite ich ein wenig an einer installierbaren PWA namens Excalidraw mit. Das ist ein Whiteboard-Tool, mit dem sich Diagramme ganz einfach skizzieren lassen, als wären sie handgezeichnet. 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 es mit Dateien auf allen verschiedenen Plattformen umgehen muss, unabhängig davon, ob sie die File System Access API unterstützen oder nicht. Daher eignet sie sich hervorragend für die Bibliothek browser-fs-access.

Ich kann beispielsweise eine Zeichnung auf meinem iPhone starten, sie in meinem iPhone-Ordner „Downloads“ speichern, die Datei auf meinem iPhone öffnen (nach der Übertragung von meinem Smartphone), sie ändern und mit meinen Änderungen überschreiben oder sie sogar als neue Datei speichern.

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

Codebeispiel aus der Praxis

Unten sehen Sie ein 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 saveAsJSON()-Methode entweder einen Dateihandle oder null an die fileSave()-Methode von browser-fs-access übergibt, wodurch die Datei überschrieben wird, wenn ein Handle angegeben ist, oder in einer neuen Datei gespeichert wird, 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

Unabhängig davon, ob Sie Excalidraw oder Ihre App verwenden, sollte sich die Benutzeroberfläche 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 die Schaltfläche Als Datei speichern anzeigen. Die folgenden Screenshots zeigen den Unterschied zwischen der responsiven Hauptsymbolleiste der App von Excalidraw auf dem iPhone und in der Chrome-Desktopversion. Auf dem iPhone fehlt die Schaltfläche Als.

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

Schlussfolgerungen

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 Speichern und Überschreiben (nicht nur das Herunterladen) von Dateien zulassen und es Nutzern ermöglichen, neue Dateien an beliebiger Stelle zu erstellen. Dabei bleibt die Funktionalität in Browsern erhalten, die die File System Access API nicht unterstützen. browser-fs-access macht Ihnen das Leben leichter, da es die Feinheiten der progressiven Verbesserung berücksichtigt 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 an Excalidraw für ihre Arbeit am Projekt und für die Überprüfung meiner Pull-Requests. Hero-Image von Ilya Pavlov auf Unsplash.