file_system_access_api library

A Dart library to expose the File System Access API from web platform. You can pick files and directories from user's file system to read, write, create, move and delete files/directories from Dart web apps.

The web API (and this library) is experimental and is only supported on Chrome, Edge and Opera browsers for now. It is not recommended to use this library in a production environment.

Features

This library reflects the web File System Access API using Dart types. See a more detailed description on MDN Web Docs.

Usage

You can reproduce example from the web API as much of the interfaces are transparent between this Dart library and the JavaScript API.

Note that you must execute code samples within main() as a result of a user gesture (e.g. click event) for security reason. This part is omitted for brevity.

Is browser supported

You can check if current browser supports the File System Access API with:

void main() {
  if (!FileSystemAccess.supported) {
    print("Browser does not support API.");
    return;
  }
  print("Browser support API.");
}

Open file(s)

You can ask a user to open file(s) with the method window.showOpenFilePicker() and access selected files like this:

void main() async {
  try {
    List<FileSystemFileHandle> handles = await window.showOpenFilePicker(
        multiple: true,
        excludeAcceptAllOption: true,
        types: [FilePickerAcceptType(description: "Pictures", accept: {"image/png+jpg": [".png", ".jpg"]})],
        startIn: WellKnownDirectory.pictures
    );

    for (final handle in handles) {
      File file = await handle.getFile();

      print("<file name='${handle.name}' size='${file.size} bytes' />");

      // You can read content of a File using a FileReader, like any other File coming from 'dart:html'
    }
  } on AbortError {
    print("User dismissed dialog or picked a file deemed too sensitive or dangerous.");
  }
}

Save a file

You can ask a user where to save a file with the method window.showSaveFilePicker() and write in the selected file like this:

void main() async {
  try {
    FileSystemFileHandle handle = await window.showSaveFilePicker(
      suggestedName: "awesome.dart",
      excludeAcceptAllOption: true,
      types: [FilePickerAcceptType(description: "Dart files", accept: {"application/dart": [".dart"]})],
      startIn: WellKnownDirectory.documents
    );

    FileSystemWritableFileStream stream = await handle.createWritable();

    await stream.writeAsText(
      "void main() {"
      "  print('This is the way.');"
      "}"
    );
    await stream.close();
    // Note that data will be written on disk only after call to close() completed.
  } on AbortError {
    print("User dismissed dialog or picked a file deemed too sensitive or dangerous.");
  } on NotAllowedError {
    print("User did not granted permission to readwrite in this file.");
  }
}

Open a directory

You can ask a user to pick a directory window.showDirectoryPicker() and recursively access files and directories like this:

void main() async {
  try {
    FileSystemDirectoryHandle directory = await window.showDirectoryPicker(
      // Use readwrite to ask permission and grant write access on files instead.
      mode: PermissionMode.read
    );

    // List of handles in a directory emitted with a [Stream].
    // Listen periodically on [Stream] to reproduce a file system watch-like feature.
    await for (FileSystemHandle handle in directory.values) {
      if (handle.kind == FileSystemKind.file) {
        print("<file name='${handle.name}' />");
      } else if (handle.kind == FileSystemKind.directory) {
        print("<directory name='${handle.name}/' />");
        // You can create, move and delete files/directories. See example/ for more on this.
      }
    }
  } on AbortError {
    print("User dismissed dialog or picked a directory deemed too sensitive or dangerous.");
  }
}

Permissions

You can query / request permission on files and directories from the user. It allows you to write in files, modify a directory structure, etc. Those methods are available per file/directory handle:

void main() async {
  try {
    final handles = await window.showOpenFilePicker(multiple: false);
    final handle = handles.single;

    var permission = await handle.queryPermission(mode: PermissionMode.readwrite);

    if (permission != PermissionState.granted) {
      print("Asking permission to read and write");
      permission = await handle.requestPermission(mode: PermissionMode.readwrite);
      if (permission != PermissionState.granted) {
        print("read/write access refused, either prompt was dismissed or denied.");
        return;
      }
      print("read/write access granted. 😀");
    }
    print("Write beautiful bytes. ✨");
  } on AbortError {
    print("User dismissed dialog or picked a file deemed too sensitive or dangerous.");
  }
}

