From e4e3c3cae0f58a68fd38d7eefcbdd96a124eb4e9 Mon Sep 17 00:00:00 2001 From: Nathan Memmott Date: Mon, 13 Nov 2023 13:57:11 -0700 Subject: [PATCH 1/4] Make "take a lock" a member of "file entry" --- index.bs | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/index.bs b/index.bs index 881fb19..43da40f 100644 --- a/index.bs +++ b/index.bs @@ -128,14 +128,14 @@ A file entry additionally consists of 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). +and a shared lock count (a number representing the number of shared locks that are taken at a given point in time). A user agent has an associated file system queue which is the result of [=starting a new parallel queue=]. This queue is to be used for all file system operations.
-To take a [=file entry/lock=] with a |value| of +To take a lock with a |value| of "`exclusive`" or "`shared`" on a given [=file entry=] |file|: 1. Let |lock| be the |file|'s [=file entry/lock=]. @@ -538,7 +538,7 @@ The getFile() method steps are: 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 + Creating a {{FileSystemWritableFileStream}} [=file entry/take a lock|takes a shared lock=] on the [=file entry=] [=locate an entry|locatable=] with |fileHandle|'s [=FileSystemHandle/locator=]. This prevents the creation of {{FileSystemSyncAccessHandle|FileSystemSyncAccessHandles}} for the entry, until the stream is closed. @@ -575,7 +575,7 @@ The createWritable(|options|) method |result| with a "{{NotFoundError}}" {{DOMException}} and abort these steps. 1. [=Assert=]: |entry| is a [=file entry=]. - 1. Let |lockResult| be the result of [=file entry/lock/take|taking a lock=] + 1. Let |lockResult| be the result of [=file entry/take a lock|taking a lock=] with "`shared`" on |entry|. 1. [=Queue a storage task=] with |global| to run these steps: @@ -603,7 +603,7 @@ The createWritable(|options|) method [=file entry=] [=locate an entry|locatable=] by |fileHandle|'s [=FileSystemHandle/locator=]. To ensure the changes are reflected in this file, the handle can be flushed. - Creating a {{FileSystemSyncAccessHandle}} [=file entry/lock/take|takes an exclusive lock=] on the + Creating a {{FileSystemSyncAccessHandle}} [=file entry/take a lock|takes an exclusive lock=] on the [=file entry=] [=locate an entry|locatable=] with |fileHandle|'s [=FileSystemHandle/locator=]. This prevents the creation of further {{FileSystemSyncAccessHandle|FileSystemSyncAccessHandles}} or {{FileSystemWritableFileStream|FileSystemWritableFileStreams}} @@ -645,7 +645,7 @@ The createSyncAccessHandle() method s |result| with a "{{NotFoundError}}" {{DOMException}} and abort these steps. 1. [=Assert=]: |entry| is a [=file entry=]. - 1. Let |lockResult| be the result of [=file entry/lock/take|taking a lock=] + 1. Let |lockResult| be the result of [=file entry/take a lock|taking a lock=] with "`exclusive`" on |entry|. 1. [=Queue a storage task=] with |global| to run these steps: From f56615cbf06ecb7c4d692232ef1023a6a396de16 Mon Sep 17 00:00:00 2001 From: Nathan Memmott Date: Tue, 19 Dec 2023 15:29:53 -0700 Subject: [PATCH 2/4] Simplify take lock and release lock. --- index.bs | 38 ++++++++++++++++++-------------------- 1 file changed, 18 insertions(+), 20 deletions(-) diff --git a/index.bs b/index.bs index 43da40f..6ea488a 100644 --- a/index.bs +++ b/index.bs @@ -128,7 +128,7 @@ A file entry additionally consists of 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 of shared locks that are taken at a given point in time). +and a lock count (a number representing the number of locks that are taken at a given point in time). A user agent has an associated file system queue which is the result of [=starting a new parallel queue=]. This queue is to be used for all @@ -139,20 +139,18 @@ To take a lock with a |value "`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=]. +1. Let |count| be the |file|'s [=file entry/lock count=]. +1. If |lock| is not "`open`": + 1. If |value| is "`exclusive`" or |lock| is "`taken-exclusive`": + 1. Return "`failure`". 1. If |value| is "`exclusive`": - 1. If |lock| is "`open`": - 1. Set lock to "`taken-exclusive`". - 1. Return "`success`". -1. If |value| is "`shared`": - 1. If |lock| is "`open`": - 1. Set |lock| to "`taken-shared`". - 1. Set |count| to 1. - 1. Return "`success`". - 1. Otherwise, if |lock| is "`taken-shared`": - 1. Increase |count| by 1. - 1. Return "`success`". -1. Return "`failure`". + 1. [=Assert=]: |lock| is "`open`". + 1. [=Assert=]: |count| is 0. + 1. Set |lock| to "`taken-exclusive`". +1. Otherwise: + 1. Set |lock| to "`taken-shared`". +1. Increase |count| by 1. +1. Return "`success`". Note: These steps have to be run on the [=file system queue=]. @@ -160,14 +158,14 @@ Note: These steps have to be run on the [=file system queue=].
To release a [=file entry/lock=] on a given -[=file entry=] |file|: +[=/file entry=] |file|: 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 1. - 1. If |count| is 0, set |lock| to "`open`". -1. Otherwise, set |lock| to "`open`". +1. [=Assert=]: |lock| is not "`open`". +1. Let |count| be the |file|'s [=file entry/lock count=]. +1. [=Assert=]: |count| is greater than 0. +1. Decrease |count| by 1. +1. If |count| is 0, set |lock| to "`open`". Note: These steps have to be run on the [=file system queue=]. From 41b97ba142ed0e8f0731e33264fea4f30d85bd51 Mon Sep 17 00:00:00 2001 From: Nathan Memmott Date: Tue, 19 Dec 2023 16:17:52 -0700 Subject: [PATCH 3/4] Create lock type --- index.bs | 19 +++++++++---------- 1 file changed, 9 insertions(+), 10 deletions(-) diff --git a/index.bs b/index.bs index 6ea488a..586b918 100644 --- a/index.bs +++ b/index.bs @@ -124,10 +124,13 @@ Issue: We should consider having further normative restrictions on file names th never be allowed using this API, rather than leaving it entirely up to underlying file systems. +A lock type is a [=string=] that may exclusively be "`open`", +"`exclusive`", or "`shared`". + A file entry additionally consists of 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`") +a lock (a [=lock type=]), and a lock count (a number representing the number of locks that are taken at a given point in time). A user agent has an associated file system queue which is the @@ -135,20 +138,16 @@ result of [=starting a new parallel queue=]. This queue is to be used for all file system operations.
-To take a lock with a |value| of -"`exclusive`" or "`shared`" on a given [=file entry=] |file|: +To take a lock with a |lockType| (a [=lock type=]) +on a given [=file entry=] |file|: +1. [=Assert=]: |lockType| is not "`open`". 1. Let |lock| be the |file|'s [=file entry/lock=]. 1. Let |count| be the |file|'s [=file entry/lock count=]. 1. If |lock| is not "`open`": - 1. If |value| is "`exclusive`" or |lock| is "`taken-exclusive`": + 1. If |lockType| is "`exclusive`" or |lock| is not equal to |lockType|: 1. Return "`failure`". -1. If |value| is "`exclusive`": - 1. [=Assert=]: |lock| is "`open`". - 1. [=Assert=]: |count| is 0. - 1. Set |lock| to "`taken-exclusive`". -1. Otherwise: - 1. Set |lock| to "`taken-shared`". +1. Set |lock| to |lockType|. 1. Increase |count| by 1. 1. Return "`success`". From 564df68be04ef24b79c6116bff7047762a02b22c Mon Sep 17 00:00:00 2001 From: Nathan Memmott Date: Mon, 13 Nov 2023 14:50:54 -0700 Subject: [PATCH 4/4] Add new SAH and WFS locking modes Adds new locking modes for sync access handles and writable file streams. Updates "file entry/take a lock" and "file entry/lock/release" to support these new modes. --- index.bs | 86 ++++++++++++++++++++++++++++++++++++++++++++------------ 1 file changed, 68 insertions(+), 18 deletions(-) diff --git a/index.bs b/index.bs index 586b918..c2dbfd5 100644 --- a/index.bs +++ b/index.bs @@ -125,7 +125,8 @@ never be allowed using this API, rather than leaving it entirely up to underlyin systems. A lock type is a [=string=] that may exclusively be "`open`", -"`exclusive`", or "`shared`". +"`exclusive`", "`writable-siloed`", "`sync-access-handle-read-only`", +"`sync-access-handle-read-write-unsafe`". A file entry additionally consists of binary data (a [=byte sequence=]), a @@ -170,8 +171,7 @@ Note: These steps have to be run on the [=file system queue=].
-Note: Locks help prevent concurrent modifications to a file. A {{FileSystemWritableFileStream}} -requires a shared lock, while a {{FileSystemSyncAccessHandle}} requires an exclusive one. +Note: Locks help prevent concurrent modifications to a file that are incompatible. A directory entry additionally consists of a [=/set=] of children, which are themselves [=/file system entries=]. @@ -417,8 +417,24 @@ The isSameEntry(|other|) method steps are ## The {{FileSystemFileHandle}} interface ## {#api-filesystemfilehandle} +enum FileSystemWritableFileStreamMode { + "exclusive", + "siloed", +}; + dictionary FileSystemCreateWritableOptions { boolean keepExistingData = false; + FileSystemWritableFileStreamMode mode = "siloed"; +}; + +enum FileSystemSyncAccessHandleMode { + "readwrite", + "read-only", + "readwrite-unsafe", +}; + +dictionary FileSystemCreateSyncAccessHandleOptions { + FileSystemSyncAccessHandleMode mode = "readwrite"; }; [Exposed=(Window,Worker), SecureContext, Serializable] @@ -426,7 +442,7 @@ interface FileSystemFileHandle : FileSystemHandle { Promise<File> getFile(); Promise<FileSystemWritableFileStream> createWritable(optional FileSystemCreateWritableOptions options = {}); [Exposed=DedicatedWorker] - Promise<FileSystemSyncAccessHandle> createSyncAccessHandle(); + Promise<FileSystemSyncAccessHandle> createSyncAccessHandle(optional FileSystemCreateSyncAccessHandleOptions options = {}); }; @@ -535,10 +551,12 @@ The getFile() method steps are: the temporary file starts out empty, otherwise the existing file is first copied to this temporary file. - Creating a {{FileSystemWritableFileStream}} [=file entry/take a lock|takes a shared lock=] on the - [=file entry=] [=locate an entry|locatable=] with |fileHandle|'s [=FileSystemHandle/locator=]. - This prevents the creation of {{FileSystemSyncAccessHandle|FileSystemSyncAccessHandles}} - for the entry, until the stream is closed. + Creating a {{FileSystemWritableFileStream}} [=file entry/take a lock|takes a lock=] on the + [=file entry=] [=locate an entry|locatable=] with |fileHandle|'s [=FileSystemHandle/locator=] + and a |lockType| determined by {{FileSystemCreateWritableOptions/mode}}. Until the stream is + closed, both {{FileSystemCreateWritableOptions/mode|modes}} prevent any file operation or the + creation of a file primitive on the [=file entry=], but "`siloed`" will allow the creation of other + {{FileSystemWritableFileStream}} in "`siloed`" {{FileSystemCreateWritableOptions/mode}}.

