Native File System

Draft Community Group Report,

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

Abstract

This document defines a web platform API that lets websites gain write access to the native file system. It builds on [FILE-API], but adds lots of new functionality on top.

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.

TODO

This provides similar functionality as earlier drafts of the [file-system-api] as well as the [entries-api], but with a more modern API.

2. Files and Directories

2.1. Concepts

A entry is either a file entry or a directory entry.

Each entry has an associated name.

A file entry additionally consists of binary data and a modification timestamp.

A directory entry additionally consists of a set of entries. Each member is either a file or a directory.

TODO: Explain how entries map to files on disk (multiple entries can map to the same file or directory on disk but doesn’t have to map to any file on disk).

2.2. The FileSystemHandle interface

dictionary FileSystemHandlePermissionDescriptor {
  boolean writable = false;
};

[Exposed=(Window,Worker), SecureContext, Serializable]
interface FileSystemHandle {
  readonly attribute boolean isFile;
  readonly attribute boolean isDirectory;
  readonly attribute USVString name;

  Promise<PermissionState> queryPermission(optional FileSystemHandlePermissionDescriptor descriptor = {});
  Promise<PermissionState> requestPermission(optional FileSystemHandlePermissionDescriptor descriptor = {});
};

A FileSystemHandle object represents a entry. Each FileSystemHandle object is assocaited with a entry (a entry). Multiple separate objects implementing the FileSystemHandle interface can all be associated with the same entry simultaneously.

FileSystemHandle objects are serializable objects.

In the Origin Trial as available in Chrome 78, these objects are not yet serializable.

Their serialization steps, given value, serialized and forStorage are:

  1. Set serialized.[[Origin]] to value’s relevant settings object's origin.

  2. TODO

Their deserialization steps, given serialized and value are:
  1. If serialized.[[Origin]] is not same origin with value’s relevant settings object's origin, then throw a DataCloneError.

  2. TODO

handle . isFile

Returns true iff handle is a FileSystemFileHandle.

handle . isDirectory

Returns true iff handle is a FileSystemDirectoryHandle.

handle . name

Returns the name of the entry represented by handle.

The isFile attribute must return true if the associated entry is a file entry, and false otherwise.

The isDirectory attribute must return true if the associated entry is a directory entry, and false otherwise.

The name attribute must return the name of the associated entry.

2.2.1. The queryPermission() method

the currently described API here assumes a model where it is not possible to have a write-only handle. I.e. it is not possible to have or request write access without also having read access. There definitely are use cases for write-only handles (i.e. directory downloads), so we might have to reconsider this.

status = await handle . queryPermission({ writable = false })
status = await handle . queryPermission()

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 chooseFileSystemEntries 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({ writable = true })

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 queryPermission(descriptor) method, when invoked, must run these steps:
  1. TODO

2.2.2. The requestPermission() method

status = await handle . requestPermission({ writable = false })
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({ writable = true })

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. TODO

2.3. The FileSystemFileHandle interface

dictionary FileSystemCreateWriterOptions {
  boolean keepExistingData = false;
};

[Exposed=(Window,Worker), SecureContext, Serializable]
interface FileSystemFileHandle : FileSystemHandle {
  Promise<File> getFile();
  Promise<FileSystemWriter> createWriter(optional FileSystemCreateWriterOptions options = {});
};

FileSystemFileHandle objects are serializable objects. Their serialization steps and deserialization steps are the same as those for FileSystemHandle.

In the Origin Trial as available in Chrome 78, these objects are not yet serializable.

2.3.1. The getFile() method

file = await fileHandle . getFile()

Returns a File representing the state on disk of the entry represented by handle. If the file on disk changes or is removed after this method is called, the returned File object will likely be no longer readable.

The getFile() method, when invoked, must run these steps:
  1. TODO

2.3.2. The createWriter() method

writer = await fileHandle . createWriter()
writer = await fileHandle . createWriter({ keepExistingData: true/false })