Note that current implementation of the File System Access API does not remember permissions across browser's sessions when used with IndexedDB. Star crbug.com/1011533 to be notified of work on persisting granted permissions.

Drag and Drop

You can get files/directories from a drag-n-drop event:

void main() {
  final $area = document.querySelector("#area") as DivElement;

  $area.addEventListener("dragover", (event) => event.preventDefault());
  $area.addEventListener("drop", (event) async {
    event.preventDefault();

    final handles = await FileSystemAccess.fromDropEvent(event);

    for (final handle in handles) {
      print("<${handle.kind.name} name='${handle.name}' />");
    }
  });
}

How to cast a FileSystemHandle

DON'T test and cast using is keyword:

FileSystemHandle handle;

if (handle is FileSystemFileHandle) {
  print("will always return false");
} else if (handle is FileSystemDirectoryHandle) {
  print("will always return false");
}

DO test and cast using FileSystemHandle.kind:

FileSystemHandle handle;

if (handle.kind == FileSystemKind.file) {
  final file = handle as FileSystemFileHandle;

  print("handle is a file");
} else if (handle.kind == FileSystemKind.directory) {
  final directory = handle as FileSystemDirectoryHandle;

  print("handle is a directory.");
}

How to compare FileSystemHandle(s)

DON'T compare using == operator or hashCode property:

FileSystemFileHandle a;
FileSystemFileHandle b;

if (a == b) {
  print("will return false even when a and b represents the same file entry.");
}

DO compare using FileSystemHandle.isSameEntry:

void main() async {
FileSystemFileHandle a;
FileSystemFileHandle b;

if (await a.isSameEntry(b)) {
  print("return true when a and b represents the same file entry.");
}
}

More

See examples in example/ folder to play with fun tools.

Origin Private File System

This is a storage endpoint private to the origin of the page. It contains files/directories within a "virtual disk" chosen by the browser implementing this API. Files and directories may be written in a database or any other data structure. You should not expect to find those files/directories as-this on the hard disk.

Get root directory

You can get a root directory of the origin private file system with:

void main() async {
  FileSystemDirectoryHandle? root = await window.navigator.storage?.getDirectory();
}

Rename a file

Applies only within Origin Private File System for now.

You can rename a file with:

void main() async {
  FileSystemDirectoryHandle root;

  FileSystemFileHandle handle = await root.getFileHandle("some-name.txt");

  await handle.rename("new-name.txt");
}

Move a file

Applies only within Origin Private File System for now.

You can move a file inside another directory and optionally rename it in the same call with:

void main() async {
  FileSystemDirectoryHandle root;

  FileSystemFileHandle handle = await root.getFileHandle("some-name.txt");
  FileSystemDirectoryHandle destination = await root.getDirectoryHandle("config", create: true);

  // Move only
  await handle.move(destination);
  // Move and rename
  await handle.move(destination, name: "new-name.txt");
}

Synchronous access in Web Workers

Applies only within a Web Worker and Origin Private File System for now.

You can get an interface to synchronously read from and write to a file. This will create an exclusive lock on the file associated with the file handle, until it is closed:

// Code executed within a Web Worker
void main() async {
  FileSystemDirectoryHandle root;

  FileSystemFileHandle source = await root.getFileHandle("linux.iso");
  FileSystemSyncAccessHandle src = await source.createSyncAccessHandle();
  Uint8List buffer = Uint8List(src.getSize());

  src.read(buffer);
  src.close();
  print(buffer);
}

Missing features

Origin Private File System

There is no wrapper around this JavaScript feature for now:

  • FileSystemHandle.remove()

Known issues

  • You cannot store a FileSystemHandle into IndexedDB as-this. See issue #50621 for more. A workaround is implemented in LightStorage within example/ folder.

