Dauerhafte Dateiverwaltung mit der File System Access API
Herkömmliche Mechanismen zum Hochladen von Dateien sind seit jeher durch die Sandbox des Browsers
eingeschränkt. Der übliche Ansatz mit <input type="file"> zwingt Sie dazu, Dateien nach
einem Neuladen der Seite erneut auszuwählen, und legt keine direkten Dateisystempfade offen. Die File
System Access API bietet eine moderne Lösung, indem sie dauerhafte Datei-Handles ermöglicht, die
Anwendungen in IndexedDB speichern können. Nach erteilter Berechtigung können wiederkehrende Nutzer
dieselbe lokale Datei erneut öffnen, ohne sie noch einmal auszuwählen. Das Fortsetzen eines Uploads
erfordert zusätzlich ein fortsetzbares Serverprotokoll und einen gespeicherten Upload-Status; ein
Datei-Handle allein leistet das nicht. Da die Browser-Unterstützung unterschiedlich ausfällt, sollten
Sie einen Standard-Fallback für die Dateiauswahl beibehalten.
Browser-Unterstützung und Progressive Enhancement
Die Bibliothek browser-fs-access greift auf ein Eingabefeld zur Dateiauswahl zurück, wenn native Auswahldialoge nicht verfügbar sind. Prüfen Sie jede Picker-Methode einzeln, statt davon auszugehen, dass alle Dateisystem-APIs gemeinsam unterstützt werden. Ziehen Sie die Kompatibilitätstabelle für Picker für Ihre Zielbrowser heran. Die Dateiauswahl über den Fallback liefert kein wiederverwendbares natives Handle.
Installieren Sie in einem JavaScript-Projekt mit Bundler browser-fs-access und
idb-keyval. Letzteres wird weiter unten für die Speicherung der Handles in
IndexedDB verwendet.
yarn add browser-fs-access idb-keyval
import { fileOpen, supported } from 'browser-fs-access'
async function handleFileAccess() {
if (supported) {
console.log('Using native File System Access API')
} else {
console.log('Using legacy fallback via browser-fs-access')
}
try {
const blob = await fileOpen({
mimeTypes: ['image/*'],
multiple: false,
})
return blob
} catch (err) {
if (err.name === 'AbortError') {
console.log('User cancelled file selection')
} else {
console.error('Error accessing file:', err)
}
return null
}
}
Sicherheitsaspekte
Die File System Access API erzwingt mehrere Sicherheitsmaßnahmen zum Schutz von Nutzerdaten:
- Berechtigungen sind an den Ursprung gebunden. Gespeicherte Handles können eine erteilte Berechtigung überdauern; prüfen Sie den Zugriff daher erneut.
- Ein sicherer Kontext (HTTPS) ist für den Produktiveinsatz zwingend erforderlich.
- Schreibvorgänge fordern vor dem Ändern von Dateien eine explizite Zustimmung der Nutzer an.
- Der Zugriff ist auf von Nutzern freigegebene Dateien oder Verzeichnisse beschränkt; einige sensible Speicherorte sind gesperrt.
Rufen Sie Berechtigungsanfragen aus einer Nutzeraktion heraus auf, etwa aus einem Klick auf eine
Schaltfläche. Wenn Schreibzugriff erforderlich ist, verwenden Sie { mode: 'readwrite' }
statt { mode: 'read' }.
async function verifyPermissions(handle) {
const options = { mode: 'read' }
try {
if ((await handle.queryPermission(options)) === 'granted') {
return true
}
const permission = await handle.requestPermission(options)
return permission === 'granted'
} catch (err) {
console.error('Permission error:', err)
return false
}
}
Um ein natives Handle dauerhaft zu speichern, nutzen Sie die Structured-Clone-Unterstützung von
IndexedDB, nicht JSON oder localStorage. Laden Sie das gespeicherte Handle, bevor Sie die
Schaltfläche zum erneuten Öffnen aktivieren, damit Berechtigungsanfragen direkt aus dem Klick heraus
ausgeführt werden können. Verbinden Sie die folgenden Funktionen mit den Schaltflächen Ihrer Seite,
nachdem Sie die zuvor gezeigte Hilfsfunktion verifyPermissions in dasselbe Modul
importiert haben:
import { get, set } from 'idb-keyval'
let savedHandle = null
async function loadSavedHandle() {
savedHandle = await get('selected-upload-file') ?? null
return savedHandle !== null
}
async function choosePersistentFile() {
if (typeof window.showOpenFilePicker !== 'function') {
return handleFileAccess()
}
const [handle] = await window.showOpenFilePicker({ multiple: false })
await set('selected-upload-file', handle)
savedHandle = handle
return handle.getFile()
}
async function reopenSavedFile() {
if (!savedHandle) return null
if (!(await verifyPermissions(savedHandle))) return null
return savedHandle.getFile()
}
Warten Sie beim Laden der Seite auf loadSavedHandle() und aktivieren Sie eine
Schaltfläche zum erneuten Öffnen nur dann, wenn sie true zurückgibt. Rufen
Sie choosePersistentFile() bzw. reopenSavedFile() aus den jeweiligen
Klick-Handlern auf. Fangen Sie in diesen Handlern Fehler beim Speichern, beim Abbrechen des Pickers
und beim Dateizugriff ab und zeigen Sie einen hilfreichen Status an. Nutzer können den
Browser-Speicher löschen, die Berechtigung widerrufen oder die Datei verschieben; behalten Sie daher
immer die Schaltfläche zur Dateiauswahl bei. Bevor Sie einen Upload fortsetzen, prüfen Sie, ob die
erneut geöffnete Datei noch zum ursprünglichen Upload passt.
Umgang mit Verzeichnissen
Wenn Sie Verzeichnis-Uploads aktivieren, können Sie ganze Dateihierarchien verarbeiten. Das folgende Beispiel sammelt Dateien rekursiv aus einem von Nutzern ausgewählten Verzeichnis:
async function handleDirectoryUpload() {
if (typeof window.showDirectoryPicker !== 'function') {
throw new Error('Directory selection is unavailable in this browser')
}
try {
const dirHandle = await window.showDirectoryPicker()
const files = []
async function* getFilesRecursively(entry) {
for await (const handle of entry.values()) {
if (handle.kind === 'file') {
const file = await handle.getFile()
if (file) yield file
} else if (handle.kind === 'directory') {
yield* getFilesRecursively(handle)
}
}
}
for await (const file of getFilesRecursively(dirHandle)) {
files.push(file)
}
return files
} catch (err) {
if (err.name === 'AbortError') {
console.log('User cancelled directory selection')
} else if (err.name === 'SecurityError') {
console.error('Permission denied')
} else {
console.error('Error accessing directory:', err)
}
return []
}
}
Integration von Drag and Drop
Integrieren Sie Drag and Drop, um die Dateiauswahl angenehmer zu gestalten. Das folgende Beispiel zeigt die Typprüfung für abgelegte Elemente und verarbeitet sowohl Datei-Handles als auch Standard-Dateiobjekte:
function setupDragAndDrop(dropZone) {
dropZone.addEventListener('dragover', (e) => {
e.preventDefault()
e.dataTransfer.dropEffect = 'copy'
dropZone.classList.add('drag-active')
})
dropZone.addEventListener('dragleave', () => {
dropZone.classList.remove('drag-active')
})
dropZone.addEventListener('drop', async (e) => {
e.preventDefault()
dropZone.classList.remove('drag-active')
const items = Array.from(e.dataTransfer.items).filter((item) => item.kind === 'file')
try {
const handles = await Promise.all(
items.map((item) => item.getAsFileSystemHandle?.() ?? item.getAsFile()),
)
for (const handle of handles) {
if (!handle) continue
if (handle instanceof File) {
// Process a regular file dropped
console.log('Processing dropped file:', handle.name)
} else if (handle.kind === 'file') {
const file = await handle.getFile()
console.log('Processing file handle:', file.name)
} else if (handle.kind === 'directory') {
console.log('Processing directory:', handle.name)
}
}
} catch (err) {
console.error('Error processing dropped items:', err)
}
})
}
Umgang mit großen Dateien
Um den Speicherverbrauch zu begrenzen, verarbeiten Sie jeden Chunk, bevor Sie den nächsten lesen;
sammeln Sie Chunks nicht in einem neuen Blob. Übergeben Sie einen asynchronen Callback
consumeChunk, der die Verarbeitung oder das Senden jedes Chunks abschließt, bevor
er aufgelöst wird. Das steuert die Pufferung in der Anwendung, nicht die internen Puffer des
Browsers.
async function handleLargeFile(fileHandle, consumeChunk) {
const file = await fileHandle.getFile()
const reader = file.stream().getReader()
let totalSize = 0
try {
while (true) {
const { done, value } = await reader.read()
if (done) break
await consumeChunk(value)
totalSize += value.length
// Report progress
const progress = (totalSize / file.size) * 100
console.log(`Processing: ${progress.toFixed(2)}%`)
}
return totalSize
} catch (err) {
await reader.cancel(err).catch(() => {})
throw err
} finally {
reader.releaseLock()
}
}
Strategien zur Fehlerbehandlung
Sorgen Sie mit umfassender Fehlerbehandlung für robuste Dateioperationen. Die folgende Strategie prüft Berechtigungen, validiert den Dateizugriff und behandelt häufige Fehlerfälle:
async function safeFileOperation(handle) {
if (!handle) {
throw new Error('No file handle provided')
}
try {
const permissionGranted = await verifyPermissions(handle)
if (!permissionGranted) {
throw new DOMException('Permission denied', 'NotAllowedError')
}
const file = await handle.getFile()
if (!file) {
throw new Error('Could not access file')
}
return file
} catch (err) {
switch (err.name) {
case 'NotFoundError':
console.error('File no longer exists')
break
case 'SecurityError':
console.error('Permission denied')
break
case 'NotAllowedError':
console.error('User denied permission')
break
default:
console.error('Unexpected error:', err)
}
throw err
}
}
Die File System Access API verändert den webbasierten Umgang mit Dateien durch die direkte Integration in das lokale Dateisystem. Auch wenn die Browser-Unterstützung auf bestimmte Umgebungen beschränkt ist, sorgt Progressive Enhancement dafür, dass Ihre Nutzer plattformübergreifend ein zuverlässiges Erlebnis haben.
Für fortgeschrittene Implementierungen von Datei-Uploads lohnt sich ein Blick auf Werkzeuge wie Uppy und tus, die robuste, fortsetzbare Uploads ermöglichen.