See WICG/file-system-access issue #67 @@ -572,8 +590,15 @@ The createWritable(|options|) method |result| with a "{{NotFoundError}}" {{DOMException}} and abort these steps. 1. [=Assert=]: |entry| is a [=file entry=]. + 1. Let |lockType| be the empty string. + 1. Let |mode| be |options|["{{FileSystemCreateWritableOptions/mode}}"]. + 1. If |mode| is "`exclusive`": + 1. Set |lockType| to "`exclusive`". + 1. Otherwise: + 1. [=Assert=]: |mode| is "`siloed`". + 1. Set |lockType| to "`writable-siloed`". 1. Let |lockResult| be the result of [=file entry/take a lock|taking a lock=] - with "`shared`" on |entry|. + with |lockType| on |entry|. 1. [=Queue a storage task=] with |global| to run these steps: 1. If |lockResult| is "`failure`", [=/reject=] |result| with a @@ -600,11 +625,12 @@ The createWritable(|options|) method [=file entry=] [=locate an entry|locatable=] by |fileHandle|'s [=FileSystemHandle/locator=]. To ensure the changes are reflected in this file, the handle can be flushed. - Creating a {{FileSystemSyncAccessHandle}} [=file entry/take a lock|takes an exclusive lock=] on the - [=file entry=] [=locate an entry|locatable=] with |fileHandle|'s [=FileSystemHandle/locator=]. - This prevents the creation of further {{FileSystemSyncAccessHandle|FileSystemSyncAccessHandles}} - or {{FileSystemWritableFileStream|FileSystemWritableFileStreams}} - for the entry, until the access handle is closed. + Creating a {{FileSystemSyncAccessHandle}} [=file entry/take a lock|takes a lock=] on the + [=file entry=] [=locate an entry|locatable=] with |fileHandle|'s [=FileSystemHandle/locator=] + and a |lockType| determined by {{FileSystemCreateSyncAccessHandleOptions/mode}}. Until the access handle is + closed, all {{FileSystemCreateSyncAccessHandleOptions/mode|modes}} prevent any file operation or the + creation of a file primitive on the [=file entry=], but "`read-only`" and "`readwrite-unsafe`" will allow the creation of other + {{FileSystemSyncAccessHandle}} in their respective {{FileSystemCreateSyncAccessHandleOptions/mode|modes}}. The returned {{FileSystemSyncAccessHandle}} offers synchronous methods. This allows for higher performance on contexts where asynchronous operations come with high overhead, e.g., WebAssembly. @@ -614,7 +640,7 @@ The createWritable(|options|) method

