1. Introduction
This section is non-normative.
TODO
This provides similar functionality as earlier drafts of the File API: Directories and System as well as the File and Directory Entries API, but with a more modern API.
2. Files and Directories
2.1. Concepts
An entry is either a file entry or a directory entry.
Each entry has an associated name (a string).
A valid file name is a string that is not an empty string, is not equal to "." or "..", and does not contain '/' or any other character used as path separator on the underlying platform.
Note: This means that '\' is not allowed in names on Windows, but might be allowed on other operating systems. Additionally underlying file systems might have further restrictions on what names are or aren’t allowed, so a string merely being a valid file name is not a guarantee that creating a file or directory with that name will succeed.
A file entry additionally consists of binary data and a modification timestamp.
A directory entry additionally consists of a set of children, which are themselves entries. Each member is either a file or a directory.
An entry entry should be contained in the children of at most one directory entry, and that directory entry is also known as entry’s parent. An entry's parent is null if no such directory entry exists.
Note: Two different entries can represent the same file or directory on disk, in which
case it is possible for both entries to have a different parent, or for one entry to have a
parent while the other entry does not have a parent. Typically an entry does not have a parent
if it was returned by chooseFileSystemEntries() or getSystemDirectory(), and an entry
will have a parent in all other cases.
Entries can (but don’t have to) be backed by files on the systems native file system, so it is possible for the binary data, modification timestamp, and children of entries to be modified by applications outside of this specification. Exactly how external changes are reflected in the data structures defined by this specification, as well as how changes made to the data structures defined here are reflected externally is left up to individual user-agent implementations.
An entry a is the same as an entry b if a is equal to b, or if a and b are backed by the same file or directory on the native file system.
TODO: Explain better how entries map to files on disk (multiple entries can map to the same file or directory on disk but an entry doesn’t have to map to any file on disk).
-
Let result be a new promise.
-
Run the following steps in parallel:
-
If child is the same as root, resolve result with an empty list, and abort.
-
Let childPromises be « ».
-
Wait for all childPromises, with the following success steps:
-
If result hasn’t been resolved yet, resolve result with
null.
-
-
-
Return result.
2.2. Permissions
Figure out some way to integrate this better with the permissions API [permissions], for example by making the entry be a part of the PermissionDescriptor.
FileSystemHandlePermissionDescriptor descriptor and an environment settings object settings,
and return a PermissionState.
Unless specified otherwise, these steps are:
-
Let parent be entry’s parent.
-
Assert: parent is not null.
Note: § 3.1 Native File System Permissions overrides these steps for entries returned by
chooseFileSystemEntries(). Additionally § 4.1 Special File System Concepts overrides these steps for entries returned bygetSystemDirectory(). All other entries always have a parent. -
Return the result of running parent’s query permission steps given descriptor and settings.
Note: These steps return the current state synchronously, however these steps are only invoked from "in parallel" sections of algorithms, so this doesn’t have to be implemented in a synchronous manner.
FileSystemHandlePermissionDescriptor descriptor and an environment settings object settings.
Unless specified otherwise, these steps are:
-
Let parent be entry’s parent.
-
If parent is not null, run parent’s request permission steps given descriptor and settings.
Note: These steps do not return anything. However as a result of executing the request permission steps for an entry the permission state for that entry (and other entries) can be updated, and the new state can be queried by executing the query permission steps.
2.3. 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 <boolean >isSameEntry (FileSystemHandle );other Promise <PermissionState >queryPermission (optional FileSystemHandlePermissionDescriptor = {});descriptor Promise <PermissionState >requestPermission (optional FileSystemHandlePermissionDescriptor = {}); };descriptor
A FileSystemHandle object represents an entry. Each FileSystemHandle object is associated
with an entry (an 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. In Chrome 82 they are.
Their serialization steps, given value, serialized and forStorage are:
-
Set serialized.[[Origin]] to value’s relevant settings object's origin.
-
Set serialized.[[Entry]] to value’s entry.
-
If serialized.[[Origin]] is not same origin with value’s relevant settings object's origin, then throw a
DataCloneError. -
Set value’s entry to serialized.[[Entry]]
- handle .
isFile -
Returns true if handle is a
FileSystemFileHandle. - handle .
isDirectory -
Returns true if 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.3.1. The isSameEntry() method
- same = await handle1 .
isSameEntry( handle2 ) -
Returns true if handle1 and handle2 represent the same file or directory.
This method is first available in Chrome 82.
isSameEntry(other) method, when invoked, must run these steps:
-
Let realm be this's relevant Realm.
-
Let p be a new promise in realm.
-
Run the following steps in parallel:
-
Return p.
2.3.2. 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 (e.g. directory downloads), so we might have to reconsider this. <https://github.com/wicg/native-file-system/issues/119>
- 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
chooseFileSystemEntrieswill 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:
-
Let result be a new promise.
-
Run the following steps in parallel:
-
Resolve result with the result of running this’s entry's query permission steps given descriptor and this’s relevant settings object.
-
-
Return result.
2.3.3. 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:
-
Let result be a new promise.
-
Run the following steps in parallel:
-
Run this’s entry's request permission steps given descriptor and this’s relevant settings object. If that throws an exception, reject result with that exception and abort.
-
Resolve result with the result of running this’s entry's query permission steps given descriptor and this’s relevant settings object.
-
-
Return result.
2.4. The FileSystemFileHandle interface
dictionary {FileSystemCreateWritableOptions boolean =keepExistingData false ; }; [Exposed =(Window ,Worker ),SecureContext ,Serializable ]interface :FileSystemFileHandle FileSystemHandle {Promise <File >getFile ();Promise <FileSystemWritableFileStream >createWritable (optional FileSystemCreateWritableOptions = {}); };options
A FileSystemFileHandle's associated entry must be a file entry.
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. In Chrome 82 they are.
2.4.1. The getFile() method
getFile() method, when invoked, must run these steps:
-
Let result be a new promise.
-
Run the following steps in parallel:
-
Let permissionStatus be the result of running entry’s query permission steps given «[ "
writable" →false]» and this’s relevant settings object. -
If permissionStatus is not
"granted", reject result with aNotAllowedErrorand abort. -
Let f be a new
File. -
Set f’s snapshot state to the current state of entry.
-
Set f’s underlying byte sequence to a copy of entry’s binary data.
-
Initialize the value of f’s
lastModifiedattribute to entry’s modification timestamp. -
Initialize the value of f’s
typeattribute to an implementation defined value, based on for example entry’s name and/or its file extension.The reading and snapshotting behvario needs to be better specified in the [FILE-API] spec, for now this is kind of hand-wavy.
-
Resolve result with f.
-
Return result.
2.4.2. The createWritable() method
In the Origin Trial as available in Chrome 82, createWritable replaces the createWriter method.
- stream = await fileHandle .
createWritable()- stream = await fileHandle .
createWritable({keepExistingData: true/false }) - stream = await fileHandle .
-
Returns a
FileSystemWritableFileStreamthat can be used to write to the file. Any changes made through stream won’t be reflected in the file represented by fileHandle until the stream has been closed. 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 stream up until the stream has been closed.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 writable filestream is closed.
If
keepExistingDataisfalseor 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 createWritable (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. <https://github.com/wicg/native-file-system/issues/67>
createWritable(options) method, when invoked, must run these steps:
-
Let result be a new promise.
-
Run the following steps in parallel:
-
Run entry’s request permission steps given «[ "
writable" →true]» and this’s relevant settings object. If that throws an exception, reject result with that exception and abort. -
Let permissionStatus be the result of running entry’s query permission steps given «[ "
writable" →true]» and this’s relevant settings object. -
If permissionStatus is not
"granted", reject result with aNotAllowedErrorand abort. -
Let stream be the result of creating a new FileSystemWritableFileStream for entry in this’s relevant realm.
-
If options.
keepExistingDataistrue:-
Set stream.[[buffer]] to a copy of entry’s binary data.
-
-
Resolve result with stream.
-
Return result.
2.5. 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 Promise <sequence <USVString >?>resolve (FileSystemHandle ); };possibleDescendant
A FileSystemDirectoryHandle's associated entry must be a directory entry.
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. In Chrome 82 they are.
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). <https://github.com/wicg/native-file-system/issues/98>
Should getEntries be its own method, or should FileSystemDirectoryHandle just be an async iterable itself? <https://github.com/wicg/native-file-system/issues/47>
2.5.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:
-
Let result be a new promise.
-
Run the following steps in parallel:
-
If name is not a valid file name, reject result with a
TypeErrorand abort. -
If options.
createistrue:-
Run entry’s request permission steps given «[ "
writable" →true]» and this’s relevant settings object. If that throws an exception, reject result with that exception and abort. -
Let permissionStatus be the result of running entry’s query permission steps given «[ "
writable" →true]» and this’s relevant settings object.
-
-
Otherwise:
-
Let permissionStatus be the result of running entry’s query permission steps given «[ "
writable" →false]» and this’s relevant settings object.
-
-
If permissionStatus is not
"granted", reject result with aNotAllowedErrorand abort. -
For each child of entry’s children:
-
If child’s name equals name:
-
If child is a directory entry:
-
Reject result with a
TypeMismatchErrorand abort.
-
-
Resolve result with a new
FileSystemFileHandlewhose entry is child and abort.
-
-
-
If options.
createisfalse:-
Reject result with a
NotFoundErrorand abort.
-
-
Let child be a new file entry.
-
Set child’s name to name.
-
Set child’s binary data to an empty byte sequence.
-
Set child’s modification timestamp to the current time.
-
If creating child in the underlying file system throws an exception, reject result with that exception and abort.
Better specify what possible exceptions this could throw. <https://github.com/wicg/native-file-system/issues/68>
-
Resolve result with a new
FileSystemFileHandlewhose entry is child.
-
-
Return result.
2.5.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:
-
Let result be a new promise.
-
Run the following steps in parallel:
-
If name is not a valid file name, reject result with a
TypeErrorand abort. -
If options.
createistrue:-
Run entry’s request permission steps given «[ "
writable" →true]» and this’s relevant settings object. If that throws an exception, reject result with that exception and abort. -
Let permissionStatus be the result of running entry’s query permission steps given «[ "
writable" →true]» and this’s relevant settings object.
-
-
Else:
-
Let permissionStatus be the result of running entry’s query permission steps given «[ "
writable" →false]» and this’s relevant settings object.
-
-
If permissionStatus is not
"granted", reject result with aNotAllowedErrorand abort. -
For each child of entry’s children:
-
If child’s name equals name:
-
If child is a file entry:
-
Reject result with a
TypeMismatchErrorand abort.
-
-
Resolve result with a new
FileSystemDirectoryHandlewhose entry is child and abort.
-
-
-
If options.
createisfalse:-
Reject result with a
NotFoundErrorand abort.
-
-
Let child be a new directory entry.
-
Set child’s name to name.
-
If creating child in the underlying file system throws an exception, reject result with that exception and abort.
Better specify what possible exceptions this could throw. <https://github.com/wicg/native-file-system/issues/68>
-
Resolve result with a new
FileSystemDirectoryHandlewhose entry is child.
-
-
Return result.
2.5.3. The getEntries() method
- for await (const handle of directoryHandle .
getEntries()) {} -
Iterates over all entries whose parent is the entry represented by directoryHandle.
Should FileSystemDirectoryHandle itself be async iterable, instead of having this getEntries method? <https://github.com/wicg/native-file-system/issues/158>
getEntries() method, when invoked, must run
these steps:
-
Let result be a new promise.
-
Run the following steps in parallel:
-
Let permissionStatus be the result of running entry’s query permission steps given «[ "
writable" →false]» and this’s relevant settings object. -
If permissionStatus is not
"granted", reject result with aNotAllowedErrorand abort. -
TODO (depends on WebIDL support for value async-iterators).
-
Return result.
2.5.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:
-
Let result be a new promise.
-
Run the following steps in parallel:
-
If name is not a valid file name, reject result with a
TypeErrorand abort. -
Run entry’s request permission steps given «[ "
writable" →true]» and this’s relevant settings object. If that throws an exception, reject result with that exception. -
Let permissionStatus be the result of running entry’s query permission steps given «[ "
writable" →true]» and this’s relevant settings object. -
If permissionStatus is not
"granted", reject result with aNotAllowedErrorand abort. -
For each child of entry’s children:
-
If child’s name equals name:
-
If child is a directory entry:
-
If child’s children is not empty and options.
recursiveisfalse:-
Reject result with an
InvalidModificationErrorand abort.
-
-
-
If removing child in the underlying file system throws an exception, reject result with that exception and abort.
Note: If
recursiveistrue, the removal can fail non-atomically. Some files or directories might have been removed while other files or directories still exist.Better specify what possible exceptions this could throw. <https://github.com/wicg/native-file-system/issues/68>
-
Resolve result with
undefined.
-
-
-
Reject result with a
NotFoundError.
-
-
Return result.
2.5.5. The resolve() method
- path = await directory .
resolve( child ) -
If child is equal to directory, path will be an empty array.
If child is a direct child of directory, path will be an array containing child’s name.
If child is a descendant of directory, path will be an array containing the names of all the intermediate directories and child’s name as last element.
Otherwise (directory and child are not related), path will be null.
This method is first available in Chrome 82.
// Assume we at some point got a valid directory handle. const dir_ref= current_project_dir; if ( ! dir_ref) return ; // Now get a file reference by showing a file picker: const file_ref= await self. chooseFileSystemEntries({ type: 'openFile' }); if ( ! file_ref) { // User cancelled, or otherwise failed to open a file. return ; } // Check if file_ref exists inside dir_ref: const relative_path= await dir_ref. resolve( file_ref); if ( relative_path=== null ) { // Not inside dir_ref } else { // relative_path is an array of names, giving the relative path // from dir_ref to the file that is represented by file_ref: assert relative_path. pop() == file_ref. name; let entry= dir_ref; for ( const nameof relative_path) { entry= await entry. getDirectory( name); } entry= await entry. getFile( file_ref. name); // Now |entry| will represent the same file on disk as |file_ref|. assert await entry. isSameEntry( file_ref) == true ; }
resolve(possibleDescendant) method,
when invoked, must return the result of resolving possibleDescendant’s entry relative to this's entry. 2.6. The FileSystemWritableFileStream interface
enum {WriteCommandType ,"write" ,"seek" , };"truncate" dictionary {WriteParams required WriteCommandType ;type unsigned long long ?;size unsigned long long ?; (position BufferSource or Blob or USVString )?; };data typedef (BufferSource or Blob or USVString or WriteParams ); [FileSystemWriteChunkType Exposed =(Window ,Worker ),SecureContext ]interface :FileSystemWritableFileStream WritableStream {Promise <void >write (FileSystemWriteChunkType );data Promise <void >seek (unsigned long long );position Promise <void >truncate (unsigned long long ); };size
A FileSystemWritableFileStream has an associated [[file]] (a file entry).
A FileSystemWritableFileStream has an associated [[buffer]] (a byte sequence).
It is initially empty.
Note: This buffer can get arbitrarily large, so it is expected that implementations will not keep this in memory, but instead use a temporary file for this. All access to [[buffer]] is done in promise returning methods and algorithms, so even though operations on it seem sync, implementations can implement them async.
A FileSystemWritableFileStream has an associated [[seekOffset]] (a number).
It is initially 0.
FileSystemWritableFileStream object is a WritableStream object with additional
convenience methods, which operates on a single file on disk.
Upon creation, an underlying sink will have been created and the stream will be usable. All operations executed on the stream are queuable and producers will be able to respond to backpressure.
The underlying sink’s write method, and therefore WritableStreamDefaultWriter’s write() method, will accept byte-like data or WriteParams as input.
The FileSystemWritableFileStream has a file position cursor initialized at byte offset 0 from the top of the file.
When using write() or by using WritableStream capabilities through the WritableStreamDefaultWriter’s write() method, this position will be advanced based on the number of bytes written through the stream object.
Similarly, when piping a ReadableStream into a FileSystemWritableFileStream object, this position is updated with the number of bytes that passed through the stream.
getWriter() returns an instance of WritableStreamDefaultWriter.
-
Let stream be a new
FileSystemWritableFileStreamin realm. -
Perform InitializeWritableStream(stream)
-
Set stream.[[file]] to file.
-
Let controller be a new
WritableStreamDefaultController. -
Let startAlgorithm be an algorithm that returns
undefined. -
Let writeAlgorithm be an algorithm which takes a chunk argument and returns the result of running the write a chunk algorithm with stream and chunk.
-
Let closeAlgorithm be the following steps:
-
Let closeResult be a new promise.
-
Run the following steps in parallel:
-
Let permissionStatus be the result of running stream.[[file]]'s query permission steps given «[ "
writable" →true]» and stream’s relevant settings object. -
If permissionStatus is not
"granted", reject closeResult with aNotAllowedErrorand abort. -
TODO: optional UA defined security checks
-
Set stream.[[file]]'s binary data to stream.[[buffer]]. If that throws an exception, reject closeResult with that exception and abort.
Note: It is expected that this atomically updates the contents of the file on disk being written to.
-
Resolve closeResult with
undefined.
-
-
Return closeResult.
-
-
Let abortAlgorithm be an algorithm that returns a promise resolved with
undefined. -
Let highWaterMark be 1.
-
Let sizeAlgorithm be an algorithm that returns
1. -
Perform SetUpWritableStreamDefaultController(stream, controller, startAlgorithm, writeAlgorithm, closeAlgorithm, abortAlgorithm, highWaterMark, sizeAlgorithm).
-
Return stream.
FileSystemWritableFileStream stream and chunk,
runs these steps:
-
Let input be the result of converting chunk to a
FileSystemWriteChunkType. If this throws an exception, then return a promise rejected with that exception. -
Let p be a new promise.
-
Run the following steps in parallel:
-
Let permissionStatus be the result of running stream.[[file]]'s query permission steps given «[ "
writable" →true]» and stream’s relevant settings object. -
If permissionStatus is not
"granted", reject p with aNotAllowedErrorand abort. -
Let command be input.
typeif input is aWriteParams, and"write"otherwise. -
If command is
"write":-
Let data be input.
dataif input is aWriteParams, and input otherwise. -
If data is
undefined, reject p with aTypeErrorand abort. -
Let writePosition be stream.[[seekOffset]].
-
If input is a
WriteParamsand input.positionis notundefined, set writePosition to input.position. -
Let oldSize be stream.[[buffer]]'s length.
-
If writePosition is larger than oldSize, reject p with a
InvalidStateErrorand abort.Not clear if this should reject, and if it does, is this really the right error type? Chrome’s implementation is actually inconsistent about this rejecting or not.
-
If data is a
BufferSource, let dataBytes be a copy of data. -
Else if data is a
Blob:-
Let dataBytes be the result of performing the read operation on data. If this throws an exception, reject p with that exception and abort.
-
-
Else:
-
Let dataBytes be the result of UTF-8 encoding data.
-
Let head be a byte sequence containing the first writePosition bytes of stream.[[buffer]].
-
Let tail be an empty byte sequence.
-
If writePosition + data.length is smaller than oldSize:
-
Let tail be a byte sequence containing the last oldSize - (writePosition + data.length) bytes of stream.[[buffer]].
-
-
Set stream.[[buffer]] to the concatenation of head, data and tail.
-
Set stream.[[seekOffset]] to writePosition + data.length.
-
Resolve p.
-
-
Else if command is
"seek": -
Else if command is
"truncate":-
If chunk.
sizeisundefined, reject p with aTypeErrorand abort. -
Let newSize be chunk.
size. -
Let oldSize be stream.[[buffer]]'s length.
-
If newSize is larger than oldSize:
-
Set stream.[[buffer]] to a byte sequence formed by concating stream.[[buffer]] with a byte sequence containing newSize-oldSize
0x00bytes.
-
-
Else if newSize is smaller than oldSize:
-
Set stream.[[buffer]] to a byte sequence containing the first newSize bytes in stream.[[buffer]].
-
-
If stream.[[seekOffset]] is bigger than newSize, set stream.[[seekOffset]] to newSize.
-
Resolve p.
-
-
-
Return p.
2.6.1. The write() method
- await stream .
write(data)- await stream .
write({type:"write",data: data }) - await stream .
-
Writes the content of data into the file associated with stream at the current file cursor offset.
No changes are written to the actual file on disk until the stream has been closed. Changes are typically written to a temporary file instead.
- await stream .
write({type:"write",position: position,data: data }) -
Writes the content of data into the file associated with stream at position bytes from the top of the file. Also updates the current file cursor offset to the end of the written data.
No changes are written to the actual file on disk until the stream has been closed. Changes are typically written to a temporary file instead.
- await stream .
write({type:"seek",position: position }) -
Updates the current file cursor offset the position bytes from the top of the file.
- await stream .
write({type:"truncate",size: size }) -
Resizes the file associated with stream to be size bytes long. If size is larger than the current file size this pads the file with null bytes, otherwise it truncates the file.
The file cursor is updated when
truncateis called. If the offset is smaller than offset, it remains unchanged. If the offset is larger than size, the offset is set to size to ensure that subsequent writes do not error.No changes are written to the actual file until on disk until the stream has been closed. Changes are typically written to a temporary file instead.
write(data) method, when invoked, must run
these steps:
-
Let writer be the result of calling AcquireWritableStreamDefaultWriter(this).
-
Let result be WritableStreamDefaultWriterWrite(writer, data).
-
Perform WritableStreamDefaultWriterRelease(writer).
-
Return result.
2.6.2. The seek() method
- await stream .
seek(position) -
Updates the current file cursor offset the position bytes from the top of the file.
seek(position) method, when invoked, must run these
steps:
-
Let writer be the result of calling AcquireWritableStreamDefaultWriter(this).
-
Let result be WritableStreamDefaultWriterWrite(writer,
{).type:"seek",position: position,size: undefined,data: undefined} -
Perform WritableStreamDefaultWriterRelease(writer).
-
Return result.
2.6.3. The truncate() method
- await stream .
truncate(size) -
Resizes the file associated with stream to be size bytes long. If size is larger than the current file size this pads the file with null bytes, otherwise it truncates the file.
The file cursor is updated when
truncateis called. If the offset is smaller than offset, it remains unchanged. If the offset is larger than size, the offset is set to size to ensure that subsequent writes do not error.No changes are written to the actual file until on disk until the stream has been closed. Changes are typically written to a temporary file instead.
truncate(size) method, when invoked, must run these
steps:
-
Let writer be the result of calling AcquireWritableStreamDefaultWriter(this).
-
Let result be WritableStreamDefaultWriterWrite(writer,
{).type:"truncate",size: size,position: undefined,data: undefined} -
Perform WritableStreamDefaultWriterRelease(writer).
-
Return result.
3. Accessing Native File System
3.1. Native File System Permissions
FileSystemHandlePermissionDescriptor descriptor and an environment settings object settings are:
-
If descriptor.
writableis true:-
Let readState be the result of running the query permission steps for entry given «[ "
writable" →false]» and settings. -
If readState is not
"granted", return readState.
-
-
If there was a previous invocation of this algorithm for the same entry, descriptor and settings, returning previousResult, and the UA has not received new information about the user’s intent since that invocation, return previousResult.
-
Return whichever of the following options most accurately reflects the user’s intent for the calling algorithm:
The fact that the user picked the specific files returned by chooseFileSystemEntries() 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 chooseFileSystemEntries() resolves, the query permission steps for the returned entries
given «[ "writable" → false ]»
should return "granted".
Additionally for calls to chooseFileSystemEntries with type="save-file",
the query permission steps for the returned entries
given «[ "writable" → true ]»
should also return "granted".
FileSystemHandlePermissionDescriptor descriptor and an environment settings object settings are:
-
Let status be the result of running the query permission steps for entry given descriptor and settings.
-
If status is not
"prompt"return. -
Let global be settings’s global object.
-
If global is not a
Window, return. -
If global does not have transient activation, throw a
NotAllowedError. -
If settings’s origin is not same origin with settings’s top-level origin, throw a
NotAllowedError. -
Display a prompt to the user requesting access as described by descriptor to entry.
-
Wait for the user to accept or reject the prompt. The user’s interaction may provide new information about the user’s intent for this descriptor and entry. Additionally if descriptor.
writableis true, the user’s interaction may also provide new information about the user’s intent for the same entry with «[ "writable" →false]».
Note: This is intentionally vague about the details of the permission UI and how the UA infers user intent. UAs should be able to explore a variety of UI approaches within this framework.
3.2. 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.
typespecifies 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.
multipleis false (or absent) the user can only select a single file, and the result will be a singleFileSystemHandle. If on the other hand options.multipleis true, the dialog can let the user select more than one file, and result will be an array ofFileSystemHandleinstances (even if the user did select a single file, ifmultipleis true this will be returned as a single-element array).Finally options.
acceptsand options.excludeAcceptAllOptionspecify the types of files the dialog will let the user select. Each entry in options.acceptsdescribes a single type of file, consisting of adescription, zero or moremimeTypesand zero or moreextensions. Options with no validmimeTypesand noextensionsare invalid and are ignored. If nodescriptionis provided one will be generated.If options.
excludeAcceptAllOptionis true, or if no valid entries exist in options.accepts, an option matching all files will be included in the file types the dialog lets the user select.
Should this return an array, even when multiple=false? And related questions, should this be one method that can return either files or directories depending on the passed in options, or separate file and directory methods? <https://github.com/wicg/native-file-system/issues/25>
chooseFileSystemEntries(options) method, when invoked, must run
these steps:
-
If options.
typeis"save-file"and options.multipleis true, return a promise rejected with aTypeError. -
Let environment be this’s relevant settings object.
-
If environment’s origin is an opaque origin, return a promise rejected with a
SecurityError. -
If environment’s origin is not same origin with environment’s top-level origin, return a promise rejected with a
SecurityError. -
Let global be environment’s global object.
-
If global does not have transient activation, throw a
SecurityError.Should this be
SecurityErrororNotAllowedError(and same question for the request permission steps checking for user activation)? -
Let p be a new promise.
-
Run the following steps in parallel:
-
Optionally, wait until any prior execution of this algorithm has terminated.
-
If options.
typeis"open-directory":-
Display a prompt to the user requesting that the user specify some directories. If options.
multipleis false, there must be no more than one directory selected; otherwise any number may be selected.Note: Chrome currently ignores the
multipleoption in combination with"open-directory". It always only lets the user select a single directory.Note:
"open-directory"also ignores theacceptsandexcludeAcceptAllOptionoptions.
-
-
Otherwise:
-
For each accept of options.
accepts:-
If accept.
mimeTypesis empty and accept.extensionsis empty, continue. -
Let description be accept.
description. -
Let filter be the following steps, given a filename (a string), and a type (a MIME type).
-
For each extension of accept.
extensions:-
If filename ends with "." followed by extension, return
true.
-
-
For each typeString of accept.
mimeTypes:-
Let parsedType be the result of parse a MIME type with typeString.
-
If type matches parsedType, return
true.The previous two steps are not correct. We do want to allow things like "image/*" as well, which are not valid for the parse a MIME type algorithm. Also we need to specify what it means for types to "match". So figure out a better way of writing this.
In general we need to better define how mime types are used here. <https://github.com/wicg/native-file-system/issues/133>
-
-
Return
false.
-
-
If description is an empty string, set description to some user understandable string describing filter.
-
Append description/filter to accepts options.
-
-
If either accepts options is empty, or options.
excludeAcceptAllOptionisfalse:-
Let description be a user understandable string describing "all files".
-
Let filter be an algorithm that returns
true. -
Append description/filter to accepts options.
-
-
Display a prompt to the user requesting that the user specify some files. If options.
multipleis 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 left up to the user agent.
If options.
typeis"save-file", the displayed prompt should make it clear that the website wants to write to the selected file.
-
Wait for the user to have made their selection.
-
If the user dismissed the prompt without making a selection, reject p with an
AbortErrorand abort.Should this resolve with
undefinedinstead? <https://github.com/wicg/native-file-system/issues/84> -
Let entries be a list of entries representing the selected files or directories.
-
Let result be a empty list.
-
For each entry of entries:
-
If entry is deemed too sensitive or dangerous to be exposed to this website by the user agent:
-
Inform the user that the selected files or directories can’t be exposed to this website.
-
At the discretion of the user agent, either go back to the beginning of these in parallel steps, or reject p with an
AbortErrorand abort.
-
-
Set entry’s query permission steps to the native file system query permission steps.
-
Set entry’s request permission steps to the native file system request permission steps.
-
If options.
typeis"open-directory":-
Add a new
FileSystemDirectoryHandleassociated with entry to result.
-
-
Otherwise:
-
Add a new
FileSystemFileHandleassociated with entry to result.
-
-
-
If options.
typeis"save-file":-
Set entries[0]'s binary data to an empty byte sequence.
-
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.
Rather than requiring the website to prompt separately for a writable directory, we should provide some kind of API to request a writable directory in one step. <https://github.com/wicg/native-file-system/issues/89>
-
If options.
multipleis true:-
Resolve p with result.
-
-
Otherwise:
-
-
Return p.
4. Accessing special file systems
4.1. Special File System Concepts
A bucket contains a sandboxed file system root, a directory entry.
The sandboxed file system root for one bucket must be completely separate from that of any other bucket.
Note: While user agents will typically implement this by persisting the contents of this sandboxed file system root to disk, it is not intended that the contents are easily user accessible. Similarly there is no expectation that files or directories with names matching the names of children of the sandboxed file system root exist.
Note: In Chrome this sandboxed file system root refers to the same storage as the temporary file system as used to be defined in File API: Directories and System.
4.2. 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 file system.
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. -
Let storage be environment’s origin's site storage unit.
-
Let bucket be storage’s bucket.
-
Return a promise resolved with a new
FileSystemDirectoryHandle, whose associated entry is bucket’s sandboxed file system root.
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 (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 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.