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:
-
Set serialized.[[Origin]] to value’s relevant settings object's origin.
-
TODO
-
If serialized.[[Origin]] is not same origin with value’s relevant settings object's origin, then throw a
DataCloneError
. -
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()
- status = await handle .
-
Queries the current state of the read permission of this handle. If this returns
"prompt"
the website will have to callrequestPermission()
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 callrequestPermission()
. There is no automatic prompting for read access when attempting to read from a file or directory.
queryPermission(descriptor)
method, when invoked, must run these steps:
-
TODO
2.2.2. The requestPermission()
method
- status = await handle .
requestPermission
({writable
= false })- status = await handle .
requestPermission()
- status = await handle .
-
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.
requestPermission(descriptor)
method, when invoked, must run these steps:
-
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
getFile()
method, when invoked, must run these steps:
-
TODO
2.3.2. The createWriter()
method
- writer = await fileHandle .
createWriter()
- writer = await fileHandle .
createWriter
({keepExistingData
: true/false }) - writer = await fileHandle .
-
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 itsclose()
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 untilclose()
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
isfalse
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.
createWriter(options)
method, when invoked, must run these steps:
-
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 = {}); // This really returns an async iterable, but that is not yet expressable in WebIDL.
options 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 }) - fileHandle = await directoryHandle .
-
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 }
getFile(name, options)
method, when invoked,
must run these steps:
-
TODO
2.4.2. The getDirectory()
method
- subdirHandle = await directoryHandle .
getDirectory
(name)- subdirHandle = await directoryHandle .
getDirectory
(name, {create
: false }) - subdirHandle = await directoryHandle .
-
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 }
getDirectory(name, options)
method, when
invoked, must run these steps:
-
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.
getEntries()
method, when invoked, must run
these steps:
-
TODO
2.4.4. The removeEntry()
method
- await directoryHandle .
removeEntry
(name)- await directoryHandle .
removeEntry
(name, {recursive
: false }) - await directoryHandle .
-
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.
removeEntry(name, options)
method, when invoked, must run
these steps:
-
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.
write(position, data)
method, when invoked, must run
these steps:
-
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.
truncate(size)
method, when invoked, must run these
steps:
-
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.
close()
method, when invoked, must run these
steps:
-
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 = "open-file";
type 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 singleFileSystemHandle
. 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 ofFileSystemHandle
instances (even if the user did select a single file, ifmultiple
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 adescription
, zero or moremimeTypes
and zero or moreextensions
. Options with no validmimeTypes
and noextensions
are invalid and are ignored. If nodescription
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.
chooseFileSystemEntries(options)
method, when invoked, must run
these steps:
-
Let environment be the current settings object.
-
If environment’s origin is an opaque origin, return a promise rejected with a
SecurityError
. -
Let browsing context be environment’s responsible browsing context.
-
Let top-level context be browsing context’s top-level browsing context.
-
If environment’s origin is not same origin with browsing context’s top-level browsing context's active document's origin, return a promise rejected with a
SecurityError
.There must be a better way to express this "no third-party iframes" constraint.
-
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>
getSystemDirectory(options)
method, when
invoked, must run these steps:
-
Let environment be the current settings object.
-
If environment’s origin is an opaque origin, return a promise rejected with a
SecurityError
. -
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.