Returns a FileSystemWriter that can be used to write to the file. Any changes made through writer won’t be reflected in the file represented by fileHandle until its close() method is called. User agents try to ensure that no partial writes happen, i.e. the file represented by fileHandle will either contains its old contents or it will contain whatever data was written through writer up until close() was called.

This is typically implemented by writing data to a temporary file, and only replacing the file represented by fileHandle with the temporary file when the writer is closed.

If keepExistingData is false or not specified, the temporary file starts out empty, otherwise the existing file is first copied to this temporary file.

There has been some discussion around and desire for a "inPlace" mode for createWriter (where changes will be written to the actual underlying file as they are written to the writer, for example to support in-place modification of large files or things like databases). This is not currently implemented in Chrome. Implementing this is currently blocked on figuring out how to combine the desire to run malware checks with the desire to let websites make fast in-place modifications to existing large files.

The createWriter(options) method, when invoked, must run these steps:
  1. TODO

2.4. The FileSystemDirectoryHandle interface

dictionary FileSystemGetFileOptions {
  boolean create = false;
};

dictionary FileSystemGetDirectoryOptions {
  boolean create = false;
};

dictionary FileSystemRemoveOptions {
  boolean recursive = false;
};

[Exposed=(Window,Worker), SecureContext, Serializable]
interface FileSystemDirectoryHandle : FileSystemHandle {
  Promise<FileSystemFileHandle> getFile(USVString name, optional FileSystemGetFileOptions options = {});
  Promise<FileSystemDirectoryHandle> getDirectory(USVString name, optional FileSystemGetDirectoryOptions options = {});

  // This really returns an async iterable, but that is not yet expressable in WebIDL.
  object getEntries();

  Promise<void> removeEntry(USVString name, optional FileSystemRemoveOptions options = {});
};

FileSystemDirectoryHandle objects are serializable objects. Their serialization steps and deserialization steps are the same as those for FileSystemHandle.

In the Origin Trial as available in Chrome 78, these objects are not yet serializable.

Should we have separate getFile and getDirectory methods, or just a single getChild/getEntry method?

Having getFile methods in both FileSystemDirectoryHandle and FileSystemFileHandle, but with very different behavior might be confusing? Perhaps rename at least one of them (but see also previous issue).

Should getEntries be its own method, or should FileSystemDirectoryHandle just be an async iterable itself?

We will probably want some method to make it possible to compare two handles, and/or determine if one handle represents a descendant of another handle. Such a method will enable for example an IDE to detect that the user tries to open a file (through the file picker), where that file actually is part of the "project" the IDE has open, allowing the IDE to highlight the selected file in a directory tree.

2.4.1. The getFile() method

fileHandle = await directoryHandle . getFile(name)
fileHandle = await directoryHandle . getFile(name, { create: false })

Returns a handle for a file named name in the directory represented by directoryHandle. If no such file exists, this rejects.

fileHandle = await directoryHandle . getFile(name, { create: true })

Returns a handle for a file named name in the directory represented by directoryHandle. If no such file exists, this creates a new file. If no file with named name can be created this rejects. Creation can fail because there already is a directory with the same name, because the name uses characters that aren’t supported in file names on the underlying file system, or because the user agent for security reasons decided not to allow creation of the file.

This operation requires write permission, even if the file being returned already exists. If this handle doesn’t already have write permission, this could result in a prompt being shown to the user. To get an existing file without needing write permission, call this method with { create: false }.

The getFile(name, options) method, when invoked, must run these steps:
  1. TODO

2.4.2. The getDirectory() method

subdirHandle = await directoryHandle . getDirectory(name)
subdirHandle = await directoryHandle . getDirectory(name, { create: false })

Returns a handle for a directory named name in the directory represented by directoryHandle. If no such directory exists, this rejects.

subdirHandle = await directoryHandle . getDirectory(name, { create: true })

Returns a handle for a directory named name in the directory represented by directoryHandle. If no such directory exists, this creates a new directory. If creating the directory failed, this rejects. Creation can fail because there already is a file with the same name, or because the name uses characters that aren’t supported in file names on the underlying file system.

