diff --git a/index.bs b/index.bs index 6ebd66e..df746f7 100644 --- a/index.bs +++ b/index.bs @@ -3,7 +3,7 @@ Group: WHATWG H1: File System Shortname: fs Text Macro: TWITTER whatfilesystem -Text Macro: LATESTRD 2022-03 +Text Macro: LATESTRD 2022-09 Abstract: File System defines infrastructure for file systems as well as their API. Indent: 2 Markup Shorthands: css no, markdown yes diff --git a/review-drafts/2022-09.bs b/review-drafts/2022-09.bs new file mode 100644 index 0000000..133766b --- /dev/null +++ b/review-drafts/2022-09.bs @@ -0,0 +1,989 @@ +
+Group: WHATWG
+Status: RD
+Date: 2022-09-19
+H1: File System
+Shortname: fs
+Text Macro: TWITTER whatfilesystem
+Text Macro: LATESTRD 2022-09
+Abstract: File System defines infrastructure for file systems as well as their API.
+Indent: 2
+Markup Shorthands: css no, markdown yes
+
+ + + +
+urlPrefix: https://tc39.es/ecma262/; spec: ECMA-262
+  type: dfn; text: realm; url: realm
+urlPrefix: https://storage.spec.whatwg.org/; spec: storage
+  type: dfn; text: storage; url: site-storage
+
+ + + + +# Introduction # {#introduction} + +*This section is non-normative.* + +This document defines fundamental infrastructure for file system APIs. In addition, it defines an +API that makes it possible for websites to get access to a file system directory without having to +first prompt the user for access. This enables use cases where a website wants to save data to disk +before a user has picked a location to save to, without forcing the website to use a completely +different storage mechanism with a different API for such files. The entry point for this is the +{{StorageManager/getDirectory()|navigator.storage.getDirectory()}} method. + + +# Files and Directories # {#files-and-directories} + +## Concepts ## {#concepts} + +An entry is either a [=file entry=] or a [=directory entry=]. + + +Each [=/entry=] has an associated query access algorithm, which takes "`read`" +or "`readwrite`" mode and returns a {{PermissionState}}. Unless specified +otherwise it returns "{{PermissionState/denied}}". The algorithm is allowed to throw. + +Each [=/entry=] has an associated request access algorithm, which takes +"`read`" or "`readwrite`" mode and returns a {{PermissionState}}. Unless specified +otherwise it returns "{{PermissionState/denied}}". The algorithm is allowed to throw. + +Note: Implementations that only implement this specification and not dependent specifications do not +need to bother implementing [=/entry=]'s [=entry/query access=] and [=entry/request access=]. + +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. + +Issue: We should consider having further normative restrictions on file names that will +never be allowed using this API, rather than leaving it entirely up to underlying file +systems. + +A file entry additionally consists of +binary data (a [=byte sequence=]) and a +modification timestamp (a number representing the number of milliseconds since the Unix Epoch). + +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 [=list/contained=] in the [=children=] of at most one +[=directory entry=], and that directory entry is also known as |entry|'s parent. +An [=/entry=]'s [=entry/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. + +[=/Entries=] can (but don't have to) be backed by files on the host operating system's local 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 local file system. + +Issue: 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). + +
+To resolve an [=/entry=] |child| relative to a [=directory entry=] |root|, +run the following steps: + +1. Let |result| be [=a new promise=]. +1. Run the following steps [=in parallel=]: + 1. If |child| is [=the same as=] |root|, + [=/resolve=] |result| with an empty list, and abort. + 1. Let |childPromises| be « ». + 1. [=set/For each=] |entry| of |root|'s [=FileSystemHandle/entry=]'s [=children=]: + 1. Let |p| be the result of [=entry/resolving=] |child| relative to |entry|. + 1. [=list/Append=] |p| to |childPromises|. + 1. [=Upon fulfillment=] of |p| with value |path|: + 1. If |path| is not null: + 1. [=list/Prepend=] |entry|'s [=entry/name=] to |path|. + 1. [=/Resolve=] |result| with |path|. + 1. [=Wait for all=] |childPromises|, with the following success steps: + 1. If |result| hasn't been resolved yet, [=/resolve=] |result| with `null`. +1. Return |result|. + +
+ +## The {{FileSystemHandle}} interface ## {#api-filesystemhandle} + + +enum FileSystemHandleKind { + "file", + "directory", +}; + +[Exposed=(Window,Worker), SecureContext, Serializable] +interface FileSystemHandle { + readonly attribute FileSystemHandleKind kind; + readonly attribute USVString name; + + Promise<boolean> isSameEntry(FileSystemHandle other); +}; + + +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=]. + +Their [=serialization steps=], given |value|, |serialized| and forStorage are: + +1. Set |serialized|.\[[Origin]] to |value|'s [=relevant settings object=]'s [=environment settings object/origin=]. +1. Set |serialized|.\[[Entry]] to |value|'s [=FileSystemHandle/entry=]. + +
+ +
+Their [=deserialization steps=], given |serialized| and |value| are: + +1. If |serialized|.\[[Origin]] is not [=same origin=] with + |value|'s [=relevant settings object=]'s [=environment settings object/origin=], + then throw a {{DataCloneError}}. +1. Set |value|'s [=FileSystemHandle/entry=] to |serialized|.\[[Entry]] + +
+ +
+ : |handle| . {{FileSystemHandle/kind}} + :: Returns {{FileSystemHandleKind/"file"}} if |handle| is a {{FileSystemFileHandle}}, + or {{FileSystemHandleKind/"directory"}} if |handle| is a {{FileSystemDirectoryHandle}}. + + This can be used to distinguish files from directories when iterating over the contents + of a directory. + + : |handle| . {{FileSystemHandle/name}} + :: Returns the [=entry/name=] of the entry represented by |handle|. +
+ +The kind attribute must +return {{FileSystemHandleKind/"file"}} if the associated [=FileSystemHandle/entry=] is a [=file entry=], +and return {{FileSystemHandleKind/"directory"}} otherwise. + +The name attribute must return the [=entry/name=] of the +associated [=FileSystemHandle/entry=]. + +### The {{FileSystemHandle/isSameEntry()}} method ### {#api-filesystemhandle-issameentry} + +
+ : same = await |handle1| . {{FileSystemHandle/isSameEntry()|isSameEntry}}( |handle2| ) + :: Returns true if |handle1| and |handle2| represent the same file or directory. +
+ +
+The isSameEntry(|other|) method, when invoked, must run these steps: + +1. Let |realm| be [=this=]'s [=relevant Realm=]. +1. Let |p| be [=a new promise=] in |realm|. +1. Run the following steps [=in parallel=]: + 1. If [=this=]'s [=FileSystemHandle/entry=] is [=the same as=] |other|'s [=FileSystemHandle/entry=], + [=/resolve=] |p| with `true`. + 1. Else [=/resolve=] |p| with `false`. +1. Return |p|. + +
+ +## The {{FileSystemFileHandle}} interface ## {#api-filesystemfilehandle} + + +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 [=FileSystemHandle/entry=] must be a [=file entry=]. + +{{FileSystemFileHandle}} objects are [=serializable objects=]. Their [=serialization steps=] and +[=deserialization steps=] are the same as those for {{FileSystemHandle}}. + +### The {{FileSystemFileHandle/getFile()}} method ### {#api-filesystemfilehandle-getfile} + +
+ : file = await |fileHandle| . {{FileSystemFileHandle/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. Let |result| be [=a new promise=]. +1. Run the following steps [=in parallel=]: + 1. Let |access| be the result of running [=this=]'s [=FileSystemHandle/entry=]'s + [=entry/query access=] given "`read`". + 1. If |access| is not "{{PermissionState/granted}}", + reject |result| with a {{NotAllowedError}} and abort. + 1. Let |entry| be [=this=]'s [=FileSystemHandle/entry=]. + 1. Let |f| be a new {{File}}. + 1. Set |f|'s snapshot state to the current state of |entry|. + 1. Set |f|'s underlying byte sequence to a copy of |entry|'s [=binary data=]. + 1. Initialize the value of |f|'s {{File/name}} attribute to |entry|'s [=entry/name=]. + 1. Initialize the value of |f|'s {{File/lastModified}} attribute to |entry|'s [=file entry/modification timestamp=]. + 1. Initialize the value of |f|'s {{Blob/type}} attribute to an [=implementation-defined=] value, based on for example |entry|'s [=entry/name=] or its file extension. + + Issue: The reading and snapshotting behavior needs to be better specified in the [[FILE-API]] spec, + for now this is kind of hand-wavy. + 1. [=/Resolve=] |result| with |f|. +1. Return |result|. + +
+ +### The {{FileSystemFileHandle/createWritable()}} method ### {#api-filesystemfilehandle-createwritable} + +
+ : |stream| = await |fileHandle| . {{FileSystemFileHandle/createWritable()}} + : |stream| = await |fileHandle| . {{FileSystemFileHandle/createWritable()|createWritable}}({ {{FileSystemCreateWritableOptions/keepExistingData}}: true/false }) + :: Returns a {{FileSystemWritableFileStream}} that 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 contain 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 {{FileSystemCreateWritableOptions/keepExistingData}} is `false` or not specified, + the temporary file starts out empty, + otherwise the existing file is first copied to this temporary file. +
+ +Issue(67): 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. + +
+The createWritable(|options|) method, when invoked, must run these steps: + +1. Let |result| be [=a new promise=]. +1. Run the following steps [=in parallel=]: + 1. Let |access| be the result of running [=this=]'s [=FileSystemHandle/entry=]'s + [=entry/request access=] given "`readwrite`". + If that throws an exception, [=reject=] |result| with that exception and abort. + 1. If |access| is not "{{PermissionState/granted}}", + reject |result| with a {{NotAllowedError}} and abort. + 1. Let |entry| be [=this=]'s [=FileSystemHandle/entry=]. + 1. Let |stream| be the result of [=create a new FileSystemWritableFileStream|creating a new FileSystemWritableFileStream=] + for |entry| in [=this=]'s [=relevant realm=]. + 1. If |options|.{{FileSystemCreateWritableOptions/keepExistingData}} is `true`: + 1. Set |stream|.[=[[buffer]]=] to a copy of |entry|'s [=file entry/binary data=]. + 1. [=/Resolve=] |result| with |stream|. +1. Return |result|. + +
+ +## The {{FileSystemDirectoryHandle}} interface ## {#api-filesystemdirectoryhandle} + + +dictionary FileSystemGetFileOptions { + boolean create = false; +}; + +dictionary FileSystemGetDirectoryOptions { + boolean create = false; +}; + +dictionary FileSystemRemoveOptions { + boolean recursive = false; +}; + +[Exposed=(Window,Worker), SecureContext, Serializable] +interface FileSystemDirectoryHandle : FileSystemHandle { + async iterable<USVString, FileSystemHandle>; + + Promise<FileSystemFileHandle> getFileHandle(USVString name, optional FileSystemGetFileOptions options = {}); + Promise<FileSystemDirectoryHandle> getDirectoryHandle(USVString name, optional FileSystemGetDirectoryOptions options = {}); + + Promise<undefined> removeEntry(USVString name, optional FileSystemRemoveOptions options = {}); + + Promise<sequence<USVString>?> resolve(FileSystemHandle possibleDescendant); +}; + + +A {{FileSystemDirectoryHandle}}'s associated [=FileSystemHandle/entry=] must be a [=directory entry=]. + +{{FileSystemDirectoryHandle}} objects are [=serializable objects=]. Their [=serialization steps=] and +[=deserialization steps=] are the same as those for {{FileSystemHandle}}. + +### Directory iteration ### {#api-filesystemdirectoryhandle-asynciterable} + +
+ : for await (let [|name|, |handle|] of |directoryHandle|) {} + : for await (let [|name|, |handle|] of |directoryHandle| . entries()) {} + : for await (let |handle| of |directoryHandle| . values()) {} + : for await (let |name| of |directoryHandle| . keys()) {} + :: Iterates over all entries whose parent is the entry represented by |directoryHandle|. Entries + that are created or deleted while the iteration is in progress might or might not be included. + No guarantees are given either way. +
+ +Issue(173): In the future we might want to add arguments to the async iterable declaration to +support for example recursive iteration. + +
+The [=asynchronous iterator initialization steps=] for a {{FileSystemDirectoryHandle}} |handle| +ant its async iterator |iterator| are: + +1. Let |access| be the result of running |handle|'s [=FileSystemHandle/entry=]'s + [=entry/query access=] given "`read`". + +1. If |access| is not "{{PermissionState/granted}}", + throw a {{NotAllowedError}}. + +1. Set |iterator|'s past results to an empty [=/set=]. + +
+ +
+To [=get the next iteration result=] for a {{FileSystemDirectoryHandle}} |handle| +and its async iterator |iterator|: + +1. Let |promise| be [=a new promise=]. + +1. Let |directory| be |handle|'s [=FileSystemHandle/entry=]. + +1. Let |access| be the result of running |handle|'s [=FileSystemHandle/entry=]'s + [=entry/query access=] given "`read`". + +1. If |access| is not "{{PermissionState/granted}}", + reject |promise| with a {{NotAllowedError}} and return |promise|. + +1. Let |child| be an [=/entry=] in |directory|'s [=directory entry/children=], + such that |child|'s [=entry/name=] is not contained in |iterator|'s [=past results=], + or `null` if no such entry exists. + + Note: This is intentionally very vague about the iteration order. Different platforms + and file systems provide different guarantees about iteration order, and we want it to + be possible to efficiently implement this on all platforms. As such no guarantees are given + about the exact order in which elements are returned. + +1. If |child| is `null`, then: + 1. [=/Resolve=] |promise| with `undefined`. + +1. Otherwise: + 1. [=set/Append=] |child|'s [=entry/name=] to |iterator|'s [=past results=]. + 1. If |child| is a [=file entry=]: + 1. Let |result| be a new {{FileSystemFileHandle}} associated with |child|. + 1. Otherwise: + 1. Let |result| be a new {{FileSystemDirectoryHandle}} associated with |child|. + 1. [=/Resolve=] |promise| with (|child|'s [=entry/name=], |result|). + +1. Return |promise|. + +
+ +### The {{FileSystemDirectoryHandle/getFileHandle()}} method ### {#api-filesystemdirectoryhandle-getfilehandle} + +
+ : |fileHandle| = await |directoryHandle| . {{FileSystemDirectoryHandle/getFileHandle()|getFileHandle}}(|name|) + : |fileHandle| = await |directoryHandle| . {{FileSystemDirectoryHandle/getFileHandle()|getFileHandle}}(|name|, { {{FileSystemGetFileOptions/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| . {{FileSystemDirectoryHandle/getFileHandle()|getFileHandle}}(|name|, { {{FileSystemGetFileOptions/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 { {{FileSystemGetFileOptions/create}}: false }. +
+ +
+The getFileHandle(|name|, |options|) method, when invoked, +must run these steps: + +1. Let |result| be [=a new promise=]. +1. Run the following steps [=in parallel=]: + 1. If |name| is not a [=valid file name=], [=/reject=] |result| with a {{TypeError}} and abort. + + 1. Let |entry| be [=this=]'s [=FileSystemHandle/entry=]. + 1. If |options|.{{FileSystemGetFileOptions/create}} is `true`: + 1. Let |access| be the result of running [=this=]'s [=FileSystemHandle/entry=]'s + [=entry/request access=] given "`readwrite`". + If that throws an exception, [=reject=] |result| with that exception and abort. + 1. Otherwise: + 1. Let |access| be the result of running [=this=]'s [=FileSystemHandle/entry=]'s + [=entry/query access=] given "`read`". + 1. If |access| is not "{{PermissionState/granted}}", + reject |result| with a {{NotAllowedError}} and abort. + + 1. [=set/For each=] |child| of |entry|'s [=directory entry/children=]: + 1. If |child|'s [=entry/name=] equals |name|: + 1. If |child| is a [=directory entry=]: + 1. [=/Reject=] |result| with a {{TypeMismatchError}} and abort. + 1. [=/Resolve=] |result| with a new {{FileSystemFileHandle}} whose [=FileSystemHandle/entry=] is |child| and abort. + 1. If |options|.{{FileSystemGetFileOptions/create}} is `false`: + 1. [=/Reject=] |result| with a {{NotFoundError}} and abort. + 1. Let |child| be a new [=file entry=] whose [=query access=] and [=request access=] algorithms + are those of |entry|. + 1. Set |child|'s [=entry/name=] to |name|. + 1. Set |child|'s [=binary data=] to an empty [=byte sequence=]. + 1. Set |child|'s [=modification timestamp=] to the current time. + 1. [=set/Append=] |child| to |entry|'s [=directory entry/children=]. + 1. If creating |child| in the underlying file system throws an exception, + [=/reject=] |result| with that exception and abort. + + Issue(68): Better specify what possible exceptions this could throw. + 1. [=/Resolve=] |result| with a new {{FileSystemFileHandle}} whose [=FileSystemHandle/entry=] is |child|. +1. Return |result|. + +
+ +### The {{FileSystemDirectoryHandle/getDirectoryHandle()}} method ### {#api-filesystemdirectoryhandle-getdirectoryhandle} + +
+ : |subdirHandle| = await |directoryHandle| . {{FileSystemDirectoryHandle/getDirectoryHandle()|getDirectoryHandle}}(|name|) + : |subdirHandle| = await |directoryHandle| . {{FileSystemDirectoryHandle/getDirectoryHandle()|getDirectoryHandle}}(|name|, { {{FileSystemGetDirectoryOptions/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| . {{FileSystemDirectoryHandle/getDirectoryHandle()|getDirectoryHandle}}(|name|, { {{FileSystemGetDirectoryOptions/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 { {{FileSystemGetDirectoryOptions/create}}: false }. +
+ +
+The getDirectoryHandle(|name|, |options|) method, when +invoked, must run these steps: + +1. Let |result| be [=a new promise=]. +1. Run the following steps [=in parallel=]: + 1. If |name| is not a [=valid file name=], [=/reject=] |result| with a {{TypeError}} and abort. + + 1. Let |entry| be [=this=]'s [=FileSystemHandle/entry=]. + 1. If |options|.{{FileSystemGetDirectoryOptions/create}} is `true`: + 1. Let |access| be the result of running [=this=]'s [=FileSystemHandle/entry=]'s + [=entry/request access=] given "`readwrite`". + If that throws an exception, [=reject=] |result| with that exception and abort. + 1. Otherwise: + 1. Let |access| be the result of running [=this=]'s [=FileSystemHandle/entry=]'s + [=entry/query access=] given "`read`". + 1. If |access| is not "{{PermissionState/granted}}", + reject |result| with a {{NotAllowedError}} and abort. + + 1. [=set/For each=] |child| of |entry|'s [=directory entry/children=]: + 1. If |child|'s [=entry/name=] equals |name|: + 1. If |child| is a [=file entry=]: + 1. [=/Reject=] |result| with a {{TypeMismatchError}} and abort. + 1. [=/Resolve=] |result| with a new {{FileSystemDirectoryHandle}} whose [=FileSystemHandle/entry=] is |child| and abort. + 1. If |options|.{{FileSystemGetFileOptions/create}} is `false`: + 1. [=/Reject=] |result| with a {{NotFoundError}} and abort. + 1. Let |child| be a new [=directory entry=] whose [=query access=] and [=request access=] + algorithms are those of |entry|. + 1. Set |child|'s [=entry/name=] to |name|. + 1. Set |child|'s [=directory entry/children=] to an empty [=/set=]. + 1. [=set/Append=] |child| to |entry|'s [=directory entry/children=]. + 1. If creating |child| in the underlying file system throws an exception, + [=/reject=] |result| with that exception and abort. + + Issue(68): Better specify what possible exceptions this could throw. + 1. [=/Resolve=] |result| with a new {{FileSystemDirectoryHandle}} whose [=FileSystemHandle/entry=] is |child|. +1. Return |result|. + +
+ +### The {{FileSystemDirectoryHandle/removeEntry()}} method ### {#api-filesystemdirectoryhandle-removeentry} + +
+ : await |directoryHandle| . {{FileSystemDirectoryHandle/removeEntry()|removeEntry}}(|name|) + : await |directoryHandle| . {{FileSystemDirectoryHandle/removeEntry()|removeEntry}}(|name|, { {{FileSystemRemoveOptions/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| . {{FileSystemDirectoryHandle/removeEntry()|removeEntry}}(|name|, { {{FileSystemRemoveOptions/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. Let |result| be [=a new promise=]. +1. Run the following steps [=in parallel=]: + 1. If |name| is not a [=valid file name=], [=/reject=] |result| with a {{TypeError}} and abort. + + 1. Let |entry| be [=this=]'s [=FileSystemHandle/entry=]. + 1. Let |access| be the result of running [=this=]'s [=FileSystemHandle/entry=]'s + [=entry/request access=] given "`readwrite`". + If that throws an exception, [=reject=] |result| with that exception and abort. + 1. If |access| is not "{{PermissionState/granted}}", + reject |result| with a {{NotAllowedError}} and abort. + + 1. [=set/For each=] |child| of |entry|'s [=directory entry/children=]: + 1. If |child|'s [=entry/name=] equals |name|: + 1. If |child| is a [=directory entry=]: + 1. If |child|'s [=directory entry/children=] is not [=set/is empty|empty=] and |options|.{{FileSystemRemoveOptions/recursive}} is `false`: + 1. [=/Reject=] |result| with an {{InvalidModificationError}} and abort. + 1. [=set/Remove=] |child| from |entry|'s [=directory entry/children=]. + 1. If removing |child| in the underlying file system throws an exception, + [=/reject=] |result| with that exception and abort. + + Note: If {{FileSystemRemoveOptions/recursive}} is `true`, the removal can fail + non-atomically. Some files or directories might have been removed while other files + or directories still exist. + + Issue(68): Better specify what possible exceptions this could throw. + 1. [=/Resolve=] |result| with `undefined`. + 1. [=/Reject=] |result| with a {{NotFoundError}}. +1. Return |result|. + + +
+ +### The {{FileSystemDirectoryHandle/resolve()}} method ### {#api-filesystemdirectoryhandle-resolve} + +
+ : |path| = await |directory| . {{FileSystemDirectoryHandle/resolve()|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. + For example if |directory| represents `/home/user/project` + and |child| represents `/home/user/project/foo/bar`, this will return + `['foo', 'bar']`. + :: Otherwise (|directory| and |child| are not related), |path| will be null. +
+ +
+ +// 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: +const file_ref = await dir_ref.getFileHandle(filename, { create: true }); + +// 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 name of 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; +} + +
+ +
+The resolve(|possibleDescendant|) method, +when invoked, must return the result of [=entry/resolving=] +|possibleDescendant|'s [=FileSystemHandle/entry=] relative to [=this=]'s [=FileSystemHandle/entry=]. + +
+ + + +## The {{FileSystemWritableFileStream}} interface ## {#api-filesystemwritablefilestream} + + +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<undefined> write(FileSystemWriteChunkType data); + Promise<undefined> seek(unsigned long long position); + Promise<undefined> 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. + +
+A {{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/write()|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 {{FileSystemWritableFileStream/write()|write()}} or by using WritableStream capabilities through the {{WritableStreamDefaultWriter/write()|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. + +{{WritableStream/getWriter()|getWriter()}} returns an instance of {{WritableStreamDefaultWriter}}. +
+ +
+To create a new FileSystemWritableFileStream given a [=file entry=] |file| +in a [=/Realm=] |realm|, perform the following steps: + +1. Let |stream| be a [=new=] {{FileSystemWritableFileStream}} in |realm|. +1. Set |stream|.[=FileSystemWritableFileStream/[[file]]=] to |file|. +1. 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|. +1. Let |closeAlgorithm| be the following steps: + 1. Let |closeResult| be [=a new promise=]. + 1. Run the following steps [=in parallel=]: + 1. Let |access| be the result of running |file|'s [=entry/query access=] given "`readwrite`". + 1. If |access| is not "{{PermissionState/granted}}", + reject |closeResult| with a {{NotAllowedError}} and abort. + 1. Perform [=implementation-defined=] malware scans and safe browsing checks. + If these checks fail, [=/reject=] |closeResult| with an {{AbortError}} and abort. + 1. Set |stream|.[=[[file]]=]'s [=file entry/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. + 1. [=/Resolve=] |closeResult| with `undefined`. + 1. Return |closeResult|. +1. Let |highWaterMark| be 1. +1. Let |sizeAlgorithm| be an algorithm that returns `1`. +1. [=WritableStream/Set up=] |stream| with writeAlgorithm set to |writeAlgorithm|, closeAlgorithm set to |closeAlgorithm|, highWaterMark set to |highWaterMark|, and sizeAlgorithm set to |sizeAlgorithm|. +1. Return |stream|. + +
+ +
+The write a chunk algorithm, +given a {{FileSystemWritableFileStream}} |stream| and |chunk|, +runs these steps: + +1. Let |input| be the result of [=converting=] |chunk| to a {{FileSystemWriteChunkType}}. + If this throws an exception, then return [=a promise rejected with=] that exception. +1. Let |p| be [=a new promise=]. +1. Run the following steps [=in parallel=]: + 1. Let |access| be the result of running |stream|'s [=FileSystemWritableFileStream/[[file]]=]'s + [=entry/query access=] given "`readwrite`". + 1. If |access| is not "{{PermissionState/granted}}", + reject |p| with a {{NotAllowedError}} and abort. + 1. Let |command| be |input|.{{WriteParams/type}} if |input| is a {{WriteParams}}, + and {{WriteCommandType/"write"}} otherwise. + 1. If |command| is {{WriteCommandType/"write"}}: + 1. Let |data| be |input|.{{WriteParams/data}} if |input| is a {{WriteParams}}, + and |input| otherwise. + 1. If |data| is `undefined`, + reject |p| with a {{TypeError}} and abort. + 1. Let |writePosition| be |stream|.[=[[seekOffset]]=]. + 1. If |input| is a {{WriteParams}} and |input|.{{WriteParams/position}} is not `undefined`, + set |writePosition| to |input|.{{WriteParams/position}}. + 1. Let |oldSize| be |stream|.[=[[buffer]]=]'s [=byte sequence/length=]. + 1. If |data| is a {{BufferSource}}, + let |dataBytes| be [=get a copy of the buffer source|a copy of=] |data|. + 1. Else if |data| is a {{Blob}}: + 1. Let |dataBytes| be the result of performing the + read operation on |data|. + If this throws an exception, [=/reject=] |p| with that exception and abort. + 1. Else: + 1. [=Assert=]: |data| is a {{USVString}}. + 1. Let |dataBytes| be the result of [=UTF-8 encoding=] |data|. + 1. If |writePosition| is larger than |oldSize|, + append |writePosition| - |oldSize| `0x00` (NUL) bytes to the end of |stream|.[=[[buffer]]=]. + + Note: Implementations are expected to behave as if the skipped over file contents + are indeed filled with NUL bytes. That doesn't mean these bytes have to actually be + written to disk and take up disk space. Instead most file systems support so called + sparse files, where these NUL bytes don't take up actual disk space. + + 1. Let |head| be a [=byte sequence=] containing the first |writePosition| bytes of |stream|.[=[[buffer]]=]. + 1. Let |tail| be an empty [=byte sequence=]. + 1. If |writePosition| + |data|.[=byte sequence/length=] is smaller than |oldSize|: + 1. Let |tail| be a [=byte sequence=] containing the last + |oldSize| - (|writePosition| + |data|.[=byte sequence/length=]) bytes of |stream|.[=[[buffer]]=]. + 1. Set |stream|.[=[[buffer]]=] to the concatenation of |head|, |data| and |tail|. + 1. If the operations modifying |stream|.[=[[buffer]]=] in the previous steps failed + due to exceeding the [=storage quota=], [=/reject=] |p| with a {{QuotaExceededError}} and abort, + leaving |stream|.[=[[buffer]]=] unmodified. + + Note: [=Storage quota=] only applies to files stored in the [=origin private file system=]. + However this operation could still fail for other files, for example if the disk being written + to runs out of disk space. + 1. Set |stream|.[=[[seekOffset]]=] to |writePosition| + |data|.[=byte sequence/length=]. + 1. [=/Resolve=] |p|. + 1. Else if |command| is {{WriteCommandType/"seek"}}: + 1. If |chunk|.{{WriteParams/position}} is `undefined`, + [=/reject=] |p| with a {{TypeError}} and abort. + 1. Set |stream|.[=[[seekOffset]]=] to |chunk|.{{WriteParams/position}}. + 1. [=/Resolve=] |p|. + 1. Else if |command| is {{WriteCommandType/"truncate"}}: + 1. If |chunk|.{{WriteParams/size}} is `undefined`, + [=/reject=] |p| with a {{TypeError}} and abort. + 1. Let |newSize| be |chunk|.{{WriteParams/size}}. + 1. Let |oldSize| be |stream|.[=[[buffer]]=]'s [=byte sequence/length=]. + 1. If |newSize| is larger than |oldSize|: + 1. Set |stream|.[=[[buffer]]=] to a [=byte sequence=] formed by concating + |stream|.[=[[buffer]]=] with a [=byte sequence=] containing |newSize|-|oldSize| `0x00` bytes. + 1. If the operation in the previous step failed due to exceeding the [=storage quota=], + [=/reject=] |p| with a {{QuotaExceededError}} and abort, + leaving |stream|.[=[[buffer]]=] unmodified. + + Note: [=Storage quota=] only applies to files stored in the [=origin private file system=]. + However this operation could still fail for other files, for example if the disk being written + to runs out of disk space. + 1. Else if |newSize| is smaller than |oldSize|: + 1. Set |stream|.[=[[buffer]]=] to a [=byte sequence=] containing the first |newSize| bytes + in |stream|.[=[[buffer]]=]. + 1. If |stream|.[=[[seekOffset]]=] is bigger than |newSize|, + set |stream|.[=[[seekOffset]]=] to |newSize|. + 1. [=/Resolve=] |p|. +1. Return |p|. + +
+ +### The {{FileSystemWritableFileStream/write()}} method ### {#api-filesystemwritablefilestream-write} + +
+ : await |stream| . {{FileSystemWritableFileStream/write()|write}}(|data|) + : await |stream| . {{FileSystemWritableFileStream/write()|write}}({ + {{WriteParams/type}}: {{WriteCommandType/"write"}}, + {{WriteParams/data}}: |data| }) + :: 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| . {{FileSystemWritableFileStream/write()|write}}({ + {{WriteParams/type}}: {{WriteCommandType/"write"}}, + {{WriteParams/position}}: |position|, + {{WriteParams/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| . {{FileSystemWritableFileStream/write()|write}}({ + {{WriteParams/type}}: {{WriteCommandType/"seek"}}, + {{WriteParams/position}}: |position| }) + :: Updates the current file cursor offset the |position| bytes from the top of the file. + + : await |stream| . {{FileSystemWritableFileStream/write()|write}}({ + {{WriteParams/type}}: {{WriteCommandType/"truncate"}}, + {{WriteParams/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 {{truncate}} is 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. +
+ +
+The write(|data|) method, when invoked, must run +these steps: + +1. Let |writer| be the result of [=WritableStream/getting a writer=] for [=this=]. +1. Let |result| be the result of [=WritableStreamDefaultWriter/writing a chunk=] to |writer| given + |data|. +1. [=WritableStreamDefaultWriter/Release=] |writer|. +1. Return |result|. + +
+ +### The {{FileSystemWritableFileStream/seek()}} method ### {#api-filesystemwritablefilestream-seek} + +
+ : await |stream| . {{FileSystemWritableFileStream/seek()|seek}}(|position|) + :: Updates the current file cursor offset the |position| bytes from the top of the file. +
+ +
+The seek(|position|) method, when invoked, must run these +steps: + +1. Let |writer| be the result of [=WritableStream/getting a writer=] for [=this=]. +1. Let |result| be the result of [=WritableStreamDefaultWriter/writing a chunk=] to |writer| given + «[ "{{WriteParams/type}}" → {{WriteCommandType/"seek"}}, "{{WriteParams/position}}" → + |position| ]». +1. [=WritableStreamDefaultWriter/Release=] |writer|. +1. Return |result|. + +
+ +### The {{FileSystemWritableFileStream/truncate()}} method ### {#api-filesystemwritablefilestream-truncate} + +
+ : await |stream| . {{FileSystemWritableFileStream/truncate()|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 {{truncate}} is 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. +
+ +
+The truncate(|size|) method, when invoked, must run these +steps: + +1. Let |writer| be the result of [=WritableStream/getting a writer=] for [=this=]. +1. Let |result| be the result of [=WritableStreamDefaultWriter/writing a chunk=] to |writer| given + «[ "{{WriteParams/type}}" → {{WriteCommandType/"truncate"}}, "{{WriteParams/size}}" → + |size| ]». +1. [=WritableStreamDefaultWriter/Release=] |writer|. +1. Return |result|. + +
+ + +# Accessing the Origin Private File System # {#sandboxed-filesystem} + +The origin private file system is a [=storage endpoint=] whose +identifier is `"fileSystem"`, +types are `« "local" »`, +and quota is null. + +Issue: Storage endpoints should be defined in [[storage]] itself, rather +than being defined here. So merge this into the table there. + +Note: While user agents will typically implement this by persisting the contents of this +[=origin private file system=] 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 [=origin private file system=] exist. + + +[SecureContext] +partial interface StorageManager { + Promise<FileSystemDirectoryHandle> getDirectory(); +}; + + +
+ : |directoryHandle| = await navigator . storage . {{StorageManager/getDirectory()}} + :: Returns the root directory of the origin private file system. +
+ +
+The getDirectory() method, when +invoked, must run these steps: + +1. Let |environment| be the [=current settings object=]. + +1. Let |map| be the result of running [=obtain a local storage bottle map=] + with |environment| and `"fileSystem"`. If this returns failure, + return [=a promise rejected with=] a {{SecurityError}}. + +1. If |map|["root"] does not [=map/exist=]: + 1. Let |dir| be a new [=directory entry=] whose [=query access=] and [=request access=] algorithms + always return "{{PermissionState/granted}}". + 1. Set |dir|'s [=entry/name=] to the empty string. + 1. Set |dir|'s [=directory entry/children=] to an empty [=/set=]. + 1. Set |map|["root"] to |dir|. + +1. Return [=a promise resolved with=] a new {{FileSystemDirectoryHandle}}, + whose associated [=FileSystemHandle/entry=] is |map|["root"]. + +
+ + +

Acknowledgments

+ +

This standard is written by Marijn Kruisselbrink +(Google, mek@chromium.org). + + +

This Living Standard includes material copied from W3C WICG's +File System Access, which is +available under the +W3C Software and Document License.