[go: up one dir, main page]

File System Access

Draft Community Group Report,

This version:
https://wicg.github.io/file-system-access/
Issue Tracking:
GitHub
Inline In Spec
Editor:
(Google)
Former Editor:
(Google)

Abstract

This document extends the API in [FS] to enable developers to build powerful web apps that interact with files on the user’s local device. It builds on File API for file reading capabilities, and adds new API surface to enable modifying files, as well as working with directories.

Status of this document

This specification was published by the Web Platform Incubator Community Group. It is not a W3C Standard nor is it on the W3C Standards Track. Please note that under the W3C Community Contributor License Agreement (CLA) there is a limited opt-out and other conditions apply. Learn more about W3C Community and Business Groups.

1. Introduction

This section is non-normative.

This API enables developers to build powerful apps that interact with other (non-Web) apps on the user’s device via the device’s file system. Prominent examples of applications where users expect this functionality are IDEs, photo and video editors, text editors, and more. After a user grants a web app access, this API allows the app to read or save changes directly to files and folders on the user’s device. Beyond reading and writing files, this API provides the ability to open a directory and enumerate its contents. Additionally, web apps can use this API to store references to files and directories they’ve been given access to, allowing the web apps to later regain access to the same content without requiring the user to select the same file again.

This API is similar to <input type=file> and <input type=file webkitdirectory> [entries-api] in that user interaction happens through file and directory picker dialogs. Unlike those APIs, this API is currently purely a javascript API, and does not integrate with forms and/or input elements.

This API extends the API in [FS], which specifies a bucket file system which websites can get access to without having to first prompt the user for access.

2. Files and Directories

2.1. Concepts

A valid suffix code point is a code point that is ASCII alphanumeric, U+002B (+), or U+002E (.).

Note: These code points were chosen to support most pre-existing file formats. The vast majority of file extensions are purely alphanumeric, but compound extensions (such as .tar.gz) and extensions such as .c++ for C++ source code are also fairly common, hence the inclusion of + and . as allowed code points.

2.2. Permissions

The "file-system" powerful feature's permission-related algorithms and types are defined as follows:

permission descriptor type

FileSystemPermissionDescriptor, defined as:

enum FileSystemPermissionMode {
  "read",
  "readwrite"
};

dictionary FileSystemPermissionDescriptor : PermissionDescriptor {
  required FileSystemHandle handle;
  FileSystemPermissionMode mode = "read";
};
permission state constraints
To determine permission state constraints for a FileSystemPermissionDescriptor desc, run these steps:
  1. Let entry be desc["handle"]'s entry.

  2. If entry represents a file system entry in a bucket file system, this descriptor’s permission state must always be "granted".

  3. Otherwise, if entry’s parent is not null, this descriptor’s permission state must be equal to the permission state for a descriptor with the same mode, and a handle representing entry’s parent.

  4. Otherwise, if desc["mode"] is "readwrite":

    1. Let read state be the permission state for a descriptor with the same handle, but whose mode is "read".

    2. If read state is not "granted", this descriptor’s permission state must be equal to read state.

