Persistent file handling with the file system access API

Traditional file upload mechanisms have long been constrained by the browser's sandbox. The typical
<input type="file">
approach forces you to re-select files after a page refresh and does not
expose direct file system paths. The File System Access API offers a modern solution by enabling
persistent file handles that support persistent file uploads and direct file system integration.
This means you may resume interrupted uploads without requiring users to reselect their files.
Because browser support varies, it is crucial to implement progressive enhancement via fallback
libraries.
Browser support and progressive enhancement
For optimal compatibility, use the browser-fs-access library, which provides a reliable fallback for browsers that do not support the native File System Access API. Currently, the API is supported in Chromium-based browsers (e.g., Chrome and Edge) from version 86 onward, while Brave offers experimental support behind a flag. In contrast, stable releases of Safari and Firefox do not support this API. By checking for native support, you can seamlessly switch between the API and the fallback.
import { fileOpen, fileSave, 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
}
}
Security considerations
The File System Access API enforces several security measures to protect user data:
- Permissions are origin-bound and automatically revoked when all tabs for the origin close.
- A secure context (HTTPS) is mandatory for production use.
- Write operations request explicit user consent before modifying files.
- Access remains restricted to specific system folders and does not allow cross-origin file access.
Implement robust permission checks as shown below. If write access is required, use
{ mode: 'readwrite' }
instead of { 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
}
}
Directory handling
Enabling directory uploads allows you to process entire file hierarchies. The following example recursively collects files from a user-selected directory:
async function handleDirectoryUpload() {
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 []
}
}
Drag and drop integration
Integrate drag and drop to improve the file selection experience. The example below demonstrates type checking for dropped items and processes both file handles and standard file objects:
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)
}
})
}
Large file handling
When working with large files, streaming data from the file minimizes memory consumption. The following example processes a file in manageable chunks while reporting progress:
async function handleLargeFile(fileHandle) {
try {
const file = await fileHandle.getFile()
const stream = file.stream()
const reader = stream.getReader()
const chunks = []
let totalSize = 0
while (true) {
const { done, value } = await reader.read()
if (done) break
chunks.push(value)
totalSize += value.length
// Report progress
const progress = (totalSize / file.size) * 100
console.log(`Processing: ${progress.toFixed(2)}%`)
}
return new Blob(chunks, { type: file.type })
} catch (err) {
console.error('Error processing file:', err)
throw err
}
}
Error handling strategies
Ensure your file operations are robust with comprehensive error handling. The strategy below checks permissions, validates file access, and addresses common error cases:
async function safeFileOperation(handle) {
if (!handle) {
throw new Error('No file handle provided')
}
try {
const permissionGranted = await verifyPermissions(handle)
if (!permissionGranted) {
throw new Error('Permission denied')
}
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
}
}
The File System Access API transforms web-based file handling with direct integration into the local file system. Although browser support is limited to certain environments, implementing progressive enhancement ensures that your users have a reliable experience across platforms.
For advanced file upload implementations, consider exploring tools like Uppy and Tus to enable resilient, resumable uploads.