From e9ba03b762757cc198aa2d90b7319d8243309a26 Mon Sep 17 00:00:00 2001 From: Nathan Memmott Date: Mon, 13 Nov 2023 14:50:54 -0700 Subject: [PATCH] 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 | 127 ++++++++++++++++++++++++++++++++++++++----------------- 1 file changed, 88 insertions(+), 39 deletions(-) diff --git a/index.bs b/index.bs index 43da40f..c041b55 100644 --- a/index.bs +++ b/index.bs @@ -124,34 +124,35 @@ 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`", "`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 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). +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 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/shared lock count=]. -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. Let |count| be the |file|'s [=file entry/lock count=]. +1. If |lock| is "`open`": + 1. Set |lock| to |lockType|. + 1. Set |count| to 1. + 1. Return "`success`". +1. If |lock| is not "`exclusive`": + 1. If |lock| equals |lockType|: + 1. Increase |count| by 1. + 1. Return "`success`". 1. Return "`failure`". Note: These steps have to be run on the [=file system queue=]. @@ -163,18 +164,16 @@ To release a [=file entry/lock=] on a given [=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. 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=].
-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=]. @@ -420,8 +419,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] @@ -429,7 +444,7 @@ interface FileSystemFileHandle : FileSystemHandle { Promise<File> getFile(); Promise<FileSystemWritableFileStream> createWritable(optional FileSystemCreateWritableOptions options = {}); [Exposed=DedicatedWorker] - Promise<FileSystemSyncAccessHandle> createSyncAccessHandle(); + Promise<FileSystemSyncAccessHandle> createSyncAccessHandle(optional FileSystemCreateSyncAccessHandleOptions options = {}); }; @@ -538,10 +553,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 @@ -575,8 +592,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 @@ -603,11 +627,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. @@ -617,7 +642,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=]. @@ -645,15 +670,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|. @@ -1440,6 +1478,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. @@ -1451,11 +1492,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|.
@@ -1518,6 +1561,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=]. @@ -1578,6 +1623,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 @@ -1634,6 +1681,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.