-The createSyncAccessHandle() method steps are: +The createSyncAccessHandle(|options|) method steps are: 1. Let |result| be [=a new promise=]. 1. Let |locator| be [=this=]'s [=FileSystemHandle/locator=]. @@ -642,15 +668,28 @@ The createSyncAccessHandle() method s |result| with a "{{NotFoundError}}" {{DOMException}} and abort these steps. 1. [=Assert=]: |entry| is a [=file entry=]. + 1. Let |lockType| be the empty string. + 1. Let |writeAccess| be the empty string. + 1. Let |mode| be |options|["{{FileSystemCreateSyncAccessHandleOptions/mode}}"]. + 1. If |mode| is "`readwrite`": + 1. Set |lockType| to "`exclusive`". + 1. Set |writeAccess| to "`writable`". + 1. Otherwise, if |mode| is "`read-only`": + 1. Set |lockType| to "`sync-access-handle-read-only`". + 1. Set |writeAccess| to "`not-writable`". + 1. Otherwise: + 1. [=Assert=]: |mode| is "`readwrite-unsafe`". + 1. Set |lockType| to "`sync-access-handle-read-write-unsafe`". + 1. Set |writeAccess| to "`writable`". 1. Let |lockResult| be the result of [=file entry/take a lock|taking a lock=] - with "`exclusive`" on |entry|. + with |lockType| on |entry|. 1. [=Queue a storage task=] with |global| to run these steps: 1. If |lockResult| is "`failure`", [=/reject=] |result| with a "{{NoModificationAllowedError}}" {{DOMException}} and abort these steps. 1. Let |handle| be the result of creating a new `FileSystemSyncAccessHandle` - for |entry| in |realm|. + with |entry| and |writeAccess| in |realm|. 1. [=/Resolve=] |result| with |handle|. 1. Return |result|. @@ -1437,6 +1476,9 @@ A {{FileSystemSyncAccessHandle}} has an associated \[[state]], a string that may exclusively be "`open`" or "`closed`". +A {{FileSystemSyncAccessHandle}} has an associated \[[writeAccess]], +a [=string=] that may exclusively be "`writable`" or "`not-writable`". + 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. @@ -1448,11 +1490,13 @@ A {{FileSystemSyncAccessHandle}} has a fil
To create a new `FileSystemSyncAccessHandle` -given a [=file entry=] |file| in a [=/Realm=] |realm|: +given a [=file entry=] |file| and a [=string=] |writeAccess| in a [=/Realm=] |realm|: +1. [=Assert=]: |writeAccess| is "`writable`" or "`not-writable`". 1. Let |handle| be a [=new=] {{FileSystemSyncAccessHandle}} in |realm|. 1. Set |handle|'s [=FileSystemSyncAccessHandle/[[file]]=] to |file|. 1. Set |handle|'s [=FileSystemSyncAccessHandle/[[state]]=] to "`open`". +1. Set |handle|'s [=FileSystemSyncAccessHandle/[[writeAccess]]=] to |writeAccess|. 1. Return |handle|.
@@ -1515,6 +1559,8 @@ The write(|buffer|, {{FileSystemReadW 1. If [=this=]'s [=[[state]]=] is "`closed`", [=throw=] an "{{InvalidStateError}}" {{DOMException}}. +1. If [=this=]'s [=[[writeAccess]]=]' is "`not-writable`", + [=throw=] a "{{NoModificationAllowedError}}" {{DOMException}}. 1. Let |writePosition| be |options|["{{FileSystemReadWriteOptions/at}}"] if |options|["{{FileSystemReadWriteOptions/at}}"] [=map/exists=]; otherwise [=this=]'s [=FileSystemSyncAccessHandle/file position cursor=]. @@ -1575,6 +1621,8 @@ The truncate(|newSize|) method 1. If [=this=]'s [=[[state]]=] is "`closed`", [=throw=] an "{{InvalidStateError}}" {{DOMException}}. +1. If [=this=]'s [=[[writeAccess]]=]' is "`not-writable`", + [=throw=] a "{{NoModificationAllowedError}}" {{DOMException}}. 1. Let |fileContents| be a copy of [=this=]'s [=FileSystemSyncAccessHandle/[[file]]=]'s [=file entry/binary data=]. 1. Let |oldSize| be the [=byte sequence/length=] of [=this=]'s @@ -1631,6 +1679,8 @@ The flush() method steps are: 1. If [=this=]'s [=[[state]]=] is "`closed`", [=throw=] an "{{InvalidStateError}}" {{DOMException}}. +1. If [=this=]'s [=[[writeAccess]]=]' is "`not-writable`", + [=throw=] a "{{NoModificationAllowedError}}" {{DOMException}}. 1. Attempt to transfer all cached modifications of the file's content to the file system's underlying storage device.