Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add AccessHandles to spec #344

Closed
wants to merge 19 commits into from
Closed
Changes from 6 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
253 changes: 246 additions & 7 deletions index.bs
Original file line number Diff line number Diff line change
Expand Up @@ -109,8 +109,44 @@ majority of file extensions are purely alphanumeric, but compound extensions (su
hence the inclusion of + and . as allowed code points.

A <dfn lt="file|file entry">file entry</dfn> additionally consists of
<dfn for="file entry">binary data</dfn> (a [=byte sequence=]) and a
<dfn for="file entry">modification timestamp</dfn> (a number representing the number of milliseconds since the <a spec=FileAPI>Unix Epoch</a>).
<dfn for="file entry">binary data</dfn> (a [=byte sequence=]), a
<dfn for="file entry">modification timestamp</dfn> (a number representing the number of milliseconds since the <a spec=FileAPI>Unix Epoch</a>)
and a <dfn for="file entry">lock</dfn>. A lock is a string that may exclusively be "open", "taken-exclusive" and "taken-shared".

<div algorithm>
To <dfn for="file entry/lock">take</dfn> a [=lock=] with a |value| of "exclusive" or "shared", for a given |file|,
run the following steps:

1. Let |result| be [=a new promise=].
1. Let |lock| be the |file|'s associated [=lock=]
1. Run the following steps [=in parallel=]:
1. If |value| is "exclusive":
1. If |lock| is "open":
1. Set lock to "taken-exclusive".
1. [=/resolve=] |result|.
1. Else [=/reject=] |result|.
1. If |value| is "shared":
1. If |lock| is "open":
1. Set lock to "taken-exclusive".
1. [=/resolve=] |result|.
1. Else if |lock| is "taken-shared":
1. [=/resolve=] |result|.
1. Else [=/reject=] |result|.
1. Return |result|.

</div>

<div algorithm>
To <dfn for="file entry/lock">release</dfn> a [=lock=] for a given |file|,
run the following steps:

1. Let |lock| be the |file|'s associated [=lock=]
1. Set lock to "open".

</div>

Note: Locks help prevent concurrent modifications to a file. A {{FileSystemWritableFileStream}}
requires a shared lock, while a {{FileSystemSyncAccessHandle}} requires an exclusive one.

A <dfn lt="directory|directory entry">directory entry</dfn> additionally consists of a [=/set=] of
<dfn for="directory entry">children</dfn>, which are themselves [=/entries=]. Each member is either a [=/file=] or a [=directory=].
Expand Down Expand Up @@ -276,7 +312,7 @@ the {{FileSystemHandle}} interface can all be associated with the same [=/entry=
<div algorithm="serialization steps">
{{FileSystemHandle}} objects are [=serializable objects=].

Advisement: In the Origin Trial as available in Chrome 78, these objects are not yet serializable.
Advisement: In the Origin Trial as available in Chrome 78, these objects are not yet serializable.
In Chrome 82 they are.

Their [=serialization steps=], given |value|, |serialized| and <var ignore>forStorage</var> are:
Expand Down Expand Up @@ -415,10 +451,18 @@ dictionary FileSystemCreateWritableOptions {
boolean keepExistingData = false;
};

enum AccessHandleMode { "in-place" };

dictionary FileSystemFileHandleCreateAccessHandleOptions {
required AccessHandleMode mode;
};

[Exposed=(Window,Worker), SecureContext, Serializable]
interface FileSystemFileHandle : FileSystemHandle {
Promise<File> getFile();
Promise<FileSystemWritableFileStream> createWritable(optional FileSystemCreateWritableOptions options = {});
[Exposed=DedicatedWorker]
Promise<FileSystemSyncAccessHandle> createSyncAccessHandle(FileSystemFileHandleCreateAccessHandleOptions options);
};
</xmp>

Expand All @@ -427,7 +471,7 @@ A {{FileSystemFileHandle}}'s associated [=FileSystemHandle/entry=] must be a [=f
{{FileSystemFileHandle}} objects are [=serializable objects=]. Their [=serialization steps=] and
[=deserialization steps=] are the same as those for {{FileSystemHandle}}.

Advisement: In the Origin Trial as available in Chrome 78, these objects are not yet serializable.
Advisement: In the Origin Trial as available in Chrome 78, these objects are not yet serializable.
In Chrome 82 they are.

### The {{FileSystemFileHandle/getFile()}} method ### {#api-filesystemfilehandle-getfile}
Expand Down Expand Up @@ -482,6 +526,10 @@ Advisement: In the Origin Trial as available in Chrome 82, createWritable replac
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.
</div>

Issue(67): There has been some discussion around and desire for a "inPlace" mode for createWritable
Expand All @@ -491,6 +539,8 @@ currently implemented in Chrome. Implementing this is currently blocked on figur
combine the desire to run malware checks with the desire to let websites make fast in-place
modifications to existing large files.

// TODO(fivedots): release lock once the stream is closed.

<div algorithm>
The <dfn method for=FileSystemFileHandle>createWritable(|options|)</dfn> method, when invoked, must run these steps:

Expand All @@ -502,6 +552,9 @@ The <dfn method for=FileSystemFileHandle>createWritable(|options|)</dfn> method,
1. If |permissionStatus| is not {{PermissionState/"granted"}},
reject |result| with a {{NotAllowedError}} and abort.
1. Let |entry| be <b>[=this=]</b>'s [=FileSystemHandle/entry=].
1. Let |lockValue| be "shared".
1. Let |lockResult| be the result of [=file entry/lock/take|taking a lock=] with |lockValue| on |entry|.
If that throws an exception, [=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 <b>[=this=]</b>'s [=relevant realm=].
1. If |options|.{{FileSystemCreateWritableOptions/keepExistingData}} is `true`:
Expand All @@ -511,6 +564,53 @@ The <dfn method for=FileSystemFileHandle>createWritable(|options|)</dfn> method,

</div>

### The {{FileSystemFileHandle/createSyncAccessHandle()}} method ### {#api-filesystemfilehandle-createsyncaccesshandle}

<div class="note domintro">
: |handle| = await |fileHandle| . {{FileSystemFileHandle/createSyncAccessHandle()|createSyncAccessHandle}}({ {{FileSystemFileHandleCreateAccessHandleOptions/mode}}: "in-place" })
:: Returns a {{FileSystemSyncAccessHandle}} that can be used to read/write from/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 must 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|FileSystemSyncAccessHandles}} or {{FileSystemWritableFileStream|FileSystemWritableFileStreams}}
for the entry, until the access handle is closed.

The returned {{FileSystemSyncAccessHandle}} offers synchronous {{FileSystemFileHandle/read()}} and
{{FileSystemFileHandle/write()}} methods. This allows for higher performance for critical methods on
contexts where asynchronous operations come with high overhead i.e., WebAssembly.

For the time being, this method will only succeed when the |fileHandle| belongs to the
[=origin private file system=]. Also temporarily, the {{FileSystemFileHandleCreateAccessHandleOptions/mode}} parameter
must be set to "in-place". This prevents us from setting a default behavior and allows us to bring access handles to
other filesytems in the future.

</div>

<div algorithm>
The <dfn method for=FileSystemFileHandle>createSyncAccessHandle(|options|)</dfn> method, when invoked, must run these steps:

1. Let |result| be [=a new promise=].
1. Run the following steps [=in parallel=]:
1. Let |permissionStatus| be the result of [=requesting file system permission=]
given <b>[=this=]</b> and {{"readwrite"}}.
If that throws an exception, [=reject=] |result| with that exception and abort.
1. If |permissionStatus| is not {{PermissionState/"granted"}},
reject |result| with a {{NotAllowedError}} and abort.
1. Let |entry| be <b>[=this=]</b>'s [=FileSystemHandle/entry=].
1. If |entry| does not represent an [=/entry=] in an [=origin private file system=],
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Important

I am surprised this exception is after the permissions check? Are there tests for that exception order?

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We currently don't have tests to check this exception order. Since NotAllowedError would fit as well (it's a not recoverable error, and it is a limitation being imposed by the platform), and to avoid adding potentially combinatorial tests on exception order, I would change this error to NotAllowedError. WDYT?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