Make these checks no longer associated with an entry. [Issue #whatwg/fs#101]

permission request algorithm
Given a FileSystemPermissionDescriptor desc and a PermissionStatus status, run these steps:
  1. Run the default permission query algorithm on desc and status.

  2. If status’s state is not "prompt", then abort these steps.

  3. Let settings be desc["handle"]'s relevant settings object.

  4. Let global be settings’s global object.

  5. If global is not a Window, then throw a "SecurityError" DOMException.

  6. If global does not have transient activation, then throw a "SecurityError" DOMException.

  7. If settings’s origin is not same origin with settings’s top-level origin, then throw a "SecurityError" DOMException.

  8. Request permission to use desc.

  9. Run the default permission query algorithm on desc and status.

Ideally this user activation requirement would be defined upstream. [Issue #WICG/permissions-request#2]

To query file system permission given a FileSystemHandle handle and a FileSystemPermissionMode mode, run these steps:
  1. Let desc be a FileSystemPermissionDescriptor.

  2. Set desc["name"] to "file-system".

  3. Set desc["handle"] to handle.

  4. Set desc["mode"] to mode.

  5. Return desc’s permission state.

To request file system permission given a FileSystemHandle handle and a FileSystemPermissionMode mode, run these steps:
  1. Let desc be a FileSystemPermissionDescriptor.

  2. Set desc["name"] to "file-system".

  3. Set desc["handle"] to handle.

  4. Set desc["mode"] to mode.

  5. Let status be the result of running create a PermissionStatus for desc.

  6. Run the permission request algorithm for the "file-system" feature, given desc and status.

  7. Return desc’s permission state.

Currently FileSystemPermissionMode can only be "read" or "readwrite". In the future we might want to add a "write" mode as well to support write-only handles. [Issue #119]

2.3. The FileSystemHandle interface

dictionary FileSystemHandlePermissionDescriptor {
  FileSystemPermissionMode mode = "read";
};

[Exposed=(Window,Worker), SecureContext, Serializable]
partial interface FileSystemHandle {
  Promise<PermissionState> queryPermission(optional FileSystemHandlePermissionDescriptor descriptor = {});
  Promise<PermissionState> requestPermission(optional FileSystemHandlePermissionDescriptor descriptor = {});
};

2.3.1. The queryPermission() method

status = await handle . queryPermission({ mode : "read" })
status = await handle . queryPermission()
status = (await navigator.permissions.query({ name : "file-system", handle : handle })).state

Queries the current state of the read permission of this handle. If this returns "prompt" the website will have to call requestPermission() before any operations on the handle can be done. If this returns "denied" any operations will reject.

Usually handles returned by the local file system handle factories will initially return "granted" for their read permission state, however other than through the user revoking permission, a handle retrieved from IndexedDB is also likely to return "prompt".

status = await handle . queryPermission({ mode : "readwrite" })
status = (await navigator.permissions.query({ name : "file-system", handle : handle, mode : "readwrite" }).state

Queries the current state of the write permission of this handle. If this returns "prompt", attempting to modify the file or directory this handle represents will require user activation and will result in a confirmation prompt being shown to the user. However if the state of the read permission of this handle is also "prompt" the website will need to call requestPermission(). There is no automatic prompting for read access when attempting to read from a file or directory.

The integration with the permissions API’s query() method is not yet implemented in Chrome.

The queryPermission(descriptor) method, when invoked, must run these steps:
  1. Let result be a new promise.

  2. Run the following steps in parallel:

    1. Let state be the result of querying file system permission given this and descriptor["mode"].

    2. Resolve result with state.

  3. Return result.

2.3.2. The requestPermission() method

status = await handle . requestPermission({ mode : "read" })
status = await handle . requestPermission()

If the state of the read permission of this handle is anything other than "prompt", this will return that state directly. If it is "prompt" however, user activation is needed and this will show a confirmation prompt to the user. The new read permission state is then returned, depending on the user’s response to the prompt.

status = await handle . requestPermission({ mode : "readwrite" })

If the state of the write permission of this handle is anything other than "prompt", this will return that state directly. If the status of the read permission of this handle is "denied" this will return that.

Otherwise the state of the write permission is "prompt" and this will show a confirmation prompt to the user. The new write permission state is then returned, depending on what the user selected.

The requestPermission(descriptor) method, when invoked, must run these steps:
  1. Let result be a new promise.

  2. Run the following steps in parallel:

    1. Let state be the result of requesting file system permission given this and descriptor["mode"]. If that throws an exception, reject result with that exception and abort.

    2. Resolve result with state.

  3. Return result.

3. Accessing Local File System

enum WellKnownDirectory {
  "desktop",
  "documents",
  "downloads",
  "music",
  "pictures",
  "videos",
};

typedef (WellKnownDirectory or FileSystemHandle) StartInDirectory;

dictionary FilePickerAcceptType {
    USVString description = "";
    record<USVString, (USVString or sequence<USVString>)> accept;
};

dictionary FilePickerOptions {
    sequence<FilePickerAcceptType> types;
    boolean excludeAcceptAllOption = false;
    DOMString id;
    StartInDirectory startIn;
};

dictionary OpenFilePickerOptions : FilePickerOptions {
    boolean multiple = false;
};

dictionary SaveFilePickerOptions : FilePickerOptions {
    USVString? suggestedName;
};

dictionary DirectoryPickerOptions {
    DOMString id;
    StartInDirectory startIn;
    FileSystemPermissionMode mode = "read";
};

[SecureContext]
partial interface Window {
    Promise<sequence<FileSystemFileHandle>> showOpenFilePicker(optional OpenFilePickerOptions options = {});
    Promise<FileSystemFileHandle> showSaveFilePicker(optional SaveFilePickerOptions options = {});
    Promise<FileSystemDirectoryHandle> showDirectoryPicker(optional DirectoryPickerOptions options = {});
};

The showOpenFilePicker(), showSaveFilePicker() and showDirectoryPicker() methods are together known as the local file system handle factories.

Note: What is referred to as the "local file system" in this spec, does not have to strictly refer to the file system on the local device. What we call the local file system could just as well be backed by a cloud provider. For example on Chrome OS these file pickers will also let you pick files and directories on Google Drive.

In Chrome versions earlier than 85, this was implemented as a generic chooseFileSystemEntries method.

3.1. Local File System Permissions

The fact that the user picked the specific files returned by the local file system handle factories in a prompt should be treated by the user agent as the user intending to grant read access to the website for the returned files. As such, at the time the promise returned by one of the local file system handle factories resolves, permission state for a descriptor with handle set to the returned handle, and mode set to "read" should be "granted".

Additionally for calls to showSaveFilePicker the permission state for a descriptor with handle set to the returned handle, and mode set to "readwrite" should be "granted".

To verify that an environment is allowed to show a file picker, run these steps:
  1. If environment’s origin is an opaque origin, return a promise rejected with a "SecurityError" DOMException.

  2. If environment’s origin is not same origin with environment’s top-level origin, return a promise rejected with a "SecurityError" DOMException.

  3. Let global be environment’s global object.

  4. If global does not have transient activation, then throw a "SecurityError" DOMException.

3.2. File picker options

3.2.1. Accepted file types

The showOpenFilePicker(options) and showSaveFilePicker(options) methods accept a FilePickerOptions argument, which lets the website specify the types of files the file picker will let the user select.

Each entry in types specifies a single user selectable option for filtering the files displayed in the file picker.

Each option consists of an optional description and a number of MIME types and extensions (specified as a mapping of MIME type to a list of extensions). 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 code points.

In addition to complete MIME types, "*" can be used as the subtype of a MIME type to match for example all image formats with "image/*".

Websites should always provide both MIME types and file extensions for each option. On platforms that only use file extensions to describe file types user agents can match on the extensions, while on platforms that don’t use extensions, user agents can match on MIME type.

By default the file picker will also include an option to not apply any filter, letting the user select any file. Set excludeAcceptAllOption to true to not include this option in the file picker.

For example , the following options will let the user pick one of three different filters. One for text files (either plain text or HTML), one for images, and a third one that doesn’t apply any filter and lets the user select any file.

const options = {
  types: [
    {
      description: 'Text Files',
      accept: {
        'text/plain': ['.txt', '.text'],
        'text/html': ['.html', '.htm']
      }
    },
    {
      description: 'Images',
      accept: {
        'image/*': ['.png', '.gif', '.jpeg', '.jpg']
      }
    }
  ],
};

On the other hand, the following example will only let the user select SVG files. The dialog will not show an option to not apply any filters.

const options = {
  types: [
    {
      accept: {
        'image/svg+xml': '.svg'
      }
    },
  ],
  excludeAcceptAllOption: true
};
To process accept types, given FilePickerOptions options, run these steps:
  1. Let accepts options be a empty list of tuples consisting of a description and a filter.

  2. For each type of options["types"]:

    1. For each typeStringsuffixes of type["accept"]:

      1. Let parsedType be the result of parse a MIME type with typeString.

      2. If parsedType is failure, then throw a TypeError.

      3. If parsedType’s parameters are not empty, then throw a TypeError.

      4. If suffixes is a string:

        1. Validate a suffix given suffixes.

      5. Otherwise, for each suffix of suffixes:

        1. Validate a suffix given suffix.

    2. Let filter be these steps, given a filename (a string) and a type (a MIME type):

      1. For each typeStringsuffixes of type["accept"]:

      2. Let parsedType be the result of parse a MIME type with typeString.

        1. If parsedType’s subtype is "*":

          1. If parsedType’s type is "*", return true.

          2. If parsedType’s type is type’s type, return true.

        2. parsedType’s essence is type’s essence, return true.

        3. If suffixes is a string, set suffixes to « suffixes ».

        4. For each suffix of suffixes:

          1. If filename ends with suffix, return true.

      3. Return false.

    3. Let description be type["description"].

    4. If description is an empty string, set description to some user understandable string describing filter.

    5. Append (description, filter) to accepts options.

  3. If either accepts options is empty, or options["excludeAcceptAllOption"] is false:

    1. Let description be a user understandable string describing "all files".

      1. Let filter be an algorithm that returns true.

      2. Append (description, filter) to accepts options.

  4. If accepts options is empty, then throw a TypeError.

  5. Return accepts options.

To validate a suffix suffix, run the following steps:
  1. If suffix does not start with ".", then throw a TypeError.

  2. If suffix contains any code points that are not valid suffix code points, then throw a TypeError.

  3. If suffix ends with ".", then throw a TypeError.

  4. If suffix’s length is more than 16, then throw a TypeError.

3.2.2. Starting Directory

The id and startIn fields can be specified to suggest the directory in which the file picker opens.

If neither of these options are specified, the user agent remembers the last directory a file or directory was picked from, and new pickers will start out in that directory. By specifying an id the user agent can remember different directories for different IDs (user agents will only remember directories for a limited number of IDs).

// If a mapping exists from this ID to a previousy picked directory, start in
// this directory. Otherwise, a mapping will be created from this ID to the
// directory of the resulting file picker invocation.
const options = {
  id: 'foo',
};

Specifying startIn as a FileSystemFileHandle will result in the dialog starting in the parent directory of that file, while passing in a FileSystemDirectoryHandle will result in the dialog to start in the passed in directory. These take precedence even if an explicit id is also passed in.

For example, given a FileSystemDirectoryHandle project_dir, the following will show a file picker that starts out in that directory:

// The picker will open to the directory of |project_dir| regardless of whether
// 'foo' has a valid mapping.
const options = {
  id: 'foo',
  startIn: |project_dir|,
};

The id and startIn fields control only the directory the picker opens to. In the above example, it cannot be assumed that the id 'foo' will map to the same directory as project_dir once the file picker operation has completed.

Specifying startIn as a WellKnownDirectory will result in the dialog starting in that directory, unless an explicit id was also passed in which has a mapping to a valid directory.

Below is an example of specifying both an id and startIn as a WellKnownDirectory. If there is an existing mapping from the given ID to a path, this mapping is used. Otherwise, the path suggested via the WellKnownDirectory is used.

// First time specifying the ID 'foo'. It is not mapped to a directory.
// The file picker will fall back to opening to the Downloads directory. TODO: link this.
const options = {
  id: 'foo',  // Unmapped.
  startIn: "downloads",  // Start here.
};

// Later...

// The ID 'foo' might or might not be mapped. For example, the mapping for this ID
// might have been evicted.
const options = {
  id: 'foo',  // Maybe mapped. If so, start here.
  startIn: "downloads",  // Otherwise, start here.
};

The startIn and id options were first introduced in Chrome 91.

A user agent holds a recently picked directory map, which is a map of origins to path id maps.

A path id map is a map of valid path ids to paths.

A valid path id is a string where each character is ASCII alphanumeric or "_" or "-".

To prevent a path id map from growing without a bound, user agents should implement some mechanism to limit how many recently picked directories will be remembered. This can for example be done by evicting least recently used entries. User agents should allow at least 16 entries to be stored in a path id map.

The WellKnownDirectory enum lets a website pick one of several well-known directories. The exact paths the various values of this enum map to is implementation-defined (and in some cases these might not even represent actual paths on disk). The following list describes the meaning of each of the values, and gives possible example paths on different operating systems:

"desktop"

The user’s Desktop directory, if such a thing exists. For example this could be C:\Documents and Settings\username\Desktop, /Users/username/Desktop, or /home/username/Desktop.

"documents"

Directory in which documents created by the user would typically be stored. For example C:\Documents and Settings\username\My Documents, /Users/username/Documents, or /home/username/Documents.

"downloads"

Directory where downloaded files would typically be stored. For example C:\Documents and Settings\username\Downloads, /Users/username/Downloads, or /home/username/Downloads.

"music"

Directory where audio files would typically be stored. For example C:\Documents and Settings\username\My Documents\My Music, /Users/username/Music, or /home/username/Music.

"pictures"

Directory where photos and other still images would typically be stored. For example C:\Documents and Settings\username\My Documents\My Pictures, /Users/username/Pictures, or /home/username/Pictures.

"videos"

Directory where videos/movies would typically be stored. For example C:\Documents and Settings\username\My Documents\My Videos, /Users/username/Movies, or /home/username/Videos.

To determine the directory the picker will start in, given an optional string id, an optional StartInDirectory startIn and an environment settings object environment, run the following steps:
  1. If id given, and is not a valid path id, then throw a TypeError.

  2. If id’s length is more than 32, then throw a TypeError.

  3. Let origin be environment’s origin.

  4. If startIn is a FileSystemHandle and startIn is not in a bucket file system:

    1. Let entry be the result of locating an entry given startIn’s locator.

    2. If entry is a file entry, return the path of entry’s parent in the local file system.

    3. If entry is a directory entry, return entry’s path in the local file system.

  5. If id is non-empty:

    1. If recently picked directory map[origin] exists:

      1. Let path map be recently picked directory map[origin].

      2. If path map[id] exists, then return path map[id].

  6. If startIn is a WellKnownDirectory:

    1. Return a user agent defined path corresponding to the WellKnownDirectory value of startIn.

  7. If id is not specified, or is an empty string:

    1. If recently picked directory map[origin] exists:

      1. Let path map be recently picked directory map[origin].

      2. If path map[""] exists, then return path map[""].

  8. Return a default path in a user agent specific manner.

To remember a picked directory, given an optional string id, a file system entry entry, and an environment settings object environment, run the following steps:
  1. Let origin be environment’s origin.

  2. If recently picked directory map[origin] does not exist, then set recently picked directory map[origin] to an empty path id map.

  3. If id is not specified, let id be an empty string.

  4. Set recently picked directory map[origin][id] to the path on the local file system corresponding to entry, if such a path can be determined.

3.3. The showOpenFilePicker() method

[ handle ] = await window . showOpenFilePicker()
[ handle ] = await window . showOpenFilePicker({ multiple: false })

Shows a file picker that lets a user select a single existing file, returning a handle for the selected file.

handles = await window . showOpenFilePicker({ multiple: true })

Shows a file picker that lets a user select multiple existing files, returning handles for the selected files.

Additional options can be passed to showOpenFilePicker() to indicate the types of files the website wants the user to select and the directory in which the file picker will open. See § 3.2 File picker options for details.

The showOpenFilePicker(options) method, when invoked, must run these steps:
  1. Let environment be this’s relevant settings object.

  2. Let accepts options be the result of processing accept types given options.

  3. Let starting directory be the result of determining the directory the picker will start in given options["id"], options["startIn"], and environment.

  4. Let global be environment’s global object.

  5. Verify that environment is allowed to show a file picker.

  6. Let p be a new promise.

  7. Run the following steps in parallel:

    1. Optionally, wait until any prior execution of this algorithm has terminated.

    2. Display a prompt to the user requesting that the user pick some files. If options["multiple"] is false, there must be no more than one file selected; otherwise any number may be selected.

      The displayed prompt should let the user pick one of the accepts options to filter the list of displayed files. Exactly how this is implemented, and what this prompt looks like is implementation-defined.

      When possible, this prompt should start out showing starting directory.

    3. Wait for the user to have made their selection.

    4. If the user dismissed the prompt without making a selection, reject p with an "AbortError" DOMException and abort.

    5. Let entries be a list of file entries representing the selected files or directories.

    6. Let result be a empty list.

    7. For each entry of entries:

      1. If entry is deemed too sensitive or dangerous to be exposed to this website by the user agent:

        1. Inform the user that the selected files or directories can’t be exposed to this website.

        2. At the discretion of the user agent, either go back to the beginning of these in parallel steps, or reject p with an "AbortError" DOMException and abort.

      2. Add a new FileSystemFileHandle associated with entry to result.

    8. Remember a picked directory given options["id"], entries[0] and environment.

    9. Perform the activation notification steps in global’s browsing context.

      Note: This lets a website immediately perform operations on the returned handles that might require user activation, such as requesting more permissions.

    10. Resolve p with result.

  8. Return p.

3.4. The showSaveFilePicker() method

handle = await window . showSaveFilePicker( options )

Shows a file picker that lets a user select a single file, returning a handle for the selected file. The selected file does not have to exist already. If the selected file does not exist a new empty file is created before this method returns, otherwise the existing file is cleared before this method returned.

handle = await window . showSaveFilePicker({ suggestedName: "README.md" })

Shows a file picker with the suggested "README.md" file name pre-filled as the default file name to save as.

Additional options can be passed to showSaveFilePicker() to indicate the types of files the website wants the user to select and the directory in which the file picker will open. See § 3.2 File picker options for details.

The suggestedName option was first introduced in Chrome 91.

The showSaveFilePicker(options) method, when invoked, must run these steps:
  1. Let environment be this’s relevant settings object.

  2. Let accepts options be the result of processing accept types given options.

  3. Let starting directory be the result of determining the directory the picker will start in given options["id"], options["startIn"] and environment.

  4. Let global be environment’s global object.

  5. Verify that environment is allowed to show a file picker.

  6. Let p be a new promise.

  7. Run the following steps in parallel:

    1. Optionally, wait until any prior execution of this algorithm has terminated.

    2. Display a prompt to the user requesting that the user pick exactly one file. The displayed prompt should let the user pick one of the accepts options to filter the list of displayed files. Exactly how this is implemented, and what this prompt looks like is implementation-defined. If accepts options are displayed in the UI, the selected option should also be used to suggest an extension to append to a user provided file name, but this is not required. In particular user agents are free to ignore potentially dangerous suffixes such as those ending in ".lnk" or ".local".

      When possible, this prompt should start out showing starting directory.

      If options["suggestedName"] exists and is not null, the file picker prompt will be pre-filled with the options["suggestedName"] as the default name to save as. The interaction between the suggestedName and accepts options is implementation-defined. If the suggestedName is deemed too dangerous, user agents should ignore or sanitize the suggested file name, similar to the sanitization done when fetching something as a download.

      Note: A user agent could for example pick whichever option in accepts options that matches suggestedName as the default filter.

    3. Wait for the user to have made their selection.

    4. If the user dismissed the prompt without making a selection, reject p with an "AbortError" DOMException and abort.

    5. Let entry be a file entry representing the selected file.

    6. If entry is deemed too sensitive or dangerous to be exposed to this website by the user agent:

      1. Inform the user that the selected files or directories can’t be exposed to this website.

      2. At the discretion of the user agent, either go back to the beginning of these in parallel steps, or reject p with an "AbortError" DOMException and abort.

    7. Set entry’s binary data to an empty byte sequence.

    8. Set result to a new FileSystemFileHandle associated with entry.

    9. Remember a picked directory given options["id"], entry and environment.

    10. Perform the activation notification steps in global’s browsing context.

      Note: This lets a website immediately perform operations on the returned handles that might require user activation, such as requesting more permissions.

    11. Resolve p with result.

  8. Return p.

3.5. The showDirectoryPicker() method

handle = await window . showDirectoryPicker()
handle = await window . showDirectoryPicker()({ mode: 'read' })

Shows a directory picker that lets the user select a single directory, returning a handle for the selected directory if the user grants read permission.

handle = await window . showDirectoryPicker()({ mode: 'readwrite' })

Shows a directory picker that lets the user select a single directory, returning a handle for the selected directory. The user agent can combine read and write permission requests on this handle into one subsequent prompt.

The id and startIn fields behave identically to the id and startIn fields, respectively. See § 3.2.2 Starting Directory for details on how to use these fields.

The showDirectoryPicker(options) method, when invoked, must run these steps:
  1. Let environment be this’s relevant settings object.

  2. Let starting directory be the result of determining the directory the picker will start in given options["id"], options["startIn"] and environment.

  3. Let global be environment’s global object.

  4. Verify that environment is allowed to show a file picker.

  5. Let p be a new promise.

  6. Run the following steps in parallel:

    1. Optionally, wait until any prior execution of this algorithm has terminated.

    2. Display a prompt to the user requesting that the user pick a directory.

      When possible, this prompt should start out showing starting directory.

    3. Wait for the user to have made their selection.

    4. If the user dismissed the prompt without making a selection, reject p with an "AbortError" DOMException and abort.

    5. Let entry be a directory entry representing the selected directory.

    6. If entry is deemed too sensitive or dangerous to be exposed to this website by the user agent:

      1. Inform the user that the selected files or directories can’t be exposed to this website.

      2. At the discretion of the user agent, either go back to the beginning of these in parallel steps, or reject p with an "AbortError" DOMException and abort.

    7. Set result to a new FileSystemDirectoryHandle associated with entry.

    8. Remember a picked directory given options["id"], entry and environment.

    9. Let desc be a FileSystemPermissionDescriptor.

    10. Set desc["name"] to "file-system".

    11. Set desc["handle"] to result.

    12. Set desc["mode"] to options["mode"].

    13. Let status be the result of running create a PermissionStatus for desc.

    14. Perform the activation notification steps in global’s browsing context.

    15. Request permission to use desc.

    16. Run the default permission query algorithm on desc and status.

    17. If status is not "granted", reject result with a "AbortError" DOMException and abort.

    18. Perform the activation notification steps in global’s browsing context.

    19. Resolve p with result.

  7. Return p.

3.6. Drag and Drop

partial interface DataTransferItem {
    Promise<FileSystemHandle?> getAsFileSystemHandle();
};

During a drag-and-drop operation, dragged file and directory items are associated with file entries and directory entries respectively.

handle = await item . getAsFileSystemHandle()

Returns a FileSystemFileHandle object if the dragged item is a file and a FileSystemDirectoryHandle object if the dragged item is a directory.

The getAsFileSystemHandle() method steps are:

  1. If the DataTransferItem object is not in the read/write mode or the read-only mode, return a promise resolved with null.

  2. If the the drag data item kind is not File, then return a promise resolved with null.

  3. Let p be a new promise.

  4. Run the following steps in parallel:

    1. Let entry be the file system entry representing the dragged file or directory.

    2. If entry is a file entry:

      1. Let handle be a FileSystemFileHandle associated with entry.

    3. Else if entry is a directory entry:

      1. Let handle be a FileSystemDirectoryHandle associated with entry.

    4. Resolve p with entry.

  5. Return p.

Handling drag and drop of files and directories:
elem.addEventListener('dragover', (e) => {
  // Prevent navigation.
  e.preventDefault();
});

elem.addEventListener('drop', async (e) => {
  e.preventDefault();

  const fileHandlesPromises = [...e.dataTransfer.items]
    .filter(item => item.kind === 'file')
    .map(item => item.getAsFileSystemHandle());

  for await (const handle of fileHandlesPromises) {
    if (handle.kind === 'directory') {
      console.log(`Directory: ${handle.name}`);
    } else {
      console.log(`File: ${handle.name}`);
    }
  }
});

This currently does not block access to too sensitive or dangerous directories, to be consistent with other APIs that give access to dropped files and directories. This is inconsistent with the local file system handle factories though, so we might want to reconsider this.

4. Accessibility Considerations

This section is non-normative.

When this specification is used to present information in the user interface, implementors will want to follow the OS level accessibility guidelines for the platform.

5. Privacy Considerations

This section is non-normative.

This API does not give websites any more read access to data than the existing <input type=file> and <input type=file webkitdirectory> APIs already do. Furthermore similarly to those APIs, all access to files and directories is explicitly gated behind a file or directory picker.

There are however several major privacy risks with this new API:

5.1. Users giving access to more, or more sensitive files than they intended.

This isn’t a new risk with this API, but user agents should try to make sure that users are aware of what exactly they’re giving websites access to. This is particularly important when giving access to a directory, where it might not be immediately clear to a user just how many files actually exist in that directory.

A related risk is having a user give access to particularly sensitive data. This could include some of a user agent’s configuration data, network cache or cookie store, or operating system configuration data such as password files. To protect against this, user agents are encouraged to restrict which directories a user is allowed to select in a directory picker, and potentially even restrict which files the user is allowed to select. This will make it much harder to accidentally give access to a directory that contains particularly sensitive data. Care must be taken to strike the right balance between restricting what the API can access while still having the API be useful. After all, this API intentionally lets the user use websites to interact with some of their most private personal data.

Examples of directories that user agents might want to restrict as being too sensitive or dangerous include:

5.2. Websites trying to use this API for tracking.

This API could be used by websites to track the user across clearing browsing data. This is because, in contrast with existing file access APIs, user agents are able to grant persistent access to files or directories and can re-prompt. In combination with the ability to write to files, websites will be able to persist an identifier on the users' disk. Clearing browsing data will not affect those files in any way, making these identifiers persist through those actions.

This risk is somewhat mitigated by the fact that clearing browsing data will clear all handles that a website had persisted (for example in IndexedDB), so websites won’t have any handles to re-prompt for permission after browsing data was cleared. Furthermore user agents are encouraged to make it clear what files and directories a website has access to, and to automatically expire permission grants except for particularly well trusted origins (for example persistent permissions could be limited to "installed" web applications).

User agents also are encouraged to provide a way for users to revoke permissions granted. Clearing browsing data is expected to revoke all permissions as well.

5.3. First-party vs third-party contexts.

In third-party contexts (e.g. an iframe whose origin does not match that of the top-level frame) websites can’t gain access to data they don’t already have access to. This includes both getting access to new files or directories via the local file system handle factories, as well as requesting more permissions to existing handles via the requestPermission API.

Handles can also only be post-messaged to same-origin destinations. Attempts to send a handle to a cross-origin destination will result in a messageerror event.

6. Security Considerations

This section is non-normative.

This API gives websites the ability to modify existing files on disk, as well as write to new files. This has a couple of important security considerations:

6.1. Malware

This API could be used by websites to try to store and/or execute malware on the users system. To mitigate this risk, this API does not provide any way to mark files as executable (on the other hand files that are already executable likely remain that way, even after the files are modified through this API). Furthermore user agents are encouraged to apply things like Mark-of-the-Web to files created or modified by this API.

Finally, user agents are encouraged to verify the contents of files modified by this API via malware scans and safe browsing checks, unless some kind of external strong trust relation already exists. This of course has effects on the performance characteristics of this API.

6.2. Ransomware attacks

Another risk factor is that of ransomware attacks. The limitations described above regarding blocking access to certain sensitive directories helps limit the damage such an attack can do. Additionally user agents can grant write access to files at whatever granularity they deem appropriate.

6.3. Filling up a users disk

Other than files in a bucket file system, files written by this API are not subject to storage quota. As such websites can fill up a users disk without being limited by quota, which could leave a users device in a bad state (do note that even with storage that is subject to storage quota it is still possible to fill up, or come close to filling up, a users disk, since storage quota in general is not dependent on the amount of available disk space).

Without this API websites can write data to disk not subject to quota limitations already by triggering downloads of large files (potentially created client side, to not incur any network overhead). While the presence of truncate() and writing at a potentially really large offset past the end of a file makes it much easier and lower cost to create large files, on most file systems such files should not actually take up as much disk space as most commonly used file systems support sparse files (and thus wouldn’t actually store the NUL bytes generated by resizing a file or seeking past the end of it).

Whatever mitigations user agents use to guard against websites filling up a disk via either quota managed storage or the existing downloads mechanism should also be employed when websites use this API to write to disk.

Conformance

Document conventions

Conformance requirements are expressed with a combination of descriptive assertions and RFC 2119 terminology. The key words “MUST”, “MUST NOT”, “REQUIRED”, “SHALL”, “SHALL NOT”, “SHOULD”, “SHOULD NOT”, “RECOMMENDED”, “MAY”, and “OPTIONAL” in the normative parts of this document are to be interpreted as described in RFC 2119. However, for readability, these words do not appear in all uppercase letters in this specification.

All of the text of this specification is normative except sections explicitly marked as non-normative, examples, and notes. [RFC2119]

Examples in this specification are introduced with the words “for example” or are set apart from the normative text with class="example", like this:

This is an example of an informative example.

Informative notes begin with the word “Note” and are set apart from the normative text with class="note", like this:

Note, this is an informative note.

Index

Terms defined by this specification

Terms defined by reference

References

Normative References

[FS]
Austin Sullivan. File System Standard. Living Standard. URL: https://fs.spec.whatwg.org/
[HTML]
Anne van Kesteren; et al. HTML Standard. Living Standard. URL: https://html.spec.whatwg.org/multipage/
[INFRA]
Anne van Kesteren; Domenic Denicola. Infra Standard. Living Standard. URL: https://infra.spec.whatwg.org/
[MIMESNIFF]
Gordon P. Hemsley. MIME Sniffing Standard. Living Standard. URL: https://mimesniff.spec.whatwg.org/
[PERMISSIONS]
Marcos Caceres; Mike Taylor. Permissions. URL: https://w3c.github.io/permissions/
[PERMISSIONS-REQUEST]
Requesting Permissions. cg-draft. URL: https://wicg.github.io/permissions-request/
[RFC2119]
S. Bradner. Key words for use in RFCs to Indicate Requirement Levels. March 1997. Best Current Practice. URL: https://datatracker.ietf.org/doc/html/rfc2119
[STORAGE]
Anne van Kesteren. Storage Standard. Living Standard. URL: https://storage.spec.whatwg.org/
[WEBIDL]
Edgar Chen; Timothy Gu. Web IDL Standard. Living Standard. URL: https://webidl.spec.whatwg.org/

Informative References

[ENTRIES-API]
File and Directory Entries API. cg-draft. URL: https://wicg.github.io/entries-api/
[FILE-API]
Marijn Kruisselbrink. File API. URL: https://w3c.github.io/FileAPI/

IDL Index

enum FileSystemPermissionMode {
  "read",
  "readwrite"
};

dictionary FileSystemPermissionDescriptor : PermissionDescriptor {
  required FileSystemHandle handle;
  FileSystemPermissionMode mode = "read";
};

dictionary FileSystemHandlePermissionDescriptor {
  FileSystemPermissionMode mode = "read";
};

[Exposed=(Window,Worker), SecureContext, Serializable]
partial interface FileSystemHandle {
  Promise<PermissionState> queryPermission(optional FileSystemHandlePermissionDescriptor descriptor = {});
  Promise<PermissionState> requestPermission(optional FileSystemHandlePermissionDescriptor descriptor = {});
};

enum WellKnownDirectory {
  "desktop",
  "documents",
  "downloads",
  "music",
  "pictures",
  "videos",
};

typedef (WellKnownDirectory or FileSystemHandle) StartInDirectory;

dictionary FilePickerAcceptType {
    USVString description = "";
    record<USVString, (USVString or sequence<USVString>)> accept;
};

dictionary FilePickerOptions {
    sequence<FilePickerAcceptType> types;
    boolean excludeAcceptAllOption = false;
    DOMString id;
    StartInDirectory startIn;
};

dictionary OpenFilePickerOptions : FilePickerOptions {
    boolean multiple = false;
};

dictionary SaveFilePickerOptions : FilePickerOptions {
    USVString? suggestedName;
};

dictionary DirectoryPickerOptions {
    DOMString id;
    StartInDirectory startIn;
    FileSystemPermissionMode mode = "read";
};

[SecureContext]
partial interface Window {
    Promise<sequence<FileSystemFileHandle>> showOpenFilePicker(optional OpenFilePickerOptions options = {});
    Promise<FileSystemFileHandle> showSaveFilePicker(optional SaveFilePickerOptions options = {});
    Promise<FileSystemDirectoryHandle> showDirectoryPicker(optional DirectoryPickerOptions options = {});
};

partial interface DataTransferItem {
    Promise<FileSystemHandle?> getAsFileSystemHandle();
};

Issues Index

Make these checks no longer associated with an entry. [Issue #whatwg/fs#101]
Ideally this user activation requirement would be defined upstream. [Issue #WICG/permissions-request#2]
Currently FileSystemPermissionMode can only be "read" or "readwrite". In the future we might want to add a "write" mode as well to support write-only handles. [Issue #119]
This currently does not block access to too sensitive or dangerous directories, to be consistent with other APIs that give access to dropped files and directories. This is inconsistent with the local file system handle factories though, so we might want to reconsider this.
MDN

DataTransferItem/getAsFileSystemHandle

In only one current engine.

FirefoxNoneSafariNoneChrome86+
Opera?Edge86+
Edge (Legacy)?IENone
Firefox for Android?iOS Safari?Chrome for AndroidNoneAndroid WebView?Samsung Internet?Opera Mobile?
MDN

FileSystemHandle/queryPermission

In only one current engine.

FirefoxNoneSafariNoneChrome86+
Opera?Edge86+
Edge (Legacy)?IENone
Firefox for Android?iOS Safari?Chrome for Android?Android WebViewNoneSamsung Internet?Opera Mobile?
MDN

FileSystemHandle/requestPermission

In only one current engine.

FirefoxNoneSafariNoneChrome86+
Opera?Edge86+
Edge (Legacy)?IENone
Firefox for Android?iOS Safari?Chrome for Android?Android WebViewNoneSamsung Internet?Opera Mobile?
MDN

window/showdirectorypicker

In only one current engine.

FirefoxNoneSafariNoneChrome86+
Opera?Edge86+
Edge (Legacy)?IENone
Firefox for Android?iOS Safari?Chrome for AndroidNoneAndroid WebView?Samsung Internet?Opera Mobile?
MDN

window/showopenfilepicker

In only one current engine.

FirefoxNoneSafariNoneChrome86+
Opera?Edge86+
Edge (Legacy)?IENone
Firefox for Android?iOS Safari?Chrome for AndroidNoneAndroid WebView?Samsung Internet?Opera Mobile?
MDN

window/showsavefilepicker

In only one current engine.

FirefoxNoneSafariNoneChrome86+
Opera?Edge86+
Edge (Legacy)?IENone
Firefox for Android?iOS Safari?Chrome for AndroidNoneAndroid WebView?Samsung Internet?Opera Mobile?