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. Bei näherem Hinsehen zeigt sich jedoch, dass nicht alles Gold ist, was glänzt.

Die traditionelle Dateiverwaltung

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 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 öffnen

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.

Dateien speichern (bzw. herunterladen)

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.

File System Access API

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

Dateien öffnen

Mit der File System Access API können Sie eine Datei mit einem einzigen Aufruf der Methode window.showOpenFilePicker() öffnen. 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, indem Sie window.showDirectoryPicker() aufrufen. Dadurch werden Verzeichnisse im Dateidialogfeld 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

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

Tabelle zur Browserunterstützung der File System Access API Alle Browser sind als „Nicht unterstützt“ oder „Unterstützt mit Einschränkungen“ 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. Das heißt, die Bibliothek ist keine Polyfill, sondern eine 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 die treffend benannten fileOpen(), directoryOpen() und fileSave(). Intern wird von der Bibliothek-Funktion erkannt, 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 zeichnen 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. Das macht es zu einem guten Kandidaten für die browser-fs-access-Bibliothek.

Ich kann beispielsweise auf meinem iPhone ein Bild zeichnen, es im Downloads-Ordner meines iPhones speichern (technisch gesehen: herunterladen, da Safari die File System Access API nicht unterstützt) und die Datei auf meinem Computer öffnen (nachdem ich sie von meinem Smartphone übertragen habe). Ich kann die Datei dann ändern und mit meinen Änderungen überschreiben oder sie 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, aber eine Datei im Downloadordner 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.

Praxisnahes Codebeispiel

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 Symbolleiste der Haupt-App von Excalidraw auf dem iPhone und in Chrome auf dem Computer. Auf dem iPhone fehlt die Schaltfläche Als.

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

Ergebnisse

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.