That seems reasonable, although it removes the ability for web author code to distinguish these cases and do something about it. You could imagine a bug in a web app where they are accidentally using this API on non-OPFS files, and they have

try {
  useAPIIncorrectly();
} catch (e) {
  if (e.name === "NotAllowedError") {
    tellTheUser("Please grant the permission!!");
  }
}

which would result in a more-confusing user experience.

Copy link
Collaborator Author

@fivedots fivedots Apr 12, 2022

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I migrated this comment to whatwg/fs#21 (comment) in order to close this PR!

reject |result| with a {{InvalidStateError}} and abort.
1. Let |lockValue| be "exclusive".
1. Let |lockResult| be the result of [=file entry/lock/take|taking a lock=] with |lockValue| on |entry|.
If that throws an exception, [=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 <b>[=this=]</b>'s [=relevant realm=].
1. [=/Resolve=] |result| with |handle|.
1. Return |result|.

</div>

## The {{FileSystemDirectoryHandle}} interface ## {#api-filesystemdirectoryhandle}

<xmp class=idl>
Expand Down Expand Up @@ -544,7 +644,7 @@ A {{FileSystemDirectoryHandle}}'s associated [=FileSystemHandle/entry=] must be
{{FileSystemDirectoryHandle}} objects are [=serializable objects=]. Their [=serialization steps=] and
[=deserialization steps=] are the same as those for {{FileSystemHandle}}.

Advisement: In the Origin Trial as available in Chrome 78, these objects are not yet serializable.
Advisement: In the Origin Trial as available in Chrome 78, these objects are not yet serializable.
In Chrome 82 they are.

Advisement: In Chrome versions upto Chrome 85 `getFileHandle` and `getDirectoryHandle` where
Expand Down Expand Up @@ -1141,6 +1241,145 @@ steps:

</div>

## The {{FileSystemSyncAccessHandle}} interface ## {#api-filesystemsyncaccesshandle}

<xmp class=idl>

dictionary FilesystemReadWriteOptions {
[EnforceRange] unsigned long long at;
}

[Exposed=DedicatedWorker, SecureContext]
interface FileSystemSyncAccessHandle {
unsigned long long read([AllowShared] BufferSource buffer,
FilesystemReadWriteOptions options);
unsigned long long write([AllowShared] BufferSource buffer,
FilesystemReadWriteOptions options);

Promise<undefined> truncate([EnforceRange] unsigned long long size);
Promise<unsigned long long> getSize();
Promise<undefined> flush();
Promise<undefined> close();
};

</xmp>

A {{FileSystemSyncAccessHandle}} has an associated <dfn for=FileSystemSyncAccessHandle>\[[file]]</dfn> (a [=file entry=]).

<div class="note domintro">

A {{FileSystemSyncAccessHandle}} is an object that is capable of reading/writing from/to,
as well as obtaining and changing the size of, a single file.

The {{FileSystemFileHandle/read()}} and {{FileSystemFileHandle/write()}} methods are synchronous.
This allows for higher performance for critical methods on contexts where asynchronous
operations come with high overhead i.e., WebAssembly.

</div>

<div algorithm>
To <dfn>create a new FileSystemSyncAccessHandle</dfn> 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. Return |handle|.

</div>

### The {{FileSystemSyncAccessHandle/read()}} method ### {#api-filesystemsyncaccesshandle-read}

<div class="note domintro">
: |handle| . {{FileSystemSyncAccessHandle/read()|read}}(|buffer|, {{FilesystemReadWriteOptions/at}}: |position|)
:: Reads the contents of the file associated with |handle| into |buffer|, with |position| as the offset.
</div>

<div algorithm>
The <dfn method for=FileSystemSyncAccessHandle>read(|buffer|, {{FilesystemReadWriteOptions/at}}: |position|)</dfn> method, when invoked, must run
these steps:

// TODO(fivedots): fill in. Does this algorithm need to check that the access handle has not been closed?

</div>

### The {{FileSystemSyncAccessHandle/write()}} method ### {#api-filesystemsyncaccesshandle-write}

<div class="note domintro">
: |handle| . {{FileSystemSyncAccessHandle/write()|write}}(|buffer|, {{FilesystemReadWriteOptions/at}}: |position|)
:: Writes the content of |buffer| into the file associated with |handle| with |position| as the offset.
</div>

<div algorithm>
The <dfn method for=FileSystemSyncAccessHandle>write(|buffer|, {{FilesystemReadWriteOptions/at}}: |position|)</dfn> method, when invoked, must run
these steps:

// TODO(fivedots): fill in. Does this algorithm need to explicitly interact with storage quota?

</div>

### The {{FileSystemSyncAccessHandle/truncate()}} method ### {#api-filesystemsyncaccesshandle-truncate}

<div class="note domintro">
: |handle| . {{FileSystemSyncAccessHandle/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.
</div>

<div algorithm>
The <dfn method for=FileSystemSyncAccessHandle>truncate(|size|)</dfn> method, when invoked, must run
these steps:

// TODO(fivedots): fill in.

</div>

### The {{FileSystemSyncAccessHandle/getSize()}} method ### {#api-filesystemsyncaccesshandle-getsize}

<div class="note domintro">
: |handle| . {{FileSystemSyncAccessHandle/getSize()}}
:: Returns the size of the file associated with |handle| in bytes.
</div>

<div algorithm>
The <dfn method for=FileSystemSyncAccessHandle>getSize()</dfn> method, when invoked, must run
these steps:

// TODO(fivedots): fill in.

</div>

### The {{FileSystemSyncAccessHandle/flush()}} method ### {#api-filesystemsyncaccesshandle-flush}

<div class="note domintro">
: |handle| . {{FileSystemSyncAccessHandle/flush()}}
:: Ensures that the contents of the file associated with |handle| contain all the modifications done through {{FileSystemSyncAccessHandle/read()}} and {{FileSystemSyncAccessHandle/write()}}.
</div>

<div algorithm>
The <dfn method for=FileSystemSyncAccessHandle>flush()</dfn> method, when invoked, must run
these steps:

// TODO(fivedots): fill in.

</div>

### The {{FileSystemSyncAccessHandle/close()}} method ### {#api-filesystemsyncaccesshandle-close}

// TODO(fivedots): |fileHandle| is not properly defined here, consider adding an attribute to |handle|.

<div class="note domintro">
: |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 |fileHandle|.
</div>

<div algorithm>
The <dfn method for=FileSystemSyncAccessHandle>close()</dfn> method, when invoked, must run
these steps:

// TODO(fivedots): fill in.

</div>

# Accessing Local File System # {#local-filesystem}

<xmp class=idl>
Expand Down Expand Up @@ -1763,10 +2002,10 @@ respectively.
The <dfn method for=DataTransferItem>getAsFileSystemHandle()</dfn> method steps are:

1. If the {{DataTransferItem}} object is not in the <a spec=html>read/write
mode</a> or the <a spec=html>read-only mode</a>, return
mode</a> or the <a spec=html>read-only mode</a>, return
[=a promise resolved with=] `null`.

1. If the <a spec=html>the drag data item kind</a> is not <em>File</em>,
1. If the <a spec=html>the drag data item kind</a> is not <em>File</em>,
then return [=a promise resolved with=] `null`.

1. Let |p| be [=a new promise=].
Expand Down