This operation requires write permission, even if the directory being returned already exists. If this handle doesn’t already have write permission, this could result in a prompt being shown to the user. To get an existing directory without needing write permission, call this method with { create: false }.

The getDirectory(name, options) method, when invoked, must run these steps:
  1. TODO

2.4.3. The getEntries() method

for await (const handle of directoryHandle . getEntries()) {}

Iterates over all entries whose parent is the entry represented by directoryHandle.

The getEntries() method, when invoked, must run these steps:
  1. TODO

2.4.4. The removeEntry() method

await directoryHandle . removeEntry(name)
await directoryHandle . removeEntry(name, { recursive: false })

If the directory represented by directoryHandle contains a file named name, or an empty directory named name, this will attempt to delete that file or directory.

Attempting to delete a file or directory that does not exist is considered success, while attempting to delete a non-empty directory will result in a promise rejection.

await directoryHandle . removeEntry(name, { recursive: true })

Removes the entry named name in the directory represented by directoryHandle. If that entry is a directory, its contents will also be deleted recursively. recursively.

Attempting to delete a file or directory that does not exist is considered success.

The removeEntry(name, options) method, when invoked, must run these steps:
  1. TODO

2.5. The FileSystemWriter interface

[Exposed=(Window,Worker), SecureContext]
interface FileSystemWriter {
  Promise<void> write(unsigned long long position, (BufferSource or Blob or USVString) data);
  Promise<void> truncate(unsigned long long size);
  Promise<void> close();
};

We want some kind of integration with writable streams. One possible option is to make FileStreamWriter inherit from WritableStream, but other options should be considered as well. <https://github.com/wicg/native-file-system/issues/19>

2.5.1. The write() method

await writer . write(position, data)

Writes the content of data into the file associated with writer at position position. If position is past the end of the file writing will fail and this method rejects.

No changes are written to the actual file until on disk until close() is called. Changes are typically written to a temporary file instead.

The write(position, data) method, when invoked, must run these steps:
  1. TODO

2.5.2. The truncate() method

await writer . truncate(size)

Resizes the file associated with writer to be size bytes long. If size is larger than the current file size this pads the file with zero bytes, otherwise it truncates the file.

No changes are written to the actual file until on disk until close() is called. Changes are typically written to a temporary file instead.

The truncate(size) method, when invoked, must run these steps:
  1. TODO

2.5.3. The close() method

await writer . close()

First flushes any data written so far to disk, and then closes the writer. No changes will be visible in the destination file until this method is called. Furthermore, if the file on disk changed between creating this writer and this invocation of close(), this will reject and all future operations on the writer will fail.

This operation can take some time to complete, as user agents might use this moment to run malware scanners or perform other security checks if the website isn’t sufficiently trusted.

The close() method, when invoked, must run these steps:
  1. TODO

3. Accessing native filesystem

3.1. The chooseFileSystemEntries() method

enum ChooseFileSystemEntriesType { "open-file", "save-file", "open-directory" };

dictionary ChooseFileSystemEntriesOptionsAccepts {
  USVString description;
  sequence<USVString> mimeTypes;
  sequence<USVString> extensions;
};

dictionary ChooseFileSystemEntriesOptions {
    ChooseFileSystemEntriesType type = "open-file";
    boolean multiple = false;
    sequence<ChooseFileSystemEntriesOptionsAccepts> accepts;
    boolean excludeAcceptAllOption = false;
};

[SecureContext]
partial interface Window {
    Promise<(FileSystemHandle or sequence<FileSystemHandle>)>
        chooseFileSystemEntries(optional ChooseFileSystemEntriesOptions options = {});
};
result = await window . chooseFileSystemEntries(options)

Shows a file picker dialog to the user and returns handles for the selected files or directories.

The options argument sets options that influence the behavior of the shown file picker.