File any potential issues you see.

Additional information

See this article on web.dev for an introduction to this API (JavaScript).

See examples and more on MDN Web Docs (JavaScript).

See specification on W3C WICG’s File System Access and WHATWG's File System.

Classes

AbortError
Thrown if a user dismisses the prompt without making a selection or if a file selected is deemed too sensitive or dangerous to be exposed to the website.
FilePickerAcceptType
Consists of an optional description and a number of MIME types and extensions (accept). If no description is provided one will be generated. Extensions have to be strings that start with a "." and only contain valid suffix code points. Additionally extensions are limited to a length of 16 characters.
FileSystemAccess
Helpers to bind native JavaScript objects with Dart types of this library.
FileSystemDirectoryHandle
FileSystemFileHandle
FileSystemHandle
FileSystemSyncAccessHandle
FileSystemWritableFileStream
InvalidModificationError
Thrown if recursive is set to false and the entry to be removed has children.
InvalidStateError
Thrown if the FileSystemSyncAccessHandle object does not represent a file in the origin private file system.
MalformedNameError
Thrown if the name specified is not a valid string or contains characters not allowed on the file system.
NoModificationAllowedError
Thrown if the browser is not able to acquire a lock on the file associated with the file handle.
NotAllowedError
Thrown if you try to access a file while PermissionState is not granted by user.
NotFoundError
Thrown if file doesn't exist and the create option is set to false or the requested file / directory could not be found at the time an operation call was processed.
QuotaExceededError
Thrown if the newSize is larger than the original size of the file, and exceeds the browser's storage quota.
SecurityError
Thrown if a file / directory picker is shown without prior user gesture (e.g. click event).
TypeMismatchError
Thrown if the named entry is a directory and not a file when using FileSystemDirectoryHandle.getFileHandle. Thrown if the named entry is a file and not a directory when using FileSystemDirectoryHandle.getDirectoryHandle.
WritableStream
WritableStreamDefaultWriter

Enums

FileSystemKind
It represents a FileSystemHandle.kind. It can be a file or a directory.
PermissionMode
Defines mode to interact with a file using read-only access or read and write access.
PermissionState
A permission represents a user's decision to allow a web application to use a powerful feature. Conceptually, a permission for a powerful feature can be in one of the following states: granted, denied or prompt.
WellKnownDirectory
Lets a website pick one of several well-known directories when used with file picker / directory picker.

Extensions

JSFileSystemDirectoryHandle on FileSystemDirectoryHandle
Provides a handle to a file system directory. The interface is accessed via the window.showDirectoryPicker method.
JSFileSystemFileHandle on FileSystemFileHandle
Represents a handle to a file system entry. The interface is accessed through the window.showOpenFilePicker method.
JSFileSystemHandle on FileSystemHandle
Represents a file or directory entry. Multiple handles can represent the same entry. For the most part you do not work with FileSystemHandle directly but rather its child interfaces FileSystemFileHandle and FileSystemDirectoryHandle.
JSFileSystemSyncAccessHandle on FileSystemSyncAccessHandle
Represents a synchronous handle to a file system entry. The synchronous nature of the file reads and writes allows for higher performance for critical methods in contexts where asynchronous operations come with high overhead, e.g. WebAssembly.
JSFileSystemWritableFileStream on FileSystemWritableFileStream
A WritableStream object with additional convenience methods, which operates on a single file on disk. The interface is accessed through the FileSystemFileHandle.createWritable method.
JSWritableStream on WritableStream
Provides a standard abstraction for writing streaming data to a destination, known as a sink. This object comes with built-in backpressure and queuing.
JSWritableStreamDefaultWriter on WritableStreamDefaultWriter
Is the object returned by WritableStream.getWriter and once created locks the writer to the WritableStream ensuring that no other streams can write to the underlying sink.
StorageManagerOriginPrivateFileSystem on StorageManager
WindowFileSystemAccess on Window