Featured image of post File System Access API: Powerful Local File Operations Featured image of post File System Access API: Powerful Local File Operations

File System Access API: Powerful Local File Operations

Explore the File System Access API: showOpenFilePicker, showSaveFilePicker, showDirectoryPicker, streaming writes, permissions, and IDE-like app patterns.

Introduction

For decades, web applications were limited to reading files through a clunky <input type="file"> element and had no reliable way to save changes back to the original file. The File System Access API changes this entirely. It enables web applications to read, write, and manage files and directories directly on the user’s local file system — with user permission, of course. This opens the door to building text editors, image editors, IDEs, and productivity tools that feel native.


Opening Files with showOpenFilePicker

The window.showOpenFilePicker() method returns a promise resolving to an array of FileSystemFileHandle objects. You can configure it with options for multiple selection, file type filtering, and MIME type restrictions:

const [fileHandle] = await window.showOpenFilePicker({
  types: [{
    description: 'Markdown Files',
    accept: { 'text/markdown': ['.md'] }
  }],
  multiple: false
});

const file = await fileHandle.getFile();
const content = await file.text();
document.querySelector('#editor').value = content;

The FileSystemFileHandle interface provides two key methods: getFile() for reading and createWritable() for writing. Reading supports file.text(), file.arrayBuffer(), and file.stream() depending on the data type. Handle errors gracefully — user cancellation throws AbortError, while permission revocation throws NotAllowedError.


Saving Files with showSaveFilePicker

For saving, window.showSaveFilePicker() returns a FileSystemFileHandle pointing to the user-selected save location. Writing involves creating a writable stream, writing content, and closing the stream:

const fileHandle = await window.showSaveFilePicker({
  suggestedName: 'document.md',
  types: [{
    description: 'Markdown Files',
    accept: { 'text/markdown': ['.md'] }
  }]
});

const writable = await fileHandle.createWritable();
await writable.write(editorContent);
await writable.close();

For large files, use streaming writes with the WriteParams object to write in chunks without loading the entire file into memory:

const writable = await fileHandle.createWritable();
const encoder = new TextEncoder();
const chunkSize = 1024 * 1024; // 1MB chunks

for (let offset = 0; offset < content.length; offset += chunkSize) {
  const chunk = content.slice(offset, offset + chunkSize);
  await writable.write({ type: 'write', data: encoder.encode(chunk), position: offset });
}
await writable.close();

The WriteParams object also supports type: 'truncate' to resize the file and type: 'seek' to reposition the write cursor, giving you precise control over file content.


Working with Directories

The window.showDirectoryPicker() method returns a FileSystemDirectoryHandle, enabling recursive traversal and modification of directory structures:

const dirHandle = await window.showDirectoryPicker();

async function traverseDirectory(dirHandle, path = '') {
  for await (const [name, handle] of dirHandle.entries()) {
    const fullPath = `${path}/${name}`;
    if (handle.kind === 'directory') {
      console.log(`📁 ${fullPath}`);
      await traverseDirectory(handle, fullPath);
    } else {
      console.log(`📄 ${fullPath}`);
    }
  }
}

await traverseDirectory(dirHandle);

Creating new files and directories is straightforward:

const newFile = await dirHandle.getFileHandle('notes.md', { create: true });
const newDir = await dirHandle.getDirectoryHandle('images', { create: true });
await dirHandle.removeEntry('temp.log', { recursive: true });

Permissions Model

The File System Access API requires transient activation — the picker methods must be called in response to a user gesture like a click or keypress. Permissions on file and directory handles persist until the page is reloaded or the handle is garbage collected. For longer-lived access, you can check and request permissions explicitly:

async function requestWritePermission(fileHandle) {
  const options = { mode: 'readwrite' };
  if ((await fileHandle.queryPermission(options)) === 'granted') {
    return true;
  }
  return (await fileHandle.requestPermission(options)) === 'granted';
}

File handles can be serialized and stored in IndexedDB for persistent access across sessions — this is how IDE-like applications remember previously opened projects. Note that private browsing mode clears stored handles, and cross-origin iframes cannot access the API.


Browser Support and Fallbacks

FeatureChromeEdgeFirefoxSafari
File System Access API86+86+
File Handling API102+102+
Origin Private File System86+86+

Always use feature detection and provide fallbacks:

if ('showOpenFilePicker' in window) {
  // Use File System Access API
} else {
  // Fallback to <input type="file"> for reading
  const input = document.createElement('input');
  input.type = 'file';
  input.click();
}

For saving in unsupported browsers, fall back to <a download> with a Blob URL. The File Handling API (Chrome 102+) lets you register your web app as a file handler via the Web App Manifest, enabling users to open files directly with your app from the operating system.


Advanced Use Cases

The File System Access API enables patterns previously reserved for native applications:

  • IDE-like applications: Open a project directory, read and write individual files, create new files, rename and delete entries.
  • Markdown editor with live preview: Open .md files, edit with real-time preview, and save back to the original file.
  • Image editor with Save and Save As: Use showSaveFilePicker for Save As and createWritable() for in-place Save.
  • Bulk file processing: Open a directory, iterate over files, apply transformations, and save results — all without downloading or uploading.

For sandboxed storage that works without user gestures, use the Origin Private File System via navigator.storage.getDirectory(). This provides a virtual file system scoped to your origin, useful for caches and offline data.


Performance Considerations

When working with large files, always prefer streaming approaches:

  • Use file.stream() for reading large files instead of file.text() or file.arrayBuffer().
  • Write in chunks (e.g., 1MB) to avoid memory pressure.
  • Use the entries() iterator for directories with thousands of files instead of collecting all entries at once.
  • Debounce auto-save operations — keep the writable stream open and write periodically, but close it only when done.
  • The Origin Private File System (navigator.storage.getDirectory()) offers sandboxed storage without permission prompts, ideal for app-internal data.

Conclusion

The File System Access API transforms web applications from read-only content consumers into full-featured creative tools. While currently limited to Chromium-based browsers, the capability gap it fills is enormous. Always use feature detection and provide fallbacks using traditional <input type="file"> and <a download> patterns. For productivity apps, creative tools, and developer tools that need native file capabilities, this API eliminates the need for Electron or other native wrappers.