options.type specifies the type of the entry the website wants the user to pick. When set to "open-file" (the default), the user can select only existing files. When set to "save-file" the dialog will additionally let the user select files that don’t yet exist, and if the user selects a file that does exist already, its contents will be cleared before the handle is returned to the website. Finally when set to "open-directory", the dialog will let the user select directories instead of files.

If options.multiple is false (or absent) the user can only select a single file, and the result will be a single FileSystemHandle. If on the other hand options.multiple is true, the dialog can let the user select more than one file, and result will be an array of FileSystemHandle instances (even if the user did select a single file, if multiple is true this will be returned as a single-element array).

Finally options.accepts and options.excludeAcceptAllOption specify the types of files the dialog will let the user select. Each entry in options.accepts describes a single type of file, consisting of a description, zero or more mimeTypes and zero or more extensions. Options with no valid mimeTypes and no extensions are invalid and are ignored. If no description is provided one will be generated.

If options.excludeAcceptAllOption is true, or if no valid entries exist in options.accepts, a option matching all files will be included in the file types the dialog lets the user select.

The chooseFileSystemEntries(options) method, when invoked, must run these steps:
  1. TODO

4. Accessing special filesystems

4.1. The getSystemDirectory() method

enum SystemDirectoryType {
  "sandbox"
};

dictionary GetSystemDirectoryOptions {
  required SystemDirectoryType type;
};

[SecureContext]
partial interface FileSystemDirectoryHandle {
  static Promise<FileSystemDirectoryHandle> getSystemDirectory(GetSystemDirectoryOptions options);
};
directoryHandle = FileSystemDirectoryHandle . getSystemDirectory({ type: "sandbox" })

Returns the sandboxed filesystem.

getSystemDirectory might not be the best name. Also perhaps should be on Window rather than on FileSystemDirectoryHandle. <https://github.com/wicg/native-file-system/issues/27>

