From 570006366f6fbc52fc69eafd15cfac56c2cadc9c Mon Sep 17 00:00:00 2001 From: Anne van Kesteren Date: Mon, 19 Sep 2022 13:43:25 +0200 Subject: [PATCH 01/11] Review Draft Publication: September 2022 --- index.bs | 2 +- review-drafts/2022-09.bs | 989 +++++++++++++++++++++++++++++++++++++++ 2 files changed, 990 insertions(+), 1 deletion(-) create mode 100644 review-drafts/2022-09.bs 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. From 47855890ec489efc059e4f966cc8ee99c04abe36 Mon Sep 17 00:00:00 2001 From: Emanuel Krivoy Date: Thu, 29 Sep 2022 19:30:42 +0200 Subject: [PATCH 02/11] Add Access Handles to spec (#21) * Add Access Handle to spec, update explainer * Update explainer examples * Fix File System typo * Mark ReadWrite options as optional in explainer * Replace else with otherwise * Reply to remaining feedback * Add right errors to write/create * Fix read reasult bug * Remove copy from write * Update TODOs * Make read/write options optional * Update note on write() * Update write() note * Clarify WritableStream truncate/cursor interaction * disambiguate dfn links --- AccessHandle.md | 304 ---------------------------------------- index.bs | 363 +++++++++++++++++++++++++++++++++++++++++++++--- 2 files changed, 340 insertions(+), 327 deletions(-) delete mode 100644 AccessHandle.md diff --git a/AccessHandle.md b/AccessHandle.md deleted file mode 100644 index 01163f3..0000000 --- a/AccessHandle.md +++ /dev/null @@ -1,304 +0,0 @@ -# AccessHandle Proposal - -## Authors: - -* Emanuel Krivoy (fivedots@chromium.org) -* Richard Stotz (rstz@chromium.org) - -## Participate - -* [Issue tracker](https://github.com/WICG/file-system-access/issues) - -## Table of Contents - - - - -- [Introduction](#introduction) -- [Goals & Use Cases](#goals--use-cases) -- [Non-goals](#non-goals) -- [What makes the new surface fast?](#what-makes-the-new-surface-fast) -- [Proposed API](#proposed-api) - - [New data access surface](#new-data-access-surface) - - [Locking semantics](#locking-semantics) -- [Open Questions](#open-questions) - - [Naming](#naming) - - [Assurances on non-awaited consistency](#assurances-on-non-awaited-consistency) -- [Appendix](#appendix) - - [AccessHandle IDL](#accesshandle-idl) -- [References & acknowledgements](#references--acknowledgements) - - - -## Introduction - -We propose augmenting the Origin Private File System (OPFS) with a new surface -that brings very performant access to data. This new surface differs from -existing ones by offering in-place and exclusive write access to a file’s -content. This change, along with the ability to consistently read unflushed -modifications and the availability of a synchronous variant on dedicated -workers, significantly improves performance and unblocks new use cases for the -File System Access API. - -More concretely, we would add a *createAccessHandle()* method to the -*FileSystemFileHandle* object. It would return an *AccessHandle* that contains -a [duplex stream](https://streams.spec.whatwg.org/#other-specs-duplex) and -auxiliary methods. The readable/writable pair in the duplex stream communicates -with the same backing file, allowing the user to read unflushed contents. -Another new method, *createSyncAccessHandle()*, would only be exposed on Worker -threads. This method would offer a more buffer-based surface with synchronous -reading and writing. The creation of AccessHandle also creates a lock that -prevents write access to the file across (and within the same) execution -contexts. - -This proposal is part of our effort to integrate [Storage Foundation -API](https://github.com/WICG/storage-foundation-api-explainer) into File System -Access API. For more context the origins of this proposal, and alternatives -considered, please check out: [Merging Storage Foundation API and the Origin -Private File -System](https://docs.google.com/document/d/121OZpRk7bKSF7qU3kQLqAEUVSNxqREnE98malHYwWec), -[Recommendation for Augmented -OPFS](https://docs.google.com/document/d/1g7ZCqZ5NdiU7oqyCpsc2iZ7rRAY1ZXO-9VoG4LfP7fM). - -Although this proposal is the successor "in spirit" to the Storage Foundation -API, the two APIs operate on entirely different sets of files. There exists no -way of accessing a file stored through Storage Foundation API using the Origin -Private File System, and vice versa. - -## Goals & Use Cases - -Our goal is to give developers flexibility by providing generic, simple, and -performant primitives upon which they can build higher-level storage -components. The new surface is particularly well suited for Wasm-based -libraries and applications that want to use custom storage algorithms to -fine-tune execution speed and memory usage. - -A few examples of what could be done with *AccessHandles*: - -* Distribute a performant Wasm port of SQLite. This gives developers the - ability to use a persistent and fast SQL engine without having to rely on - the deprecated WebSQL API. -* Allow a music production website to operate on large amounts of media, by - relying on the new surface's performance and direct buffered access to - offload sound segments to disk instead of holding them in memory. -* Provide a fast and persistent [Emscripten](https://emscripten.org/) - filesystem to act as generic and easily accessible storage for Wasm. - -## Non-goals - -This proposal is focused only on additions to the [Origin Private File -System](https://wicg.github.io/file-system-access/#sandboxed-filesystem), and -doesn't currently consider changes to the rest of File System Access API or how -files in the host machine are accessed. - -This proposal does not consider accessing files stored using the Storage -Foundation API through OPFS or vice versa. - -## What makes the new surface fast? - -There are a few design choices that primarily contribute to the performance of -AccessHandles: - -* Write operations are not guaranteed to be immediately persistent, rather - persistency is achieved through calls to *flush()*. At the same time, data - can be consistently read before flushing. This allows applications to only - schedule time consuming flushes when they are required for long-term data - storage, and not as a precondition to operate on recently written data. -* The exclusive write lock held by the AccessHandle saves implementations - from having to provide a central data access point across execution - contexts. In multi-process browsers, such as Chrome, this helps avoid costly - inter-process communication (IPCs) between renderer and browser processes. -* Data copies are avoided when reading or writing. In the async surface this - is achieved through SharedArrayBuffers and BYOB readers. In the sync - surface, we rely on user-allocated buffers to hold the data. - -For more information on what affects the performance of similar storage APIs, -see [Design considerations for the Storage Foundation -API](https://docs.google.com/document/d/1cOdnvuNIWWyJHz1uu8K_9DEgntMtedxfCzShI7d01cs) - -## Proposed API - -### New data access surface - -```javascript -// In all contexts -// For details on the `mode` parameter see "Exposing AccessHandles on all -// filesystems" below -const handle = await file.createAccessHandle({ mode: "in-place" }); -await handle.writable.getWriter().write(buffer); -const reader = handle.readable.getReader({ mode: "byob" }); -// Assumes seekable streams, and SharedArrayBuffer support are available -await reader.read(buffer, { at: 1 }); - -// Only in a worker context -const handle = await file.createSyncAccessHandle(); -const writtenBytes = handle.write(buffer); -const readBytes = handle.read(buffer, { at: 1 }); -``` - -As mentioned above, a new *createAccessHandle()* method would be added to -*FileSystemFileHandle*. Another method, *createSyncAccessHandle()*, would be -only exposed on Worker threads. An IDL description of the new interface can be -found in the [Appendix](#appendix). - -The reason for offering a Worker-only synchronous interface, is that consuming -asynchronous APIs from Wasm has severe performance implications (more details -[here](https://docs.google.com/document/d/1lsQhTsfcVIeOW80dr467Auud_VCeAUv2ZOkC63oSyKo)). -Since this overhead is most impactful on methods that are called often, we've -only made *read()* and *write()* synchronous. This allows us to keep a simpler -mental model (where the sync and async handle are identical, except reading and -writing) and reduce the number of new sync methods, while avoiding the most -important perfomance penalties. - -This proposal assumes that [seekable -streams](https://github.com/whatwg/streams/issues/1128) will be available. If -this doesn’t happen, we can emulate the seeking behavior by extending the -default reader and writer with a *seek()* method. - -### Locking semantics - -```javascript -const handle1 = await file.createAccessHandle({ mode: "in-place" }); -try { - const handle2 = await file.createAccessHandle({ mode: "in-place" }); -} catch (e) { - // This catch will always be executed, since there is an open access handle -} -await handle1.close(); -// Now a new access handle may be created -``` - -*createAccessHandle()* would take an exclusive write lock on the file that -prevents the creation of any other access handles or *WritableFileStreams*. -Similarly *createWritable()* would take a shared write lock that blocks the -creation of access handles, but not of other writable streams. This prevents -the file from being modified from multiple contexts, while still being -backwards compatible with the current OPFS spec and supporting multiple -*WritableFileStreams* at once. - -Creating a [File](https://www.w3.org/TR/FileAPI/#dfn-file) through *getFile()* -would be possible when a lock is in place. The returned File behaves as it -currently does in OPFS i.e., it is invalidated if file contents are changed -after it was created. It is worth noting that these Files could be used to -observe changes done through the new API, even if a lock is still being held. - -## Open Questions - -### Naming - -The exact name of the new methods hasn’t been defined. The current placeholder -for data access is *createAccessHandle()* and *createSyncAccessHandle()*. -*createUnflushedStreams()* and *createDuplexStream()* have been suggested. - -### Exposing AccessHandles on all filesystems - -This proposal only currently considers additions to OPFS, but it would probably -be worthwhile to expand the new functionality to arbitrary file handles. While -the exact behavior of *AccessHandles* outside of OPFS would need to be defined -in detail, it's almost certain that the one described in this proposal should -not be the default. To avoid setting it as such, we propose adding an optional -*mode* string parameter to *createAccessHandle()* and -*createSyncAccessHandle()*. Some possible values *mode* could take are: - -* 'shared': The current behavior seen in File System Access API in general, - there is no locking and modifications are atomic (meaning that they would - only actually change the file when the *AccessHandle* is closed). This mode - would be a safe choice as a default value. -* 'exclusive': An exclusive write lock is taken on the file, but modifications - are still atomic. This is a useful mode for developers that want to - coordinate various writing threads but still want "all or nothing" writes. -* 'in-place': The behavior described in this proposal, allowing developers to - use high performance access to files at the cost of not having atomic writes. - It's possible that this mode would only be allowed in OPFS. - -Both the naming and semantics of the *mode* parameter have to be more concretely -defined. - -### Assurances on non-awaited consistency - -It would be possible to clearly specify the behavior of an immediate async read -operation after a non-awaited write operation, by serializing file operations -(as is currently done in Storage Foundation API). We should decide if this is -convenient, both from a specification and performance point of view. - -## Trying It Out - -A prototype of the synchronous surface (i.e., *createSyncAccessHandles()* and -the *FileSystemSyncAccessHandle* object) is available in Chrome. If you're -using version 95 or higher, you can enable it by launching Chrome with the -`--enable-blink-features=FileSystemAccessAccessHandle` flag or enabling -"Experimental Web Platform features" in "chrome://flags". If you're using -version 94, launch Chrome with the -`--enable-features=FileSystemAccessAccessHandle` flag. - -Sync access handles are available in an Origin Trial, starting with Chrome 95. -Sign up -[here](https://developer.chrome.com/origintrials/#/view_trial/3378825620434714625) -to participate. - -We have also developed an Emscripten file system based on access handles. -Instructions on how to use it can be found -[here](https://github.com/rstz/emscripten-pthreadfs/blob/main/pthreadfs/README.md). - -## Appendix - -### AccessHandle IDL - -```webidl -interface FileSystemFileHandle : FileSystemHandle { - Promise getFile(); - Promise createWritable(optional FileSystemCreateWritableOptions options = {}); - - Promise createAccessHandle(optional FileSystemFileHandleCreateAccessHandleOptions options = {}); - [Exposed=DedicatedWorker] - Promise createSyncAccessHandle(optional FileSystemFileHandleCreateAccessHandleOptions options = {}); -}; - -dictionary FileSystemFileHandleCreateAccessHandleOptions { - AccessHandleMode mode; -}; - -// For more details and possible modes, see "Exposing AccessHandles on all -// filesystems" above -enum AccessHandleMode { "in-place" }; - -interface FileSystemAccessHandle { - // Assumes seekable streams are available. The - // Seekable extended attribute is ad-hoc notation for this proposal. - [Seekable] readonly attribute WritableStream writable; - [Seekable] readonly attribute ReadableStream readable; - - // Resizes the file to be size bytes long. If size is larger than the current - // size the file is padded with null bytes, otherwise it is truncated. - Promise truncate([EnforceRange] unsigned long long size); - // Returns the current size of the file. - Promise getSize(); - // Persists the changes that have been written to disk - Promise flush(); - // Flushes and closes the streams, then releases the lock on the file - Promise close(); -}; - -[Exposed=DedicatedWorker] -interface FileSystemSyncAccessHandle { - unsigned long long read([AllowShared] BufferSource buffer, - FilesystemReadWriteOptions options); - unsigned long long write([AllowShared] BufferSource buffer, - FilesystemReadWriteOptions options); - - Promise truncate([EnforceRange] unsigned long long size); - Promise getSize(); - Promise flush(); - Promise close(); -}; - -dictionary FilesystemReadWriteOptions { - [EnforceRange] unsigned long long at; -}; -``` - -## References & acknowledgements - -Many thanks for valuable feedback and advice from: - -Domenic Denicola, Marijn Kruisselbrink, Victor Costan diff --git a/index.bs b/index.bs index df746f7..14e90ef 100644 --- a/index.bs +++ b/index.bs @@ -87,8 +87,48 @@ never be allowed using this API, rather than leaving it entirely up to underlyin 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). +binary data (a [=byte sequence=]), a +modification timestamp (a number representing the number of milliseconds since the Unix Epoch), +a lock (a string that may exclusively be "`open`", "`taken-exclusive`" or "`taken-shared`") +and a shared lock count (a number representing the number shared locks that are taken at a given point in time). + +

+To take a [=file entry/lock=] with a |value| of "`exclusive`" or "`shared`" on a given [=file entry=] |file|, +run the following steps: + +1. Let |lock| be the |file|'s [=file entry/lock=]. +1. Let |count| be the |file|'s [=file entry/shared lock count=]. +1. If |value| is "`exclusive`": + 1. If |lock| is "`open`": + 1. Set lock to "`taken-exclusive`". + 1. Return true. +1. If |value| is "`shared`": + 1. If |lock| is "`open`": + 1. Set |lock| to "`taken-shared`". + 1. Set |count| to 1. + 1. Return true. + 1. Otherwise, if |lock| is "`taken-shared`": + 1. Increase |count| by one. + 1. Return true. +1. Return false. + +
+ +
+To release a [=file entry/lock=] on a given [=file entry=] |file|, +run the following steps: + +1. Let |lock| be the |file|'s associated [=file entry/lock=]. +1. Let |count| be the |file|'s [=file entry/shared lock count=]. +1. If |lock| is "`taken-shared`": + 1. Decrease |count| by one. + 1. If |count| is 0, set |lock| to "`open`". +1. Otherwise, set |lock| to "`open`". + +
+ +Note: Locks help prevent concurrent modifications to a file. A {{FileSystemWritableFileStream}} +requires a shared lock, while a {{FileSystemSyncAccessHandle}} requires an exclusive one. A directory entry additionally consists of a [=/set=] of children, which are themselves [=/entries=]. Each member is either a [=/file=] or a [=directory=]. @@ -210,8 +250,8 @@ The isSameEntry(|other|) method, when inv 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`. + [=/resolve=] |p| with true. + 1. Otherwise [=/resolve=] |p| with false. 1. Return |p|. @@ -227,6 +267,8 @@ dictionary FileSystemCreateWritableOptions { interface FileSystemFileHandle : FileSystemHandle { Promise getFile(); Promise createWritable(optional FileSystemCreateWritableOptions options = {}); + [Exposed=DedicatedWorker] + Promise createSyncAccessHandle(); }; @@ -282,9 +324,13 @@ The getFile() method, when invoked, m 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, + 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. + + Creating a {{FileSystemWritableFileStream}} [=file entry/lock/take|takes a shared lock=] on the + [=FileSystemHandle/entry=] associated with |fileHandle|. This prevents the creation of + {{FileSystemSyncAccessHandle|FileSystemSyncAccessHandles}} for the entry, until the stream is closed. Issue(67): There has been some discussion around and desire for a "inPlace" mode for createWritable @@ -305,15 +351,60 @@ The createWritable(|options|) method, 1. If |access| is not "{{PermissionState/granted}}", reject |result| with a {{NotAllowedError}} and abort. 1. Let |entry| be [=this=]'s [=FileSystemHandle/entry=]. + 1. Let |lockResult| be the result of [=file entry/lock/take|taking a lock=] with "`shared`" on |entry|. + 1. If |lockResult| is false, [=reject=] |result| with a {{NoModificationAllowedError}} and abort. 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. 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 {{FileSystemFileHandle/createSyncAccessHandle()}} method ### {#api-filesystemfilehandle-createsyncaccesshandle} + +
+ : |handle| = await |fileHandle| . {{FileSystemFileHandle/createSyncAccessHandle()|createSyncAccessHandle}}() + :: Returns a {{FileSystemSyncAccessHandle}} that can be used to read from/write to the file. + Changes made through |handle| might be immediately reflected in the file represented by |fileHandle|. + To ensure the changes are reflected in this file, the handle can be flushed or closed. + + Creating a {{FileSystemSyncAccessHandle}} [=file entry/lock/take|takes an exclusive lock=] on the + [=FileSystemHandle/entry=] associated with |fileHandle|. This prevents the creation of + further {{FileSystemSyncAccessHandle}}s or {{FileSystemWritableFileStream}}s + for the entry, until the access handle is closed. + + The returned {{FileSystemSyncAccessHandle}} offers synchronous {{FileSystemSyncAccessHandle/read()}} and + {{FileSystemSyncAccessHandle/write()}} methods. This allows for higher performance for critical methods on + contexts where asynchronous operations come with high overhead, e.g., WebAssembly. + + For the time being, this method will only succeed when the |fileHandle| belongs to the + [=origin private file system=]. +
+ +
+The createSyncAccessHandle() 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. If |entry| does not represent an [=/entry=] in an [=origin private file system=], + reject |result| with an {{InvalidStateError}} and abort. + 1. Let |lockResult| be the result of [=file entry/lock/take|taking a lock=] with "`exclusive`" on |entry|. + 1. If |lockResult| is false, [=reject=] |result| with a {{NoModificationAllowedError}} and abort. + 1. Let |handle| be the result of [=create a new FileSystemSyncAccessHandle|creating a new FileSystemSyncAccessHandle=] + for |entry| in [=this=]'s [=relevant realm=]. + 1. [=/Resolve=] |result| with |handle|. +1. Return |result|. + +
+ ## The {{FileSystemDirectoryHandle}} interface ## {#api-filesystemdirectoryhandle} @@ -444,7 +535,7 @@ must run these steps: 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. 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. @@ -459,7 +550,7 @@ must run these steps: 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. 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|. @@ -506,7 +597,7 @@ invoked, must run these steps: 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. 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. @@ -521,7 +612,7 @@ invoked, must run these steps: 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. 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|. @@ -574,13 +665,13 @@ these steps: 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. 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 + 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. @@ -717,18 +808,23 @@ in a [=/Realm=] |realm|, perform the following steps: 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]]=]. + 1. Set |stream|.[=FileSystemWritableFileStream/[[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. [=file entry/lock/release|Release the lock=] on |stream|.[=FileSystemWritableFileStream/[[file]]=]. 1. [=/Resolve=] |closeResult| with `undefined`. 1. Return |closeResult|. +1. Let |abortAlgorithm| be the following step: + 1. [=file entry/lock/release|Release the lock=] on |stream|.[=FileSystemWritableFileStream/[[file]]=]. 1. Let |highWaterMark| be 1. 1. Let |sizeAlgorithm| be an algorithm that returns `1`. 1. [=WritableStream/Set up=] |stream| with <a for="WritableStream/set up"><var ignore>writeAlgorithm</var></a> set to |writeAlgorithm|, <a for="WritableStream/set up"><var ignore>closeAlgorithm</var></a> set to |closeAlgorithm|, <a for="WritableStream/set up"><var + ignore>abortAlgorithm</var></a> set to |abortAlgorithm|, <a for="WritableStream/set up"><var ignore>highWaterMark</var></a> set to |highWaterMark|, and <a for="WritableStream/set up"><var ignore>sizeAlgorithm</var></a> set to |sizeAlgorithm|. 1. Return |stream|. @@ -761,15 +857,15 @@ runs these steps: 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. Otherwise, if |data| is a {{Blob}}: 1. Let |dataBytes| be the result of performing the <a spec=FileAPI>read operation</a> on |data|. If this throws an exception, [=/reject=] |p| with that exception and abort. - 1. Else: + 1. Otherwise: 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]]=]. + 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 @@ -791,12 +887,12 @@ runs these steps: 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. Otherwise, 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. Otherwise, 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}}. @@ -811,7 +907,7 @@ runs these steps: 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. Otherwise, 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|, @@ -856,8 +952,8 @@ runs these steps: :: 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 + The file cursor is updated when {{truncate}} is called. If the cursor is smaller than |size|, + it remains unchanged. If the cursor is larger than |size|, it 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. @@ -903,8 +999,8 @@ steps: :: 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 + The file cursor is updated when {{truncate}} is called. If the cursor is smaller than |size|, + it remains unchanged. If the cursor is larger than |size|, it 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. @@ -924,6 +1020,227 @@ steps: </div> +## The {{FileSystemSyncAccessHandle}} interface ## {#api-filesystemsyncaccesshandle} + +<xmp class=idl> + +dictionary FileSystemReadWriteOptions { + [EnforceRange] required unsigned long long at = 0; +}; + +[Exposed=DedicatedWorker, SecureContext] +interface FileSystemSyncAccessHandle { + unsigned long long read([AllowShared] BufferSource buffer, + optional FileSystemReadWriteOptions options = {}); + unsigned long long write([AllowShared] BufferSource buffer, + optional FileSystemReadWriteOptions options = {}); + + Promise<undefined> truncate([EnforceRange] unsigned long long newSize); + Promise<unsigned long long> getSize(); + Promise<undefined> flush(); + Promise<undefined> close(); +}; + + + +A {{FileSystemSyncAccessHandle}} has an associated \[[file]] +(a [=file entry=]). + +A {{FileSystemSyncAccessHandle}} has an associated \[[state]], +a string that may exclusively be "`open`" or "`closed`". + +A {{FileSystemSyncAccessHandle}} is an object that is capable of reading from/writing to, +as well as obtaining and changing the size of, a single file. + +The {{FileSystemSyncAccessHandle/read()}} and {{FileSystemSyncAccessHandle/write()}} methods are synchronous. +This allows for higher performance for critical methods on contexts where asynchronous +operations come with high overhead, e.g., WebAssembly. + +
+To create a new FileSystemSyncAccessHandle given a [=file entry=] |file| +in a [=/Realm=] |realm|, perform the following steps: + +1. Let |handle| be a [=new=] {{FileSystemSyncAccessHandle}} in |realm|. +1. Set |handle|.[=FileSystemSyncAccessHandle/[[file]]=] to |file|. +1. Set |handle|.[=FileSystemSyncAccessHandle/[[state]]=] to "`open`". +1. Return |handle|. + +
+ +### The {{FileSystemSyncAccessHandle/read()}} method ### {#api-filesystemsyncaccesshandle-read} + +
+ : |handle| . {{FileSystemSyncAccessHandle/read()|read}}(|buffer|) + : |handle| . {{FileSystemSyncAccessHandle/read()|read}}(|buffer|, { {{FileSystemReadWriteOptions/at}} }) + :: Reads the contents of the file associated with |handle| into |buffer|, optionally at a given offset. +
+ +// TODO(fivedots): Specify how Access Handles should react when reading from a file that has been modified externally. +
+The read(|buffer|, {{FileSystemReadWriteOptions}}: |options|) method, when invoked, must run +these steps: + +1. If [=this=].[=[[state]]=] is "`closed`", throw an {{InvalidStateError}}. +1. Let |bufferSize| be |buffer|'s [=byte length=]. +1. Let |fileContents| be [=this=].[=FileSystemSyncAccessHandle/[[file]]=]'s [=file entry/binary data=]. +1. Let |fileSize| be |fileContents|'s [=byte sequence/length=]. +1. Let |readStart| be |options|.{{FileSystemReadWriteOptions/at}}. +1. If |readStart| is larger than |fileSize|, return 0. +1. Let |readEnd| be |readStart| + (|bufferSize| − 1). +1. If |readEnd| is larger than |fileSize|, set |readEnd| to |fileSize|. +1. Let |bytes| be a [=byte sequence=] containing the bytes from |readStart| to |readEnd| of |fileContents|. +1. Let |result| be |bytes|'s [=byte sequence/length=]. +1. If the operations reading from |fileContents| in the previous steps failed: + 1. If there were partial reads and the number of bytes that were read into |bytes| is known, + set |result| to the number of read bytes. + 1. Otherwise set |result| to 0. +1. Let |arrayBuffer| be |buffer|'s [=underlying buffer=]. +1. [=ArrayBuffer/write|Write=] |bytes| into |arrayBuffer|. +1. Return |result|. + +
+ +### The {{FileSystemSyncAccessHandle/write()}} method ### {#api-filesystemsyncaccesshandle-write} + +
+ : |handle| . {{FileSystemSyncAccessHandle/write()|write}}(|buffer|) + : |handle| . {{FileSystemSyncAccessHandle/write()|write}}(|buffer|, { {{FileSystemReadWriteOptions/at}} }) + :: Writes the content of |buffer| into the file associated with |handle|, optionally at a given offset, and returns the number of written bytes. + Checking the returned number of written bytes allows callers to detect and handle errors and partial writes. +
+ +// TODO(fivedots): Figure out how to properly check the available storage quota (in this method and others) by passing the right storage shelf. +
+The write(|buffer|, {{FileSystemReadWriteOptions}}: |options|) method, when invoked, must run +these steps: + +1. If [=this=].[=[[state]]=] is "`closed`", throw a {{InvalidStateError}}. +1. Let |writePosition| be |options|.{{FileSystemReadWriteOptions/at}}. +1. Let |fileContents| be a copy of [=this=].[=FileSystemSyncAccessHandle/[[file]]=]'s [=file entry/binary data=]. +1. Let |oldSize| be |fileContents|'s [=byte sequence/length=]. +1. Let |bufferSize| be |buffer|'s [=byte length=]. +1. If |writePosition| is larger than |oldSize|, + append |writePosition| − |oldSize| 0x00 (NUL) bytes to the end of |fileContents|. + + 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 |fileContents|. +1. Let |tail| be an empty [=byte sequence=]. +1. If |writePosition| + |bufferSize| is smaller than |oldSize|: + 1. Set |tail| to a [=byte sequence=] containing the last + |oldSize| − (|writePosition| + |bufferSize|) bytes of |fileContents|. +1. Let |newSize| be |head|'s [=byte sequence/length=] + |bufferSize| + |tail|'s [=byte sequence/length=]. +1. If |newSize| − |oldSize| exceeds the available [=storage quota=], throw a {{QuotaExceededError}}. +1. Set [=this=].[=FileSystemSyncAccessHandle/[[file]]=]'s [=file entry/binary data=] to the concatenation of |head|, the contents of |buffer| and |tail|. + + Note: The mechanism used to access buffer's contents is left purposely vague. + It is likely that implementations will choose to focus on performance by issuing + direct write calls to the host operating system (instead of creating a copy of buffer), + which prevents a detailed specification of the write order and the results of partial writes. + +1. If the operations modifying the [=this=].[=FileSystemSyncAccessHandle/[[file]]=]'s [=file entry/binary data=] in the previous steps + failed: + 1. If there were partial writes and the number of bytes that were written from |buffer| is known, + return the number of bytes that were written from |buffer|. + 1. Otherwise throw an {{InvalidStateError}}. +1. Return |bufferSize|. + +
+ +### The {{FileSystemSyncAccessHandle/truncate()}} method ### {#api-filesystemsyncaccesshandle-truncate} + +
+ : |handle| . {{FileSystemSyncAccessHandle/truncate()|truncate}}(|newSize|) + :: Resizes the file associated with stream to be |newSize| bytes long. If size is larger than the current file size this pads the file with null bytes; otherwise it truncates the file. +
+ +
+The truncate(|newSize|) method, when invoked, must run +these steps: + +1. If [=this=].[=[[state]]=] is "`closed`", return [=a promise rejected with=] an {{InvalidStateError}}. +1. Let |fileContents| be a copy of [=this=].[=FileSystemSyncAccessHandle/[[file]]=]'s [=file entry/binary data=]. +1. Let |p| be [=a new promise=] created in the [=relevant Realm=] of [=this=]. +1. Run the following steps [=in parallel=]: + 1. Let |oldSize| be the [=byte sequence/length=] of [=this=].[=FileSystemSyncAccessHandle/[[file]]=]'s [=file entry/binary data=]. + 1. If |newSize| is larger than |oldSize|: + 1. If |newSize| − |oldSize| exceeds the available [=storage quota=], [=/reject=] |p| + with a {{QuotaExceededError}} and abort. + 1. Set [=this=].[=FileSystemSyncAccessHandle/[[file]]=]'s to a [=byte sequence=] formed by concatenating + |fileContents| with a [=byte sequence=] + containing |newSize| − |oldSize| 0x00 bytes. + 1. If the operations modifying the [=this=].[=FileSystemSyncAccessHandle/[[file]]=]'s [=file entry/binary data=] in the previous steps + failed, [=/reject=] |p| with a {{InvalidStateError}} and abort. + 1. Otherwise, if |newSize| is smaller than |oldSize|: + 1. Set [=this=].[=FileSystemSyncAccessHandle/[[file]]=]'s to a [=byte sequence=] containing the first |newSize| bytes + in |fileContents|. + 1. If the operations modifying the [=this=].[=FileSystemSyncAccessHandle/[[file]]=]'s [=file entry/binary data=] in the previous steps + failed, [=/reject=] |p| with a {{InvalidStateError}} and abort. + 1. [=/Resolve=] |p|. +1. Return |p|. + +
+ +### The {{FileSystemSyncAccessHandle/getSize()}} method ### {#api-filesystemsyncaccesshandle-getsize} + +
+ : |handle| . {{FileSystemSyncAccessHandle/getSize()}} + :: Returns the size of the file associated with |handle| in bytes. +
+ +
+The getSize() method, when invoked, must run +these steps: + +1. If [=this=].[=[[state]]=] is "`closed`", return [=a promise rejected with=] an {{InvalidStateError}}. +1. Let |p| be [=a new promise=] created in the [=relevant Realm=] of [=this=]. +1. Run the following steps [=in parallel=]: + 1. Let |size| be the [=byte sequence/length=] of [=this=].[=FileSystemSyncAccessHandle/[[file]]=]'s [=file entry/binary data=]. + 1. [=/Resolve=] |p| with |size|. +1. Return |p|. + + +
+ +### The {{FileSystemSyncAccessHandle/flush()}} method ### {#api-filesystemsyncaccesshandle-flush} + +
+ : |handle| . {{FileSystemSyncAccessHandle/flush()}} + :: Ensures that the contents of the file associated with |handle| contain all the modifications done through {{FileSystemSyncAccessHandle/write()}}. +
+ +
+The flush() method, when invoked, must run +these steps: + +// TODO(fivedots): Fill in, after figuring out language to describe flushing at the OS level. + +
+ +### The {{FileSystemSyncAccessHandle/close()}} method ### {#api-filesystemsyncaccesshandle-close} + +
+ : |handle| . {{FileSystemSyncAccessHandle/close()}} + :: Flushes the access handle and then closes it. Closing an access handle disables any further operations on it and + [=file entry/lock/release|releases the lock=] on the [=FileSystemHandle/entry=] associated with |handle|. +
+ +//TODO(fivedots): Figure out language to describe flushing the file at the OS level before closing the handle. +
+The close() method, when invoked, must run +these steps: + +1. Let |p| be [=a new promise=] created in the [=relevant Realm=] of [=this=]. +1. Run the following steps [=in parallel=]: + 1. Set [=this=].[=[[state]]=] to "`closed`". + 1. [=/Resolve=] |p|. +1. Return |p|. + +
+ # Accessing the Origin Private File System # {#sandboxed-filesystem} From 862d4aadc632c2ba883ff021d721d7733e450745 Mon Sep 17 00:00:00 2001 From: Edgar Chen Date: Wed, 5 Oct 2022 18:32:33 +0200 Subject: [PATCH 03/11] Fix IDL type conversion reference (#58) Fixes #48. --- index.bs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/index.bs b/index.bs index 14e90ef..b82d69b 100644 --- a/index.bs +++ b/index.bs @@ -836,7 +836,7 @@ 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}}. +1. Let |input| be the result of [=converted to an IDL value|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=]: From 74a723f77ab8db683f2d774f0c35962b75402da6 Mon Sep 17 00:00:00 2001 From: Austin Sullivan Date: Tue, 11 Oct 2022 00:01:29 -0700 Subject: [PATCH 04/11] Make at dictionary member optional Fixes #56. --- index.bs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/index.bs b/index.bs index b82d69b..81e5e44 100644 --- a/index.bs +++ b/index.bs @@ -1025,7 +1025,7 @@ steps: dictionary FileSystemReadWriteOptions { - [EnforceRange] required unsigned long long at = 0; + [EnforceRange] unsigned long long at = 0; }; [Exposed=DedicatedWorker, SecureContext] From c10ba63630700a51da860879ba55e1495cd3d936 Mon Sep 17 00:00:00 2001 From: Anne van Kesteren <annevk@annevk.nl> Date: Mon, 17 Oct 2022 11:28:42 +0200 Subject: [PATCH 05/11] Meta: update repository files See https://github.com/whatwg/spec-factory for details. --- .github/workflows/build.yml | 6 +++--- .gitignore | 1 - Makefile | 6 +----- PULL_REQUEST_TEMPLATE.md | 7 ++++--- 4 files changed, 8 insertions(+), 12 deletions(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 29d0c65..445136a 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -11,13 +11,13 @@ jobs: name: Build runs-on: ubuntu-20.04 steps: - - uses: actions/checkout@v2 + - uses: actions/checkout@v3 with: fetch-depth: 2 # Note: `python` will also be this version, which various scripts depend on. - - uses: actions/setup-python@v2 + - uses: actions/setup-python@v3 with: - python-version: 3.8 + python-version: "3.10" # Note: `make deploy` will do a deploy dry run on PRs. - run: make deploy env: diff --git a/.gitignore b/.gitignore index ef9c01c..388676d 100644 --- a/.gitignore +++ b/.gitignore @@ -1,4 +1,3 @@ /fs.spec.whatwg.org/ /deploy.sh /index.html -/review.sh diff --git a/Makefile b/Makefile index 5ab728b..8adfd85 100644 --- a/Makefile +++ b/Makefile @@ -1,5 +1,5 @@ SHELL=/bin/bash -o pipefail -.PHONY: local remote deploy review +.PHONY: local remote deploy remote: index.bs @ (HTTP_STATUS=$$(curl https://api.csswg.org/bikeshed/ \ @@ -21,7 +21,3 @@ local: index.bs deploy: index.bs curl --remote-name --fail https://resources.whatwg.org/build/deploy.sh bash ./deploy.sh - -review: index.bs - curl --remote-name --fail https://resources.whatwg.org/build/review.sh - bash ./review.sh diff --git a/PULL_REQUEST_TEMPLATE.md b/PULL_REQUEST_TEMPLATE.md index 4a9c02e..543574a 100644 --- a/PULL_REQUEST_TEMPLATE.md +++ b/PULL_REQUEST_TEMPLATE.md @@ -8,8 +8,9 @@ Thank you for contributing to the File System Standard! Please describe the chan - [ ] [Tests](https://github.com/web-platform-tests/wpt) are written and can be reviewed and commented upon at: * … - [ ] [Implementation bugs](https://github.com/whatwg/meta/blob/main/MAINTAINERS.md#handling-pull-requests) are filed: - * Chrome: … - * Firefox: … - * Safari: … + * Chromium: … + * Gecko: … + * WebKit: … +- [ ] [MDN issue](https://github.com/whatwg/meta/blob/main/MAINTAINERS.md#handling-pull-requests) is filed: … (See [WHATWG Working Mode: Changes](https://whatwg.org/working-mode#changes) for more details.) From ad813af0d33d07397a7e919e3afcf1d8c15f4e85 Mon Sep 17 00:00:00 2001 From: Thomas Steiner <tomac@google.com> Date: Fri, 21 Oct 2022 18:29:41 +0200 Subject: [PATCH 06/11] Change `stream` to `handle` (#62) * Change `stream` to `handle` * Update index.bs --- index.bs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/index.bs b/index.bs index 81e5e44..d207688 100644 --- a/index.bs +++ b/index.bs @@ -1154,7 +1154,7 @@ these steps: <div class="note domintro"> : |handle| . {{FileSystemSyncAccessHandle/truncate()|truncate}}(|newSize|) - :: Resizes the file associated with stream to be |newSize| bytes long. If size is larger than the current file size this pads the file with null bytes; otherwise it truncates the file. + :: Resizes the file associated with |handle| to be |newSize| bytes long. If |newSize| is larger than the current file size this pads the file with null bytes; otherwise it truncates the file. </div> <div algorithm> From b345282a58e91213e1a4c7ed5ac4136d14a554c0 Mon Sep 17 00:00:00 2001 From: dslee414 <100442186+dslee414@users.noreply.github.com> Date: Wed, 26 Oct 2022 13:43:22 -0400 Subject: [PATCH 07/11] Update FileSystemSyncAccessHandle async methods to sync (#55) * Update index.bs Update FileSystemSyncAccessHandle methods all sync. * Update index.bs Change the return type to `undefined` from `void` * Fix typo for FileSystemSyncAccessHandle.file * Update index.bs Co-authored-by: Anne van Kesteren <annevk@annevk.nl> Co-authored-by: Austin Sullivan <asully@google.com> Co-authored-by: Anne van Kesteren <annevk@annevk.nl> --- index.bs | 67 ++++++++++++++++++++++---------------------------------- 1 file changed, 26 insertions(+), 41 deletions(-) diff --git a/index.bs b/index.bs index d207688..19b44cb 100644 --- a/index.bs +++ b/index.bs @@ -375,9 +375,8 @@ The <dfn method for=FileSystemFileHandle>createWritable(|options|)</dfn> method, further {{FileSystemSyncAccessHandle}}s or {{FileSystemWritableFileStream}}s for the entry, until the access handle is closed. - The returned {{FileSystemSyncAccessHandle}} offers synchronous {{FileSystemSyncAccessHandle/read()}} and - {{FileSystemSyncAccessHandle/write()}} methods. This allows for higher performance for critical methods on - contexts where asynchronous operations come with high overhead, e.g., WebAssembly. + The returned {{FileSystemSyncAccessHandle}} offers synchronous methods. This allows for higher performance + on contexts where asynchronous operations come with high overhead, e.g., WebAssembly. For the time being, this method will only succeed when the |fileHandle| belongs to the [=origin private file system=]. @@ -1035,10 +1034,10 @@ interface FileSystemSyncAccessHandle { unsigned long long write([AllowShared] BufferSource buffer, optional FileSystemReadWriteOptions options = {}); - Promise<undefined> truncate([EnforceRange] unsigned long long newSize); - Promise<unsigned long long> getSize(); - Promise<undefined> flush(); - Promise<undefined> close(); + undefined truncate([EnforceRange] unsigned long long newSize); + unsigned long long getSize(); + undefined flush(); + undefined close(); }; @@ -1052,9 +1051,8 @@ a string that may exclusively be "`open`" or "`closed`". A {{FileSystemSyncAccessHandle}} is an object that is capable of reading from/writing to, as well as obtaining and changing the size of, a single file. -The {{FileSystemSyncAccessHandle/read()}} and {{FileSystemSyncAccessHandle/write()}} methods are synchronous. -This allows for higher performance for critical methods on contexts where asynchronous -operations come with high overhead, e.g., WebAssembly. +A {{FileSystemSyncAccessHandle}} offers synchronous methods. This allows for higher performance on +contexts where asynchronous operations come with high overhead, e.g., WebAssembly.
To create a new FileSystemSyncAccessHandle given a [=file entry=] |file| @@ -1114,7 +1112,7 @@ these steps: The write(|buffer|, {{FileSystemReadWriteOptions}}: |options|) method, when invoked, must run these steps: -1. If [=this=].[=[[state]]=] is "`closed`", throw a {{InvalidStateError}}. +1. If [=this=].[=[[state]]=] is "`closed`", throw an {{InvalidStateError}}. 1. Let |writePosition| be |options|.{{FileSystemReadWriteOptions/at}}. 1. Let |fileContents| be a copy of [=this=].[=FileSystemSyncAccessHandle/[[file]]=]'s [=file entry/binary data=]. 1. Let |oldSize| be |fileContents|'s [=byte sequence/length=]. @@ -1161,26 +1159,21 @@ these steps: The truncate(|newSize|) method, when invoked, must run these steps: -1. If [=this=].[=[[state]]=] is "`closed`", return [=a promise rejected with=] an {{InvalidStateError}}. +1. If [=this=].[=[[state]]=] is "`closed`", throw an {{InvalidStateError}}. 1. Let |fileContents| be a copy of [=this=].[=FileSystemSyncAccessHandle/[[file]]=]'s [=file entry/binary data=]. -1. Let |p| be [=a new promise=] created in the [=relevant Realm=] of [=this=]. -1. Run the following steps [=in parallel=]: - 1. Let |oldSize| be the [=byte sequence/length=] of [=this=].[=FileSystemSyncAccessHandle/[[file]]=]'s [=file entry/binary data=]. - 1. If |newSize| is larger than |oldSize|: - 1. If |newSize| − |oldSize| exceeds the available [=storage quota=], [=/reject=] |p| - with a {{QuotaExceededError}} and abort. - 1. Set [=this=].[=FileSystemSyncAccessHandle/[[file]]=]'s to a [=byte sequence=] formed by concatenating - |fileContents| with a [=byte sequence=] - containing |newSize| − |oldSize| 0x00 bytes. - 1. If the operations modifying the [=this=].[=FileSystemSyncAccessHandle/[[file]]=]'s [=file entry/binary data=] in the previous steps - failed, [=/reject=] |p| with a {{InvalidStateError}} and abort. - 1. Otherwise, if |newSize| is smaller than |oldSize|: - 1. Set [=this=].[=FileSystemSyncAccessHandle/[[file]]=]'s to a [=byte sequence=] containing the first |newSize| bytes - in |fileContents|. - 1. If the operations modifying the [=this=].[=FileSystemSyncAccessHandle/[[file]]=]'s [=file entry/binary data=] in the previous steps - failed, [=/reject=] |p| with a {{InvalidStateError}} and abort. - 1. [=/Resolve=] |p|. -1. Return |p|. +1. 1. Let |oldSize| be the [=byte sequence/length=] of [=this=].[=FileSystemSyncAccessHandle/[[file]]=]'s [=file entry/binary data=]. +1. If |newSize| is larger than |oldSize|: + 1. If |newSize| − |oldSize| exceeds the available [=storage quota=], throw a {{QuotaExceededError}}. + 1. Set [=this=].[=FileSystemSyncAccessHandle/[[file]]=]'s to a [=byte sequence=] formed by concatenating + |fileContents| with a [=byte sequence=] + containing |newSize| − |oldSize| 0x00 bytes. + 1. If the operations modifying the [=this=].[=FileSystemSyncAccessHandle/[[file]]=]'s [=file entry/binary data=] in the previous steps + failed, throw an {{InvalidStateError}}. +1. Otherwise, if |newSize| is smaller than |oldSize|: + 1. Set [=this=].[=FileSystemSyncAccessHandle/[[file]]=]'s to a [=byte sequence=] containing the first |newSize| bytes + in |fileContents|. + 1. If the operations modifying the [=this=].[=FileSystemSyncAccessHandle/[[file]]=]'s [=file entry/binary data=] in the previous steps + failed, throw an {{InvalidStateError}}.
@@ -1195,12 +1188,8 @@ these steps: The getSize() method, when invoked, must run these steps: -1. If [=this=].[=[[state]]=] is "`closed`", return [=a promise rejected with=] an {{InvalidStateError}}. -1. Let |p| be [=a new promise=] created in the [=relevant Realm=] of [=this=]. -1. Run the following steps [=in parallel=]: - 1. Let |size| be the [=byte sequence/length=] of [=this=].[=FileSystemSyncAccessHandle/[[file]]=]'s [=file entry/binary data=]. - 1. [=/Resolve=] |p| with |size|. -1. Return |p|. +1. If [=this=].[=[[state]]=] is "`closed`", throw an {{InvalidStateError}}. +1. Return [=this=].[=FileSystemSyncAccessHandle/[[file]]=]'s [=file entry/binary data=]'s [=byte sequence/length=]. @@ -1233,11 +1222,7 @@ these steps: The close() method, when invoked, must run these steps: -1. Let |p| be [=a new promise=] created in the [=relevant Realm=] of [=this=]. -1. Run the following steps [=in parallel=]: - 1. Set [=this=].[=[[state]]=] to "`closed`". - 1. [=/Resolve=] |p|. -1. Return |p|. +1. Set [=this=].[=[[state]]=] to "`closed`". From d64f79905e2c5ceff4aebcebacd67e51d9a4b16c Mon Sep 17 00:00:00 2001 From: Austin Sullivan Date: Thu, 27 Oct 2022 09:34:00 -0700 Subject: [PATCH 08/11] Editorial: modernize method definitions See #64. --- index.bs | 58 ++++++++++++++++++++------------------------------------ 1 file changed, 21 insertions(+), 37 deletions(-) diff --git a/index.bs b/index.bs index 19b44cb..07dd1e1 100644 --- a/index.bs +++ b/index.bs @@ -244,7 +244,7 @@ associated [=FileSystemHandle/entry=].
-The isSameEntry(|other|) method, when invoked, must run these steps: +The isSameEntry(|other|) method steps are: 1. Let |realm| be [=this=]'s [=relevant Realm=]. 1. Let |p| be [=a new promise=] in |realm|. @@ -287,7 +287,7 @@ A {{FileSystemFileHandle}}'s associated [=FileSystemHandle/entry=] must be a [=f
-The getFile() method, when invoked, must run these steps: +The getFile() method steps are: 1. Let |result| be [=a new promise=]. 1. Run the following steps [=in parallel=]: @@ -341,7 +341,7 @@ combine the desire to run malware checks with the desire to let websites make fa modifications to existing large files.
-The createWritable(|options|) method, when invoked, must run these steps: +The createWritable(|options|) method steps are: 1. Let |result| be [=a new promise=]. 1. Run the following steps [=in parallel=]: @@ -383,7 +383,7 @@ The createWritable(|options|) method,
-The createSyncAccessHandle() method, when invoked, must run these steps: +The createSyncAccessHandle() method steps are: 1. Let |result| be [=a new promise=]. 1. Run the following steps [=in parallel=]: @@ -526,8 +526,7 @@ and its async iterator |iterator|:
-The getFileHandle(|name|, |options|) method, when invoked, -must run these steps: +The getFileHandle(|name|, |options|) method steps are: 1. Let |result| be [=a new promise=]. 1. Run the following steps [=in parallel=]: @@ -588,8 +587,7 @@ must run these steps:
-The getDirectoryHandle(|name|, |options|) method, when -invoked, must run these steps: +The getDirectoryHandle(|name|, |options|) method steps are: 1. Let |result| be [=a new promise=]. 1. Run the following steps [=in parallel=]: @@ -647,8 +645,7 @@ invoked, must run these steps:
-The removeEntry(|name|, |options|) method, when invoked, must run -these steps: +The removeEntry(|name|, |options|) method steps are: 1. Let |result| be [=a new promise=]. 1. Run the following steps [=in parallel=]: @@ -727,14 +724,12 @@ if (relative_path === null) {
-The resolve(|possibleDescendant|) method, -when invoked, must return the result of [=entry/resolving=] -|possibleDescendant|'s [=FileSystemHandle/entry=] relative to [=this=]'s [=FileSystemHandle/entry=]. +The resolve(|possibleDescendant|) method steps are +to return the result of [=entry/resolving=]|possibleDescendant|'s [=FileSystemHandle/entry=] +relative to [=this=]'s [=FileSystemHandle/entry=].
- - ## The {{FileSystemWritableFileStream}} interface ## {#api-filesystemwritablefilestream} @@ -960,8 +955,7 @@ runs these steps: </div> <div algorithm> -The <dfn method for=FileSystemWritableFileStream>write(|data|)</dfn> method, when invoked, must run -these steps: +The <dfn method for=FileSystemWritableFileStream>write(|data|)</dfn> method steps are: 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 @@ -979,8 +973,7 @@ these steps: </div> <div algorithm> -The <dfn method for=FileSystemWritableFileStream>seek(|position|)</dfn> method, when invoked, must run these -steps: +The <dfn method for=FileSystemWritableFileStream>seek(|position|)</dfn> method steps are: 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 @@ -1007,8 +1000,7 @@ steps: </div> <div algorithm> -The <dfn method for=FileSystemWritableFileStream>truncate(|size|)</dfn> method, when invoked, must run these -steps: +The <dfn method for=FileSystemWritableFileStream>truncate(|size|)</dfn> method steps are: 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 @@ -1075,8 +1067,7 @@ in a [=/Realm=] |realm|, perform the following steps: // TODO(fivedots): Specify how Access Handles should react when reading from a file that has been modified externally. <div algorithm> -The <dfn method for=FileSystemSyncAccessHandle>read(|buffer|, {{FileSystemReadWriteOptions}}: |options|)</dfn> method, when invoked, must run -these steps: +The <dfn method for=FileSystemSyncAccessHandle>read(|buffer|, {{FileSystemReadWriteOptions}}: |options|)</dfn> method steps are: 1. If [=this=].[=[[state]]=] is "`closed`", throw an {{InvalidStateError}}. 1. Let |bufferSize| be |buffer|'s [=byte length=]. @@ -1109,8 +1100,7 @@ these steps: // TODO(fivedots): Figure out how to properly check the available storage quota (in this method and others) by passing the right storage shelf. <div algorithm> -The <dfn method for=FileSystemSyncAccessHandle>write(|buffer|, {{FileSystemReadWriteOptions}}: |options|)</dfn> method, when invoked, must run -these steps: +The <dfn method for=FileSystemSyncAccessHandle>write(|buffer|, {{FileSystemReadWriteOptions}}: |options|)</dfn> method steps are: 1. If [=this=].[=[[state]]=] is "`closed`", throw an {{InvalidStateError}}. 1. Let |writePosition| be |options|.{{FileSystemReadWriteOptions/at}}. @@ -1156,8 +1146,7 @@ these steps: </div> <div algorithm> -The <dfn method for=FileSystemSyncAccessHandle>truncate(|newSize|)</dfn> method, when invoked, must run -these steps: +The <dfn method for=FileSystemSyncAccessHandle>truncate(|newSize|)</dfn> method steps are: 1. If [=this=].[=[[state]]=] is "`closed`", throw an {{InvalidStateError}}. 1. Let |fileContents| be a copy of [=this=].[=FileSystemSyncAccessHandle/[[file]]=]'s [=file entry/binary data=]. @@ -1185,8 +1174,7 @@ these steps: </div> <div algorithm> -The <dfn method for=FileSystemSyncAccessHandle>getSize()</dfn> method, when invoked, must run -these steps: +The <dfn method for=FileSystemSyncAccessHandle>getSize()</dfn> method steps are: 1. If [=this=].[=[[state]]=] is "`closed`", throw an {{InvalidStateError}}. 1. Return [=this=].[=FileSystemSyncAccessHandle/[[file]]=]'s [=file entry/binary data=]'s [=byte sequence/length=]. @@ -1202,8 +1190,7 @@ these steps: </div> <div algorithm> -The <dfn method for=FileSystemSyncAccessHandle>flush()</dfn> method, when invoked, must run -these steps: +The <dfn method for=FileSystemSyncAccessHandle>flush()</dfn> method steps are: // TODO(fivedots): Fill in, after figuring out language to describe flushing at the OS level. @@ -1219,10 +1206,8 @@ these steps: //TODO(fivedots): Figure out language to describe flushing the file at the OS level before closing the handle. <div algorithm> -The <dfn method for=FileSystemSyncAccessHandle>close()</dfn> method, when invoked, must run -these steps: - -1. Set [=this=].[=[[state]]=] to "`closed`". +The <dfn method for=FileSystemSyncAccessHandle>close()</dfn> method steps are +to set [=this=].[=[[state]]=] to "`closed`". </div> @@ -1255,8 +1240,7 @@ partial interface StorageManager { </div> <div algorithm> -The <dfn method for=StorageManager>getDirectory()</dfn> method, when -invoked, must run these steps: +The <dfn method for=StorageManager>getDirectory()</dfn> method steps are: 1. Let |environment| be the [=current settings object=]. From 114593ee804806e73131777f201d00ed5050052a Mon Sep 17 00:00:00 2001 From: Austin Sullivan <asully@google.com> Date: Thu, 27 Oct 2022 10:26:29 -0700 Subject: [PATCH 09/11] Editorial: fix typo in async iterator --- index.bs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/index.bs b/index.bs index 07dd1e1..c655536 100644 --- a/index.bs +++ b/index.bs @@ -454,7 +454,7 @@ support for example recursive iteration. <div algorithm="iterator initialization"> The [=asynchronous iterator initialization steps=] for a {{FileSystemDirectoryHandle}} |handle| -ant its async iterator |iterator| are: +and its async iterator |iterator| are: 1. Let |access| be the result of running |handle|'s [=FileSystemHandle/entry=]'s [=entry/query access=] given "`read`". From 38b0517443d90ad8ad8cfd4b10f447fb0d30c1da Mon Sep 17 00:00:00 2001 From: Austin Sullivan <asully@google.com> Date: Sun, 30 Oct 2022 01:34:57 -0700 Subject: [PATCH 10/11] Editorial: modernize getters and setters Fixes #64. Co-authored-by: Anne van Kesteren <annevk@annevk.nl> --- index.bs | 15 +++++++-------- 1 file changed, 7 insertions(+), 8 deletions(-) diff --git a/index.bs b/index.bs index c655536..13c02a8 100644 --- a/index.bs +++ b/index.bs @@ -229,12 +229,11 @@ Their [=deserialization steps=], given |serialized| and |value| are: :: Returns the [=entry/name=] of the entry represented by |handle|. </div> -The <dfn attribute for=FileSystemHandle>kind</dfn> attribute must -return {{FileSystemHandleKind/"file"}} if the associated [=FileSystemHandle/entry=] is a [=file entry=], -and return {{FileSystemHandleKind/"directory"}} otherwise. +The <dfn attribute for=FileSystemHandle>kind</dfn> getter steps are to return +{{FileSystemHandleKind/"file"}} if [=this=] is a [=file entry=]; otherwise +{{FileSystemHandleKind/"directory"}}. -The <dfn attribute for=FileSystemHandle>name</dfn> attribute must return the [=entry/name=] of the -associated [=FileSystemHandle/entry=]. +The <dfn attribute for=FileSystemHandle>name</dfn> getter steps are to return [=this=]'s [=entry/name=]. ### The {{FileSystemHandle/isSameEntry()}} method ### {#api-filesystemhandle-issameentry} @@ -299,9 +298,9 @@ The <dfn method for=FileSystemFileHandle>getFile()</dfn> method steps are: 1. Let |f| be a new {{File}}. 1. Set |f|'s <a spec=FileAPI>snapshot state</a> 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. + 1. Set |f|.{{File/name}} to |entry|'s [=entry/name=]. + 1. Set |f|.{{File/lastModified}} to |entry|'s [=file entry/modification timestamp=]. + 1. Set |f|.{{Blob/type}} 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. From abad896095edb53a0479e345ca825607f1d30768 Mon Sep 17 00:00:00 2001 From: Austin Sullivan <asully@google.com> Date: Tue, 1 Nov 2022 03:19:30 -0700 Subject: [PATCH 11/11] Editorial: modernize algorithm steps language Related to #64. --- index.bs | 41 ++++++++++++++++++++--------------------- 1 file changed, 20 insertions(+), 21 deletions(-) diff --git a/index.bs b/index.bs index 13c02a8..043fec2 100644 --- a/index.bs +++ b/index.bs @@ -93,8 +93,7 @@ a <dfn for="file entry">lock</dfn> (a string that may exclusively be "`open`", " and a <dfn for="file entry">shared lock count</dfn> (a number representing the number shared locks that are taken at a given point in time). <div algorithm> -To <dfn for="file entry/lock">take</dfn> a [=file entry/lock=] with a |value| of "`exclusive`" or "`shared`" on a given [=file entry=] |file|, -run the following steps: +To <dfn for="file entry/lock">take</dfn> a [=file entry/lock=] with a |value| of "`exclusive`" or "`shared`" on a given [=file entry=] |file|: 1. Let |lock| be the |file|'s [=file entry/lock=]. 1. Let |count| be the |file|'s [=file entry/shared lock count=]. @@ -116,7 +115,7 @@ run the following steps: <div algorithm> To <dfn for="file entry/lock">release</dfn> a [=file entry/lock=] on a given [=file entry=] |file|, -run the following steps: +run these steps: 1. Let |lock| be the |file|'s associated [=file entry/lock=]. 1. Let |count| be the |file|'s [=file entry/shared lock count=]. @@ -156,10 +155,10 @@ directory on disk but an entry doesn't have to map to any file on disk). <div algorithm> To <dfn for="entry">resolve</dfn> an [=/entry=] |child| relative to a [=directory entry=] |root|, -run the following steps: +run these steps: 1. Let |result| be [=a new promise=]. -1. Run the following steps [=in parallel=]: +1. Run these steps [=in parallel=]: 1. If |child| is [=the same as=] |root|, [=/resolve=] |result| with an empty list, and abort. 1. Let |childPromises| be « ». @@ -170,7 +169,7 @@ run the following steps: 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. [=Wait for all=] |childPromises|, with the these success steps: 1. If |result| hasn't been resolved yet, [=/resolve=] |result| with `null`. 1. Return |result|. @@ -247,7 +246,7 @@ The <dfn method for=FileSystemHandle>isSameEntry(|other|)</dfn> method steps are 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. Run these steps [=in parallel=]: 1. If [=this=]'s [=FileSystemHandle/entry=] is [=the same as=] |other|'s [=FileSystemHandle/entry=], [=/resolve=] |p| with true. 1. Otherwise [=/resolve=] |p| with false. @@ -289,7 +288,7 @@ A {{FileSystemFileHandle}}'s associated [=FileSystemHandle/entry=] must be a [=f The <dfn method for=FileSystemFileHandle>getFile()</dfn> method steps are: 1. Let |result| be [=a new promise=]. -1. Run the following steps [=in parallel=]: +1. Run these 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}}", @@ -343,7 +342,7 @@ modifications to existing large files. The <dfn method for=FileSystemFileHandle>createWritable(|options|)</dfn> method steps are: 1. Let |result| be [=a new promise=]. -1. Run the following steps [=in parallel=]: +1. Run these 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. @@ -385,7 +384,7 @@ The <dfn method for=FileSystemFileHandle>createWritable(|options|)</dfn> method The <dfn method for=FileSystemFileHandle>createSyncAccessHandle()</dfn> method steps are: 1. Let |result| be [=a new promise=]. -1. Run the following steps [=in parallel=]: +1. Run these 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. @@ -528,7 +527,7 @@ and its async iterator |iterator|: The <dfn method for=FileSystemDirectoryHandle>getFileHandle(|name|, |options|)</dfn> method steps are: 1. Let |result| be [=a new promise=]. -1. Run the following steps [=in parallel=]: +1. Run these 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=]. @@ -589,7 +588,7 @@ The <dfn method for=FileSystemDirectoryHandle>getFileHandle(|name|, |options|)</ The <dfn method for=FileSystemDirectoryHandle>getDirectoryHandle(|name|, |options|)</dfn> method steps are: 1. Let |result| be [=a new promise=]. -1. Run the following steps [=in parallel=]: +1. Run these 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=]. @@ -647,7 +646,7 @@ The <dfn method for=FileSystemDirectoryHandle>getDirectoryHandle(|name|, |option The <dfn method for=FileSystemDirectoryHandle>removeEntry(|name|, |options|)</dfn> method steps are: 1. Let |result| be [=a new promise=]. -1. Run the following steps [=in parallel=]: +1. Run these 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=]. @@ -787,19 +786,19 @@ Similarly, when piping a {{ReadableStream}} into a {{FileSystemWritableFileStrea <div algorithm> To <dfn>create a new FileSystemWritableFileStream</dfn> given a [=file entry=] |file| -in a [=/Realm=] |realm|, perform the following steps: +in a [=/Realm=] |realm|, run these 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 |closeAlgorithm| be these steps: 1. Let |closeResult| be [=a new promise=]. - 1. Run the following steps [=in parallel=]: + 1. Run these 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. + 1. Run [=implementation-defined=] malware scans and safe browsing checks. If these checks fail, [=/reject=] |closeResult| with an {{AbortError}} and abort. 1. Set |stream|.[=FileSystemWritableFileStream/[[file]]=]'s [=file entry/binary data=] to |stream|.[=[[buffer]]=]. If that throws an exception, [=/reject=] |closeResult| with that exception and abort. @@ -810,8 +809,8 @@ in a [=/Realm=] |realm|, perform the following steps: 1. [=file entry/lock/release|Release the lock=] on |stream|.[=FileSystemWritableFileStream/[[file]]=]. 1. [=/Resolve=] |closeResult| with `undefined`. 1. Return |closeResult|. -1. Let |abortAlgorithm| be the following step: - 1. [=file entry/lock/release|Release the lock=] on |stream|.[=FileSystemWritableFileStream/[[file]]=]. +1. Let |abortAlgorithm| be this step: [=file entry/lock/release|release the lock=] on + |stream|.[=FileSystemWritableFileStream/[[file]]=]. 1. Let |highWaterMark| be 1. 1. Let |sizeAlgorithm| be an algorithm that returns `1`. 1. [=WritableStream/Set up=] |stream| with <a for="WritableStream/set up"><var @@ -832,7 +831,7 @@ runs these steps: 1. Let |input| be the result of [=converted to an IDL value|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. Run these 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}}", @@ -1047,7 +1046,7 @@ contexts where asynchronous operations come with high overhead, e.g., WebAssembl <div algorithm> To <dfn>create a new FileSystemSyncAccessHandle</dfn> given a [=file entry=] |file| -in a [=/Realm=] |realm|, perform the following steps: +in a [=/Realm=] |realm|, run these steps: 1. Let |handle| be a [=new=] {{FileSystemSyncAccessHandle}} in |realm|. 1. Set |handle|.[=FileSystemSyncAccessHandle/[[file]]=] to |file|.