The getSystemDirectory(options) method, when invoked, must run these steps:
  1. TODO

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.

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 also clear 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 (i.e. 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 chooseFileSystemEntries API, 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.

"Atomic writes" attempts to make it explicit what this API can and can’t do, and how performance can be effected by safe browsing checks. <https://github.com/wicg/native-file-system/issues/51>

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.

Conformance

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

[FILE-API]
Marijn Kruisselbrink; Arun Ranganathan. File API. 11 September 2019. WD. URL: https://www.w3.org/TR/FileAPI/
[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/
[PERMISSIONS]
Mounir Lamouri; Marcos Caceres; Jeffrey Yasskin. Permissions. 25 September 2017. WD. URL: https://www.w3.org/TR/permissions/
[RFC2119]
S. Bradner. Key words for use in RFCs to Indicate Requirement Levels. March 1997. Best Current Practice. URL: https://tools.ietf.org/html/rfc2119
[WebIDL]
Boris Zbarsky. Web IDL. 15 December 2016. ED. URL: https://heycam.github.io/webidl/

Informative References

[ENTRIES-API]
File and Directory Entries API. cg-draft. URL: https://wicg.github.io/entries-api/
[FILE-SYSTEM-API]
Eric Uhrhane. File API: Directories and System. 24 April 2014. NOTE. URL: https://www.w3.org/TR/file-system-api/

IDL Index

dictionary FileSystemHandlePermissionDescriptor {
  boolean writable = false;
};

[Exposed=(Window,Worker), SecureContext, Serializable]
interface FileSystemHandle {
  readonly attribute boolean isFile;
  readonly attribute boolean isDirectory;
  readonly attribute USVString name;

  Promise<PermissionState> queryPermission(optional FileSystemHandlePermissionDescriptor descriptor = {});
  Promise<PermissionState> requestPermission(optional FileSystemHandlePermissionDescriptor descriptor = {});
};

dictionary FileSystemCreateWriterOptions {
  boolean keepExistingData = false;
};

[Exposed=(Window,Worker), SecureContext, Serializable]
interface FileSystemFileHandle : FileSystemHandle {
  Promise<File> getFile();
  Promise<FileSystemWriter> createWriter(optional FileSystemCreateWriterOptions options = {});
};

dictionary FileSystemGetFileOptions {
  boolean create = false;
};

dictionary FileSystemGetDirectoryOptions {
  boolean create = false;
};

dictionary FileSystemRemoveOptions {
  boolean recursive = false;
};

[Exposed=(Window,Worker), SecureContext, Serializable]
interface FileSystemDirectoryHandle : FileSystemHandle {
  Promise<FileSystemFileHandle> getFile(USVString name, optional FileSystemGetFileOptions options = {});
  Promise<FileSystemDirectoryHandle> getDirectory(USVString name, optional FileSystemGetDirectoryOptions options = {});

  // This really returns an async iterable, but that is not yet expressable in WebIDL.
  object getEntries();

  Promise<void> removeEntry(USVString name, optional FileSystemRemoveOptions options = {});
};

[Exposed=(Window,Worker), SecureContext]
interface FileSystemWriter {
  Promise<void> write(unsigned long long position, (BufferSource or Blob or USVString) data);
  Promise<void> truncate(unsigned long long size);
  Promise<void> close();
};

enum ChooseFileSystemEntriesType { "open-file", "save-file", "open-directory" };

dictionary ChooseFileSystemEntriesOptionsAccepts {
  USVString description;
  sequence<USVString> mimeTypes;
  sequence<USVString> extensions;
};

dictionary ChooseFileSystemEntriesOptions {
    ChooseFileSystemEntriesType type = "open-file";
    boolean multiple = false;
    sequence<ChooseFileSystemEntriesOptionsAccepts> accepts;
    boolean excludeAcceptAllOption = false;
};

[SecureContext]
partial interface Window {
    Promise<(FileSystemHandle or sequence<FileSystemHandle>)>
        chooseFileSystemEntries(optional ChooseFileSystemEntriesOptions options = {});
};

enum SystemDirectoryType {
  "sandbox"
};

dictionary GetSystemDirectoryOptions {
  required SystemDirectoryType type;
};

[SecureContext]
partial interface FileSystemDirectoryHandle {
  static Promise<FileSystemDirectoryHandle> getSystemDirectory(GetSystemDirectoryOptions options);
};

Issues Index

TODO: Explain how entries map to files on disk (multiple entries can map to the same file or directory on disk but doesn’t have to map to any file on disk).
the currently described API here assumes a model where it is not possible to have a write-only handle. I.e. it is not possible to have or request write access without also having read access. There definitely are use cases for write-only handles (i.e. directory downloads), so we might have to reconsider this.
There has been some discussion around and desire for a "inPlace" mode for createWriter (where changes will be written to the actual underlying file as they are written to the writer, for example to support in-place modification of large files or things like databases). This is not currently implemented in Chrome. Implementing this is currently blocked on figuring out how to combine the desire to run malware checks with the desire to let websites make fast in-place modifications to existing large files.
Should we have separate getFile and getDirectory methods, or just a single getChild/getEntry method?
Having getFile methods in both FileSystemDirectoryHandle and FileSystemFileHandle, but with very different behavior might be confusing? Perhaps rename at least one of them (but see also previous issue).
Should getEntries be its own method, or should FileSystemDirectoryHandle just be an async iterable itself?
We will probably want some method to make it possible to compare two handles, and/or determine if one handle represents a descendant of another handle. Such a method will enable for example an IDE to detect that the user tries to open a file (through the file picker), where that file actually is part of the "project" the IDE has open, allowing the IDE to highlight the selected file in a directory tree.
We want some kind of integration with writable streams. One possible option is to make FileStreamWriter inherit from WritableStream, but other options should be considered as well. <https://github.com/wicg/native-file-system/issues/19>
getSystemDirectory might not be the best name. Also perhaps should be on Window rather than on FileSystemDirectoryHandle. <https://github.com/wicg/native-file-system/issues/27>
"Atomic writes" attempts to make it explicit what this API can and can’t do, and how performance can be effected by safe browsing checks. <https://github.com/wicg/native-file-system/issues/51>