Repository: WICG/file-system-access
Branch: main
Commit: 93119927fa7a
Files: 15
Total size: 124.5 KB
Directory structure:
gitextract_jv1ysorf/
├── .github/
│ └── workflows/
│ └── pr-push.yml
├── .gitignore
├── .pr-preview.json
├── CODE_OF_CONDUCT.md
├── CONTRIBUTING.md
├── EXPLAINER.md
├── LICENSE.md
├── README.md
├── changes.md
├── index.bs
├── proposals/
│ ├── AccessHandle.md
│ ├── CloudIdentifier.md
│ └── SuggestedNameAndDir.md
├── security-privacy-questionnaire.md
└── w3c.json
================================================
FILE CONTENTS
================================================
================================================
FILE: .github/workflows/pr-push.yml
================================================
name: CI
on:
pull_request: {}
push:
branches: [main]
jobs:
main:
name: Build, Validate and Deploy
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v2
- uses: sidvishnoi/spec-prod@v1
with:
GH_PAGES_BRANCH: gh-pages
VALIDATE_MARKUP: false
================================================
FILE: .gitignore
================================================
/index.html
================================================
FILE: .pr-preview.json
================================================
{
"src_file": "index.bs",
"type": "bikeshed",
"params": {
"force": 1
}
}
================================================
FILE: CODE_OF_CONDUCT.md
================================================
# Code of Conduct
All documentation, code and communication under this repository are covered by the [W3C Code of Ethics and Professional Conduct](https://www.w3.org/Consortium/cepc/).
================================================
FILE: CONTRIBUTING.md
================================================
# Web Platform Incubator Community Group
This repository is being used for work in the W3C Web Platform Incubator Community Group, governed by the [W3C Community License
Agreement (CLA)](http://www.w3.org/community/about/agreements/cla/). To make substantive contributions,
you must join the CG.
If you are not the sole contributor to a contribution (pull request), please identify all
contributors in the pull request comment.
To add a contributor (other than yourself, that's automatic), mark them one per line as follows:
```
+@github_username
```
If you added a contributor by mistake, you can remove them in a comment with:
```
-@github_username
```
If you are making a pull request on behalf of someone else but you had no part in designing the
feature, you can remove yourself with the above syntax.
================================================
FILE: EXPLAINER.md
================================================
# What is all this?
At a high level what we're providing is several bits:
1. A modernized version of the existing (but not really standardized)
[`Entry`](https://www.w3.org/TR/2012/WD-file-system-api-20120417/#idl-def-Entry)
API in the form of new (names TBD) `FileSystemFileHandle` and
`FileSystemDirectoryHandle` interfaces (see also the [wicg entries-api](https://wicg.github.io/entries-api/),
a read-only and slightly more standardized subset of this same API).
2. A modernized version of the existing (and also not really standardized)
[`FileWriter`](https://dev.w3.org/2009/dap/file-system/file-writer.html#the-filewriter-interface)
interface.
3. Various entry points to get a handle representing a limited view of the
local file system. I.e. either via a file picker, or to get access to
certain well known directories. Mimicking things such as chrome's
[`chrome.fileSystem.chooseEntry`](https://developer.chrome.com/apps/fileSystem#method-chooseEntry) API.
## Use-Cases
In native applications, there are common file access patterns that we aim to address with this API.
### Single-file Editor
1. Open a file from the user's file system
1. Edit the file and save the changes back to the file system
1. Open another file in the same manner described above
1. Auto-save any changes to the files in the browsing session
1. The files can be opened in any native or web applications concurrently
1. Changes to the files on disk, made in any native or other web application, are accessible
1. Access the files with the same access in future browsing sessions
1. Create a new file in the editor
1. Auto-save changes to the new file in a temporary location, even before the user has picked a file name/location
### Multi-file Editor
1. Open a directory that contains many files and sub-directories, represented hierarchically
1. Find and edit multiple files and save the changes back to the file system
1. Auto-save any further changes to the files in the browsing session
1. The files can be opened in any native or web applications concurrently
1. Changes to the files on disk, made in any native or other web applications, are accessible
1. Access the files with the same access in future browsing sessions
1. New files in the directory tree, that were not present at the time the root directory was
opened, created in any native or other web application, are accessible
### File Libraries
1. Open one or more directories that contain many files and sub-directories
1. Changes to the files on disk, made in any native or other web applications, are accessible
1. Access the files with the same access in future browsing sessions
1. New files in the directory tree, that were not present at the time the root directory was
opened, created in any native or other web application, are accessible
1. When the user chooses to do some work, access one or more of those files
## Goals
The main overarching goal here is to increase interoperability of web applications
with native applications, specifically where it comes to being able to operate on
the native file system.
Traditionally the file system is how different apps collaborate and share data on
desktop platforms, but also on mobile there is generally at least some sort of
concept of a file system, although it is less prevalent there.
Some example applications of the API we would like to address:
* A simple "single file" editor. Also possible integration with a "file-type
handler" kind of API. Things like (rich) text editors, photo editors, etc.
* Multi-File editors. Things like IDEs, CAD style applications, the kind of apps
where you work on a project consisting of multiple files, usually together in
the same directory.
* Apps that want to work with "libraries" of certain types of files. I.e. photo
managers, music managers/media players, or even drawing/publishing apps that
want access to the raw font files for all fonts on the system.
But even though we'd like to design the API to eventually enable all these use
cases, initially we'd almost certainly be shipping a very limited API surface
with limited capabilities.
Additionally we want to make it possible for websites to get access to some
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. It also makes it easier
to write automated tests for code using this API.
## Non-goals
At least for now out of scope is access to the full file system, subscribing to
file change notifications, probably many things related to file metadata (i.e.
marking files as executable/hidden, etc). Also not yet planning to address how
this new API might integrate with `<input type=file>`.
# Example code
```javascript
// Show a file picker to open a file.
const [file_ref] = await self.showOpenFilePicker({
multiple: false,
types: [{description: 'Images', accept: {'image/*': ['.jpg', '.gif', '.png']}}],
suggestedStartLocation: 'pictures-library'
});
if (!file_ref) {
// User cancelled, or otherwise failed to open a file.
return;
}
// Read the contents of the file.
const file_reader = new FileReader();
file_reader.onload = async (event) => {
// File contents will appear in event.target.result. See
// https://developer.mozilla.org/en-US/docs/Web/API/FileReader/onload for
// more info.
// ...
// Write changed contents back to the file. Rejects if file reference is not
// writable. Note that it is not generally possible to know if a file
// reference is going to be writable without actually trying to write to it.
// For example, both the underlying filesystem level permissions for the
// file might have changed, or the user/user agent might have revoked write
// access for this website to this file after it acquired the file
// reference.
const writable = await file_ref.createWritable();
await writable.write(new Blob(['foobar']));
await writable.seek(1024);
await writable.write(new Blob(['bla']));
// |writable| is also a WritableStream, so you can for example pipe into it.
let response = await fetch('foo');
await response.body.pipeTo(writable);
// pipeTo by default closes the destination pipe, otherwise an explicit
// writable.close() call would have been needed to persist the written data.
};
// file_ref.file() method will reject if site (no longer) has access to the
// file.
let file = await file_ref.file();
// readAsArrayBuffer() is async and returns immediately. |file_reader|'s onload
// handler will be called with the result of the file read.
file_reader.readAsArrayBuffer(file);
```
Also possible to store file references in IDB to re-read and write to them later.
```javascript
// Open a db instance to save file references for later sessions
let db;
let request = indexedDB.open("WritableFilesDemo");
request.onerror = function(e) { console.log(e); }
request.onsuccess = function(e) { db = e.target.result; }
// Show file picker UI.
const [file_ref] = await self.showOpenFilePicker();
if (file_ref) {
// Save the reference to open the file later.
let transaction = db.transaction(["filerefs"], "readwrite");
let request = transaction.objectStore("filerefs").add( file_ref );
request.onsuccess = function(e) { console.log(e); }
// Do other useful things with the opened file.
};
// ...
// Retrieve a file you've opened before. Show's no filepicker UI, but can show
// some other permission prompt if the browser so desires.
// The browser can choose when to allow or not allow this open.
let file_id = "123"; // Some logic to determine which file you'd like to open
let transaction = db.transaction(["filerefs"], "readonly");
let request = transaction.objectStore("filerefs").get(file_id);
request.onsuccess = async function(e) {
let ref = e.result;
// Permissions for the handle may have expired while the handle was stored
// in IndexedDB. Before it is safe to use the handle we should request at
// least read access to the handle again.
if (await ref.requestPermission() != 'granted') {
// No longer allowed to access the handle.
return;
}
// Rejects if file is no longer readable, either because it doesn't exist
// anymore or because the website no longer has permission to read it.
let file = await ref.file();
// ... read from file
// Rejects if file is no longer writable, because the website no longer has
// permission to write to it.
let file_writer = await ref.createWritable();
// ... write to file_writer
}
```
The fact that handles are serializable also means you can `postMessage` them around:
```javascript
// In a service worker:
self.addEventListener('some-hypothetical-launch-event', async (e) => {
// e.file is a FileSystemFileHandle representing the file this SW was launched with.
let win = await clients.openWindow('bla.html');
if (win)
win.postMessage({openFile: e.file});
});
// In bla.html
navigator.serviceWorker.addEventListener('message', e => {
let file_ref = e.openFile;
// Do something useful with the file reference.
});
```
Also possible to get access to an entire directory.
```javascript
const dir_ref = await self.showDirectoryPicker();
if (!dir_ref) {
// User cancelled, or otherwise failed to open a directory.
return;
}
// Read directory contents.
for await (const [name, entry] of dir_ref) {
// entry is a FileSystemFileHandle or a FileSystemDirectoryHandle.
// name is equal to entry.name
}
// Get a specific file.
const file_ref = await dir_ref.getFile('foo.js');
// Do something useful with the file.
// Get a subdirectory.
const subdir = await dir_ref.getDirectory('bla', {create: true});
// No special API to create copies, but still possible to do so by using
// available read and write APIs.
const new_file = await dir_ref.getFile('new_name', {create: true});
const new_file_writer = await new_file.createWritable();
await new_file_writer.write(await file_ref.getFile());
await new_file_writer.close();
// Or using streams:
const copy2 = await dir_ref.getFile('new_name', {create: true});
(await file_ref.getFile()).stream().pipeTo(await copy2.createWritable());
```
You can also check if two references reference the same file or directory (or at
least reference the same path), as well as lookup the relative path of an entry
inside another directory you have access to.
If for example an IDE has access to a directory, and uses that to display a tree
view of said directory, this can be useful to be able to highlight a file in that
tree, even if the file is opened through a new file picker by opening an existing
file or saving to a new file.
```javascript
// Assume we at some point got a valid directory handle.
const dir_ref = await self.showDirectoryPicker();
if (!dir_ref) return;
// Now get a file reference by showing another file picker:
const file_ref = await self.showOpenFilePicker();
if (!file_ref) {
// User cancelled, or otherwise failed to open a file.
return;
}
// 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:
let entry = dir_ref;
for (const name of relative_path) {
entry = await entry.getChild(name);
}
// Now |entry| will represent the same file on disk as |file_ref|.
assert await entry.isSameEntry(file_ref) == true;
}
```
To get access to a writable directory without having to ask the user for access,
we also provide a "sandboxed" file system. Files in this directory are not
exposed to native applications (or other web applications), but instead are
private to the origin. Storage in this sandboxed file system is subject to
quota restrictions and eviction measures like other web exposed storage mechanisms.
```javascript
const sandboxed_dir = await self.getSandboxedFileSystem();
// The website can freely create files and directories in this directory.
const cache_dir = await sandboxed_dir.getDirectory('cache', {create: true});
for await (const entry of cache_dir.values()) {
// Do something with entry.
};
const new_file = await sandboxed_dir.getFile('Untitled 1.txt', {create: true});
const writer = await new_file.createWritable();
writer.write("some data");
await writer.close();
```
And perhaps even possible to get access to certain "well-known" directories,
without showing a file picker, i.e. to get access to all fonts, all photos, or
similar. Could still include some kind of permission prompt if needed.
```javascript
const font_dir = await FileSystemDirectoryHandle.getSystemDirectory({type: 'fonts'});
for await (const entry of font_dir.values()) {
// Use font entry.
};
```
# Proposed security models
By far the hardest part for this API is of course going to be the security model
to use. The API provides a lot of scary power to websites that could be abused
in many terrible ways. There are both major privacy risks (websites getting
access to private data they weren't supposed to have access to) as well as
security risks (websites modifying executables, installing viruses, encrypting
the users data and demanding ransoms, etc). So great care will have to be taken
to limit how much damage a website can do, and make sure a user understands what
they are giving a website access to. Persistent access to a file could also be
used as some form of super-cookie (but of course all access to files should be
revoked when cookies/storage are cleared, so this shouldn't be too bad).
The primary entry point for this API is a file picker (i.e. a chooser). As such
the user always is in full control over what files and directories a website has
access to. Furthermore every access to the file (either reading or writing)
after a website has somehow gotten a handle is done through an asynchronous API,
so browser could include more prompting and/or permission checking at those
points. This last bit is particularly important when it comes to persisting
handles in IndexedDB. When a handle is retrieved later a user agent might want
to re-prompt to allow access to the file or directory.
Other parts that can contribute to making this API as safe as possible for users
include:
## Limiting access to certain directories
For example it is probably a good idea for a user agent to not allow the user
to select things like the root of a filesystem, certain system directories,
the users entire home directory, or even their entire downloads directory.
## Limiting write access to certain file types
Not allowing websites to write to certain file types such as executables will
limit the possible attack surface.
## Other things user agents come up with
# Staged implementation
At least in chrome we're not planning on implementing and shipping all this at
once. Quite likely an initial implementation will for example not include any of
the transferability/serializability and thus retainability of references. We do
want to add those feature in later iterations, so we're designing the API to
support them and hope to come up with a security model that can be adapted to
support them, but explicitly not supporting everything initially should make
things slightly less scary/dangerous and give more time to figure out how to
expose the really powerful bits.
================================================
FILE: LICENSE.md
================================================
All Reports in this Repository are licensed by Contributors
under the
[W3C Software and Document License](http://www.w3.org/Consortium/Legal/2015/copyright-software-and-document).
Contributions to Specifications are made under the
[W3C CLA](https://www.w3.org/community/about/agreements/cla/).
Contributions to Test Suites are made under the
[W3C 3-clause BSD License](https://www.w3.org/Consortium/Legal/2008/03-bsd-license.html)
================================================
FILE: README.md
================================================
# File System Access API
View proposals in the [EXPLAINER](EXPLAINER.md) and the [spec](https://wicg.github.io/file-system-access/).
See [changes.md](changes.md) for a list of changes that were made to the API surface between what was available as an Origin Trial in Chrome 83 through Chrome 85, and what will be shipped in Chrome 86.
## Problem
Today, if a web site wants to create experiences involving local files (document editor, image compressor, etc.) they are at a disadvantage to native apps. A web site must ask the user to reopen a file every time they want to edit it. After opening, the site can only save changes by re downloading the file to the Downloads folder. A native app, by comparison, can maintain a most recently used list, auto save, and save files anywhere the user wants.
## Use cases
- Open local file to read
- Open local file to edit and then save
- Open local file to edit with auto save
- Create and save a new file
- Delete an existing file
- Read meta data about files
## Workarounds
- [FileSaver.js](https://github.com/eligrey/FileSaver.js/) polyfills `saveAs()` from the [W3C File API](https://www.w3.org/TR/FileAPI/), but files open in a new window instead of downloading on Safari 6.1+ and iOS.
- In Edge, Firefox, and Chrome developers can:
- Create a fake anchor element (`var a = document.createElement('a')`)
- Set `download` to the desired filename (`a.download = 'file.txt'`)
- Set `href` to a data URI or Blob URL (`a.href = URL.createObjectURL(blob)`)
- Fake a click on the anchor element (`a.click()`)
- Clean up if necessary (`URL.revokeObjectURL(a.href)`)
This is also the approach taken in the
[browser-fs-access](https://github.com/GoogleChromeLabs/browser-fs-access)
support library.
- Setting `window.location` to `'data:application/octet-stream' + data_stream`.
- Hidden Flash controls to display a “save as” dialog.
These methods are clunky and only support “save as” (and depending on the UA may automatically appear in Downloads without prompting the user for location). They do not support most recently used lists, auto save, save, or deleting a file.
================================================
FILE: changes.md
================================================
This file enumerates the changes that were made to the API surface between the Origin Trial as shipped in Chrome 83,
and what is or will be available in Chrome 86.
## File Picker API entry points
**Before (in Origin Trial)**
```javascript
let file1 = await window.chooseFileSystemEntries(
{type: 'open-file'});
let files = await window.chooseFileSystemEntries(
{type: 'open-file', multiple: true});
let file2 = await window.chooseFileSystemEntries(
{type: 'save-file'});
let dir = window.chooseFileSystemEntries(
{type: 'open-directory'});
```
**After (in Chrome M86)**
```javascript
let [file1] = await window.showOpenFilePicker();
let files = await window.showOpenFilePicker({multiple: true});
let file2 = await window.showSaveFilePicker();
let dir = await window.showDirectoryPicker();
```
## Specifying accepted file types
**Before (in Origin Trial)**
```javascript
await window.chooseFileSystemEntries({
accepts: [
{
description: 'Text Files',
mimeTypes: ['text/plain', 'text/html'],
extensions: ['txt', 'text', 'html', 'htm']
},
{
description: 'Images',
mimeTypes: ['image/*'],
extensions: ['png', 'gif', 'jpeg', 'jpg']
}
]
});
```
**After (in Chrome M86)**
```javascript
await window.showOpenFilePicker({
types: [
{
description: 'Text Files',
accept: {
'text/plain': ['.txt', '.text'],
'text/html': ['.html', '.htm']
}
},
{
description: 'Images',
accept: {
'image/*': ['.png', '.gif', '.jpeg', '.jpg']
}
}
]
});
```
## Determining if a handle is a file or directory
**Before (in Origin Trial)**
```javascript
if (handle.isFile) {
// handle is a file
} else if (handle.isDirectory) {
// handle is a directory
} else {
// can't happen
}
```
**After (in Chrome M86)**
```javascript
switch (handle.kind) {
case 'file':
// handle is a file
break;
case 'directory':
// handle is a directory
break;
default:
// can't happen
}
```
## Getting children of a directory
**Before (in Origin Trial)**
```javascript
let file1 = parent.getFile('name');
let file2 = parent.getFile('name2', {create: true});
let dir1 = parent.getDirectory('dir1');
let dir2 = parent.getDirectory('dir2', {create: true});
```
**After (in Chrome M86)**
```javascript
let file1 = parent.getFileHandle('name');
let file2 = parent.getFileHandle('name2', {create: true});
let dir1 = parent.getDirectoryHandle('dir1');
let dir2 = parent.getDirectoryHandle('dir2', {create: true});
```
## Directory iteration
**Before (in Origin Trial)**
```javascript
for await (let handle of parent.getEntries()) {
// Use handle and/or handle.name
}
```
**After (in Chrome M86)**
```javascript
for await (let handle of parent.values()) { /* ... */ }
for await (let [name, handle] of parent) { /* ... */ }
for await (let [name, handle] of parent.entries()) { /* ... */ }
for await (let name of parent.keys()) { /* ... */ }
```
## Changes to permissions
**Before (in Origin Trial)**
```javascript
await handle.queryPermission();
await handle.queryPermission({writable: false});
await handle.requestPermission();
await handle.requestPermission({writable: false});
```
**After (in Chrome M86)**
```javascript
await handle.queryPermission();
await handle.queryPermission({mode: 'read'});
await handle.requestPermission();
await handle.requestPermission({mode: 'read'});
```
**Before (in Origin Trial)**
```javascript
await handle.queryPermission({writable: true});
await handle.requestPermission({writable: true});
```
**After (in Chrome M86)**
```javascript
await handle.queryPermission({mode: 'readwrite'});
await handle.requestPermission({mode: 'readwrite'});
```
## Origin Private/Sandboxed File System
**Before (in Origin Trial)**
```javascript
let root = await FileSystemDirectoryHandle.getSystemDirectory(type: 'sandbox');
```
**After (in Chrome M86)**
```javascript
let root = await navigator.storage.getDirectory();
```
================================================
FILE: index.bs
================================================
<pre class=metadata>
Title: File System Access
Shortname: file-system-access
Abstract: This document extends the API in [[FS]] to enable developers to build
powerful web apps that interact with files on the user's local device.
It builds on [[FILE-API|File API]] for file reading capabilities, and adds new API
surface to enable modifying files, as well as working with directories.
Status: CG-DRAFT
ED: https://wicg.github.io/file-system-access/
Level: 1
Editor: Ming-Ying Chung, Google, mych@chromium.org, w3cid 139196
Former Editor: Austin Sullivan, Google, asully@chromium.org, w3cid 126126
Former Editor: Marijn Kruisselbrink, Google, mek@chromium.org, w3cid 72440
Group: WICG
Repository: wicg/file-system-access
Indent: 2
Complain About: accidental-2119 yes, missing-example-ids yes
Markup Shorthands: css no, markdown yes
</pre>
<pre class=link-defaults>
spec:webidl; type:dfn; text:resolve
</pre>
<pre class=anchors>
urlPrefix: https://tc39.github.io/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
urlPrefix: https://www.w3.org/TR/webdriver-bidi/; spec: webdriver-bidi
type: dfn; text: WebDriver BiDi file dialog opened; url: webdriver-bidi-file-dialog-opened
</pre>
<style>
.domintro dt {
font-family: Menlo, Consolas, "DejaVu Sans Mono", Monaco, monospace;
padding-top: 0.5em;
padding-bottom: 1em;
}
.domintro dt a {
color: inherit; border-bottom-style: none;
}
.domintro dt code {
font-size: inherit;
}
.domintro::before {
content: 'For web developers (non-normative)';
text-transform: initial;
}
</style>
# Introduction # {#introduction}
*This section is non-normative.*
This API enables developers to build powerful apps that interact with other
(non-Web) apps on the user's device via the device's file system. Prominent
examples of applications where users expect this functionality are IDEs,
photo and video editors, text editors, and more.
After a user grants a web app access, this API allows the app to read or save
changes directly to files and folders on the user's device. Beyond reading and
writing files, this API provides the ability to open a directory and enumerate
its contents. Additionally, web apps can use this API to store references to
files and directories they've been given access to, allowing the web apps to
later regain access to the same content without requiring the user to select the
same file again.
This API is similar to <a href="https://html.spec.whatwg.org/multipage/input.html#file-upload-state-(type=file)">`<input type=file>`</a>
and <a href="https://wicg.github.io/entries-api/#html-forms">`<input type=file webkitdirectory>`</a>
[[entries-api]]
in that user interaction happens through file and directory picker dialogs.
Unlike those APIs, this API is currently purely a javascript API, and
does not integrate with forms and/or input elements.
This API extends the API in [[FS]], which specifies a [=/bucket file system=]
which websites can get access to without having to first prompt the user for access.
# Files and Directories # {#files-and-directories}
## Concepts ## {#concepts}
A <dfn>valid suffix code point</dfn> is a [=code point=] that is [=ASCII alphanumeric=],
U+002B (+), or U+002E (.).
Note: These code points were chosen to support most pre-existing file formats. The vast
majority of file extensions are purely alphanumeric, but compound extensions (such as
`.tar.gz`) and extensions such as `.c++` for C++ source code are also fairly common,
hence the inclusion of + and . as allowed code points.
## Permissions ## {#permissions}
The <dfn for=PermissionName enum-value>"file-system"</dfn> [=powerful feature=]'s
permission-related algorithms and types are defined as follows:
: [=permission descriptor type=]
:: {{FileSystemPermissionDescriptor}}, defined as:
<xmp class=idl>
enum FileSystemPermissionMode {
"read",
"readwrite"
};
dictionary FileSystemPermissionDescriptor : PermissionDescriptor {
required FileSystemHandle handle;
FileSystemPermissionMode mode = "read";
};
</xmp>
: [=permission state constraints=]
:: <div algorithm="permission state constraints">
To determine [=permission state constraints=] for a {{FileSystemPermissionDescriptor}} |desc|,
run these steps:
1. Let |entry| be |desc|["{{FileSystemPermissionDescriptor/handle}}"]'s
[=FileSystemHandle/entry=].
1. If |entry| represents a [=/file system entry=] in a [=/bucket file system=],
this descriptor's [=permission state=] must always be
"{{PermissionState/granted}}".
1. Otherwise, if |entry|'s [=file system entry/parent=] is not null, this descriptor's [=permission state=] must be
equal to the [=permission state=] for a descriptor with the same {{FileSystemPermissionDescriptor/mode}},
and a {{FileSystemPermissionDescriptor/handle}} representing |entry|'s [=file system entry/parent=].
1. Otherwise, if |desc|["{{FileSystemPermissionDescriptor/mode}}"] is
"{{FileSystemPermissionMode/readwrite}}":
1. Let |read state| be the [=permission state=] for a descriptor
with the same {{FileSystemPermissionDescriptor/handle}},
but whose {{FileSystemPermissionDescriptor/mode}} is
"{{FileSystemPermissionMode/read}}".
1. If |read state| is not "{{PermissionState/granted}}", this descriptor's [=permission state=]
must be equal to |read state|.
Issue(whatwg/fs#101): Make these checks no longer associated with an entry.
: [=permission request algorithm=]
:: <div algorithm="permission request algorithm">
Given a {{FileSystemPermissionDescriptor}} |desc| and a {{PermissionStatus}} |status|,
run these steps:
1. Run the [=default permission query algorithm=] on |desc| and |status|.
1. If |status|'s {{PermissionStatus/state}} is not
"{{PermissionState/prompt}}", then abort these steps.
1. Let |settings| be |desc|["{{FileSystemPermissionDescriptor/handle}}"]'s
[=relevant settings object=].
1. Let |global| be |settings|'s [=environment settings object/global object=].
1. If |global| is not a {{Window}}, then
[=throw=] a "{{SecurityError}}" {{DOMException}}.
1. If |global| does not have [=transient activation=], then
[=throw=] a "{{SecurityError}}" {{DOMException}}.
1. If |settings|'s [=environment settings object/origin=]
is not [=same origin=] with |settings|'s [=top-level origin=], then
[=throw=] a "{{SecurityError}}" {{DOMException}}.
1. [=Request permission to use=] |desc|.
1. Run the [=default permission query algorithm=] on |desc| and |status|.
Issue(WICG/permissions-request#2): Ideally this user activation requirement would be defined upstream.
<div algorithm>
To <dfn lt="querying file system permission|query file system permission">query file system permission</dfn>
given a {{FileSystemHandle}} |handle| and a {{FileSystemPermissionMode}} |mode|, run these steps:
1. Let |desc| be a {{FileSystemPermissionDescriptor}}.
1. Set |desc|["{{PermissionDescriptor/name}}"] to
"{{PermissionName/file-system}}".
1. Set |desc|["{{FileSystemPermissionDescriptor/handle}}"] to |handle|.
1. Set |desc|["{{FileSystemPermissionDescriptor/mode}}"] to |mode|.
1. Return |desc|'s [=permission state=].
</div>
<div algorithm>
To <dfn lt="requesting file system permission|request file system permission">request file system permission</dfn>
given a {{FileSystemHandle}} |handle| and a {{FileSystemPermissionMode}} |mode|, run these steps:
1. Let |desc| be a {{FileSystemPermissionDescriptor}}.
1. Set |desc|["{{PermissionDescriptor/name}}"] to
"{{PermissionName/file-system}}".
1. Set |desc|["{{FileSystemPermissionDescriptor/handle}}"] to |handle|.
1. Set |desc|["{{FileSystemPermissionDescriptor/mode}}"] to |mode|.
1. Let |status| be the result of running <a spec=permissions>create a PermissionStatus</a> for |desc|.
1. Run the [=permission request algorithm=] for the
"{{PermissionName/file-system}}" feature, given |desc| and |status|.
1. Return |desc|'s [=permission state=].
</div>
Issue(119): Currently {{FileSystemPermissionMode}} can only be
"{{FileSystemPermissionMode/read}}" or "{{FileSystemPermissionMode/readwrite}}".
In the future we might want to add a "write" mode as well to support write-only
handles.
## The {{FileSystemHandle}} interface ## {#api-filesystemhandle}
<xmp class=idl>
dictionary FileSystemHandlePermissionDescriptor {
FileSystemPermissionMode mode = "read";
};
[Exposed=(Window,Worker), SecureContext, Serializable]
partial interface FileSystemHandle {
Promise<PermissionState> queryPermission(optional FileSystemHandlePermissionDescriptor descriptor = {});
Promise<PermissionState> requestPermission(optional FileSystemHandlePermissionDescriptor descriptor = {});
};
</xmp>
### The {{FileSystemHandle/queryPermission()}} method ### {#api-filesystemhandle-querypermission}
<div class="note domintro">
: |status| = await |handle| . {{FileSystemHandle/queryPermission()|queryPermission}}({ {{FileSystemHandlePermissionDescriptor/mode}} : "{{FileSystemPermissionMode/read}}" })
: |status| = await |handle| . {{FileSystemHandle/queryPermission()}}
: |status| = (await navigator.{{Navigator/permissions}}.{{Permissions/query()|query}}({ {{PermissionDescriptor/name}} : "{{PermissionName/file-system}}", {{FileSystemPermissionDescriptor/handle}} : |handle| })).{{PermissionStatus/state}}
:: Queries the current state of the read permission of this handle.
If this returns "{{PermissionState/prompt}}" the website will have to call
{{FileSystemHandle/requestPermission()}} before any operations on the
handle can be done.
If this returns "{{PermissionState/denied}}" any operations will reject.
Usually handles returned by the [=local file system handle factories=] will
initially return "{{PermissionState/granted}}" for their read permission
state, however other than through the user revoking permission, a handle
retrieved from IndexedDB is also likely to return
"{{PermissionState/prompt}}".
: |status| = await |handle| . {{FileSystemHandle/queryPermission()|queryPermission}}({ {{FileSystemHandlePermissionDescriptor/mode}} : "{{FileSystemPermissionMode/readwrite}}" })
: |status| = (await navigator.{{Navigator/permissions}}.{{Permissions/query()|query}}({ {{PermissionDescriptor/name}} : "{{PermissionName/file-system}}", {{FileSystemPermissionDescriptor/handle}} : |handle|, {{FileSystemPermissionDescriptor/mode}} : "{{FileSystemPermissionMode/readwrite}}" }).{{PermissionStatus/state}}
:: Queries the current state of the write permission of this handle.
If this returns "{{PermissionState/prompt}}", attempting to modify the
file or directory this handle represents will require user activation
and will result in a confirmation prompt being shown to the user.
However if the state of the read permission of this handle is also
"{{PermissionState/prompt}}" the website will need to call
{{FileSystemHandle/requestPermission()}}.
There is no automatic prompting for read access when attempting to
read from a file or directory.
</div>
Advisement: The integration with the permissions API's {{Permissions/query()}} method is not yet implemented in Chrome.
<div algorithm>
The <dfn method for=FileSystemHandle>queryPermission(|descriptor|)</dfn> method, when invoked, must run these steps:
1. Let |result| be [=a new promise=].
1. Run the following steps [=in parallel=]:
1. Let |state| be the result of [=querying file system permission=]
given <b>[=this=]</b> and
|descriptor|["{{FileSystemHandlePermissionDescriptor/mode}}"].
1. [=/Resolve=] |result| with |state|.
1. Return |result|.
</div>
### The {{FileSystemHandle/requestPermission()}} method ### {#api-filesystemhandle-requestpermission}
<div class="note domintro">
: |status| = await |handle| . {{FileSystemHandle/requestPermission()|requestPermission}}({ {{FileSystemHandlePermissionDescriptor/mode}} : "{{FileSystemPermissionMode/read}}" })
: |status| = await |handle| . {{FileSystemHandle/requestPermission()}}
:: If the state of the read permission of this handle is anything other than
"{{PermissionState/prompt}}", this will return that state directly.
If it is "{{PermissionState/prompt}}" however, user activation is needed and
this will show a confirmation prompt to the user.
The new read permission state is then returned, depending on
the user's response to the prompt.
: |status| = await |handle| . {{FileSystemHandle/requestPermission()|requestPermission}}({ {{FileSystemHandlePermissionDescriptor/mode}} : "{{FileSystemPermissionMode/readwrite}}" })
:: If the state of the write permission of this handle is anything other than
"{{PermissionState/prompt}}", this will return that state directly.
If the status of the read permission of this handle is
"{{PermissionState/denied}}" this will return that.
Otherwise the state of the write permission is "{{PermissionState/prompt}}"
and this will show a confirmation prompt to the user.
The new write permission state is then returned, depending on
what the user selected.
</div>
<div algorithm>
The <dfn method for=FileSystemHandle>requestPermission(|descriptor|)</dfn> method, when invoked, must run these steps:
1. Let |result| be [=a new promise=].
1. Run the following steps [=in parallel=]:
1. Let |state| be the result of [=requesting file system permission=]
given <b>[=this=]</b> and
|descriptor|["{{FileSystemHandlePermissionDescriptor/mode}}"].
If that throws an exception, [=/reject=] |result| with that exception and abort.
1. [=/Resolve=] |result| with |state|.
1. Return |result|.
</div>
# Accessing Local File System # {#local-filesystem}
<xmp class=idl>
enum WellKnownDirectory {
"desktop",
"documents",
"downloads",
"music",
"pictures",
"videos",
};
typedef (WellKnownDirectory or FileSystemHandle) StartInDirectory;
dictionary FilePickerAcceptType {
USVString description = "";
record<USVString, (USVString or sequence<USVString>)> accept;
};
dictionary FilePickerOptions {
sequence<FilePickerAcceptType> types;
boolean excludeAcceptAllOption = false;
DOMString id;
StartInDirectory startIn;
};
dictionary OpenFilePickerOptions : FilePickerOptions {
boolean multiple = false;
};
dictionary SaveFilePickerOptions : FilePickerOptions {
USVString? suggestedName;
};
dictionary DirectoryPickerOptions {
DOMString id;
StartInDirectory startIn;
FileSystemPermissionMode mode = "read";
};
[SecureContext]
partial interface Window {
Promise<sequence<FileSystemFileHandle>> showOpenFilePicker(optional OpenFilePickerOptions options = {});
Promise<FileSystemFileHandle> showSaveFilePicker(optional SaveFilePickerOptions options = {});
Promise<FileSystemDirectoryHandle> showDirectoryPicker(optional DirectoryPickerOptions options = {});
};
</xmp>
The {{showOpenFilePicker()}}, {{showSaveFilePicker()}} and {{showDirectoryPicker()}} methods
are together known as the <dfn>local file system handle factories</dfn>.
Note: What is referred to as the "local file system" in this spec, does not have to
strictly refer to the file system on the local device. What we call the local file system
could just as well be backed by a cloud provider. For example on Chrome OS these
file pickers will also let you pick files and directories on Google Drive.
Advisement: In Chrome versions earlier than 85, this was implemented as a generic `chooseFileSystemEntries` method.
## Local File System Permissions ## {#local-file-system-permissions}
The fact that the user picked the specific files returned by the [=local file system handle factories=] in a prompt
should be treated by the user agent as the user intending to grant read access to the website
for the returned files. As such, at the time the promise returned by one of the [=local file system handle factories=]
resolves, [=permission state=] for a descriptor with {{FileSystemPermissionDescriptor/handle}} set to the returned handle,
and {{FileSystemPermissionDescriptor/mode}} set to "{{FileSystemPermissionMode/read}}"
should be "{{PermissionState/granted}}".
Additionally for calls to {{showSaveFilePicker}}
the [=permission state=] for a descriptor with {{FileSystemPermissionDescriptor/handle}} set to the returned handle,
and {{FileSystemPermissionDescriptor/mode}} set to "{{FileSystemPermissionMode/readwrite}}"
should be "{{PermissionState/granted}}".
<div algorithm>
To verify that an |environment| <dfn>is allowed to show a file picker</dfn>, run these steps:
1. If |environment|'s [=environment settings object/origin=] is an [=opaque origin=],
return [=a promise rejected with=] a "{{SecurityError}}" {{DOMException}}.
1. If |environment|'s [=environment settings object/origin=] is not [=same origin=] with
|environment|'s [=top-level origin=],
return [=a promise rejected with=] a "{{SecurityError}}" {{DOMException}}.
1. Let |global| be |environment|'s [=environment settings object/global object=].
1. If |global| does not have [=transient activation=], then
[=throw=] a "{{SecurityError}}" {{DOMException}}.
</div>
## File picker options ## {#api-filepickeroptions}
### Accepted file types ### {#api-filepickeroptions-types}
<div class="note domintro">
The {{showOpenFilePicker(options)}} and {{showSaveFilePicker(options)}} methods accept a
{{FilePickerOptions}} argument, which lets the website specify the types of files
the file picker will let the user select.
Each entry in {{FilePickerOptions/types}} specifies a single user selectable option
for filtering the files displayed in the file picker.
Each option consists of an <span class=allow-2119>optional</span> {{FilePickerAcceptType/description}}
and a number of MIME types and extensions (specified as a mapping of
MIME type to a list of extensions). If no description is provided one will be generated.
Extensions have to be strings that start with a "." and only contain [=valid suffix code points=].
Additionally extensions are limited to a length of 16 code points.
In addition to complete MIME types, "\*" can be used as the subtype of a MIME type to match
for example all image formats with "image/\*".
Websites <span class=allow-2119>should</span> always provide both MIME types and file
extensions for each option. On platforms that only use file extensions to describe file types
user agents can match on the extensions, while on platforms that don't use extensions,
user agents can match on MIME type.
By default the file picker will also include an option to not apply any filter,
letting the user select any file. Set {{excludeAcceptAllOption}} to `true` to not
include this option in the file picker.
For example , the following options will let the user pick one of three different filters.
One for text files (either plain text or HTML), one for images, and a third one that doesn't apply
any filter and lets the user select any file.
<pre class=example id="filepickeroptions-types-example1" highlight=js>
const options = {
<l>{{FilePickerOptions/types}}</l>: [
{
<l>{{FilePickerAcceptType/description}}</l>: 'Text Files',
<l>{{FilePickerAcceptType/accept}}</l>: {
'text/plain': ['.txt', '.text'],
'text/html': ['.html', '.htm']
}
},
{
<l>{{FilePickerAcceptType/description}}</l>: 'Images',
<l>{{FilePickerAcceptType/accept}}</l>: {
'image/*': ['.png', '.gif', '.jpeg', '.jpg']
}
}
],
};
</pre>
On the other hand, the following example will only let the user select SVG files. The dialog
will not show an option to not apply any filters.
<pre class=example id="filepickeroptions-types-example2" highlight=js>
const options = {
<l>{{FilePickerOptions/types}}</l>: [
{
<l>{{FilePickerAcceptType/accept}}</l>: {
'image/svg+xml': '.svg'
}
},
],
<l>{{FilePickerOptions/excludeAcceptAllOption}}</l>: true
};
</pre>
</div>
<div algorithm>
To <dfn>process accept types</dfn>, given {{FilePickerOptions}} |options|,
run these steps:
1. Let |accepts options| be a empty [=/list=] of [=tuples=]
consisting of a description and a filter.
1. [=list/For each=] |type| of |options|["{{FilePickerOptions/types}}"]:
1. [=map/For each=] |typeString| → |suffixes| of
|type|["{{FilePickerAcceptType/accept}}"]:
1. Let |parsedType| be the result of [=parse a MIME type=] with |typeString|.
1. If |parsedType| is failure, then [=throw=] a {{TypeError}}.
1. If |parsedType|'s [=MIME type/parameters=] are not empty, then
[=throw=] a {{TypeError}}.
1. If |suffixes| is a string:
1. [=Validate a suffix=] given |suffixes|.
1. Otherwise, [=list/for each=] |suffix| of |suffixes|:
1. [=Validate a suffix=] given |suffix|.
1. Let |filter| be these steps, given a |filename| (a [=string=]) and a |type| (a [=MIME type=]):
1. [=map/For each=] |typeString| → |suffixes| of
|type|["{{FilePickerAcceptType/accept}}"]:
1. Let |parsedType| be the result of [=parse a MIME type=] with |typeString|.
1. If |parsedType|'s [=MIME type/subtype=] is "*":
1. If |parsedType|'s [=MIME type/type=] is "*", return `true`.
1. If |parsedType|'s [=MIME type/type=] is |type|'s [=MIME type/type=], return `true`.
1. |parsedType|'s [=MIME type/essence=] is |type|'s [=MIME type/essence=], return `true`.
1. If |suffixes| is a string, set |suffixes| to « |suffixes| ».
1. [=list/For each=] |suffix| of |suffixes|:
1. If |filename| ends with |suffix|, return `true`.
1. Return `false`.
1. Let |description| be |type|["{{FilePickerAcceptType/description}}"].
1. If |description| is an empty string,
set |description| to some user understandable string describing |filter|.
1. [=list/Append=] (|description|, |filter|) to |accepts options|.
1. If either |accepts options| is [=list/empty=],
or |options|["{{FilePickerOptions/excludeAcceptAllOption}}"] is `false`:
1. Let |description| be a user understandable string describing "all files".
1. Let |filter| be an algorithm that returns `true`.
1. [=list/Append=] (|description|, |filter|) to |accepts options|.
1. If |accepts options| is empty, then [=throw=] a {{TypeError}}.
1. Return |accepts options|.
</div>
<div algorithm>
To <dfn>validate a suffix</dfn> |suffix|, run the following steps:
1. If |suffix| does not [=string/starts with|start with=] ".", then
[=throw=] a {{TypeError}}.
1. If |suffix| contains any [=code points=] that are not
[=valid suffix code points=], then [=throw=] a {{TypeError}}.
1. If |suffix| ends with ".", then [=throw=] a {{TypeError}}.
1. If |suffix|'s [=string/length=] is more than 16, then
[=throw=] a {{TypeError}}.
</div>
### Starting Directory ### {#api-filepickeroptions-starting-directory}
<div class="note domintro">
The {{FilePickerOptions/id}} and {{FilePickerOptions/startIn}} fields can be specified to suggest
the directory in which the file picker opens.
If neither of these options are specified, the user agent remembers the last directory a file
or directory was picked from, and new pickers will start out in that directory. By specifying an
{{FilePickerOptions/id}} the user agent can remember different directories for different IDs
(user agents will only remember directories for a limited number of IDs).
<pre class=example id="filepickeroptions-starting-directory-example1" highlight=js>
// If a mapping exists from this ID to a previousy picked directory, start in
// this directory. Otherwise, a mapping will be created from this ID to the
// directory of the resulting file picker invocation.
const options = {
<l>{{FilePickerOptions/id}}</l>: 'foo',
};
</pre>
Specifying {{FilePickerOptions/startIn}} as a {{FileSystemFileHandle}} will result in the dialog
starting in the parent directory of that file, while passing in a {{FileSystemDirectoryHandle}}
will result in the dialog to start in the passed in directory. These take precedence even if
an explicit {{FilePickerOptions/id}} is also passed in.
For example, given a {{FileSystemDirectoryHandle}} <var>project_dir</var>, the following will show
a file picker that starts out in that directory:
<pre class=example id="filepickeroptions-starting-directory-example2" highlight=js>
// The picker will open to the directory of |project_dir| regardless of whether
// 'foo' has a valid mapping.
const options = {
<l>{{FilePickerOptions/id}}</l>: 'foo',
<l>{{FilePickerOptions/startIn}}</l>: |project_dir|,
};
</pre>
The {{FilePickerOptions/id}} and {{FilePickerOptions/startIn}} fields control only
the directory the picker opens to. In the above example, it cannot be assumed that
the {{FilePickerOptions/id}} 'foo' will map to the same directory as |project_dir|
once the file picker operation has completed.
Specifying {{FilePickerOptions/startIn}} as a {{WellKnownDirectory}} will result in the dialog
starting in that directory, unless an explicit {{FilePickerOptions/id}} was also passed
in which has a mapping to a valid directory.
Below is an example of specifying both an {{FilePickerOptions/id}} and
{{FilePickerOptions/startIn}} as a {{WellKnownDirectory}}. If there is an existing
mapping from the given ID to a path, this mapping is used. Otherwise, the path suggested
via the {{WellKnownDirectory}} is used.
<pre class=example id="filepickeroptions-starting-directory-example3" highlight=js>
// First time specifying the ID 'foo'. It is not mapped to a directory.
// The file picker will fall back to opening to the Downloads directory. TODO: link this.
const options = {
<l>{{FilePickerOptions/id}}</l>: 'foo', // Unmapped.
<l>{{FilePickerOptions/startIn}}</l>: "<l>{{WellKnownDirectory/downloads}}</l>", // Start here.
};
// Later...
// The ID 'foo' might or might not be mapped. For example, the mapping for this ID
// might have been evicted.
const options = {
<l>{{FilePickerOptions/id}}</l>: 'foo', // Maybe mapped. If so, start here.
<l>{{FilePickerOptions/startIn}}</l>: "<l>{{WellKnownDirectory/downloads}}</l>", // Otherwise, start here.
};
</pre>
</div>
Advisement: The {{FilePickerOptions/startIn}} and {{FilePickerOptions/id}} options were first introduced in Chrome 91.
A user agent holds a <dfn>recently picked directory map</dfn>, which is a
[=map=] of [=/origins=] to [=path id maps=].
A <dfn>path id map</dfn> is a [=map=] of [=valid path ids=] to paths.
A <dfn>valid path id</dfn> is a [=string=] where each character is [=ASCII alphanumeric=] or "_" or "-".
To prevent a [=path id map=] from growing without a bound, user agents should implement
some mechanism to limit how many recently picked directories will be remembered. This
can for example be done by evicting least recently used entries.
User agents should allow at least 16 entries to be stored in a [=path id map=].
The <dfn enum>WellKnownDirectory</dfn> enum lets a website pick one of several well-known
directories. The exact paths the various values of this enum map to is [=implementation-defined=]
(and in some cases these might not even represent actual paths on disk).
The following list describes the meaning of each of the values, and gives possible example paths on different operating systems:
<dl dfn-for=WellKnownDirectory>
: <dfn enum-value>"desktop"</dfn>
:: The user's Desktop directory, if such a thing exists. For example this could be
`C:\Documents and Settings\username\Desktop`, `/Users/username/Desktop`, or `/home/username/Desktop`.
: <dfn enum-value>"documents"</dfn>
:: Directory in which documents created by the user would typically be stored.
For example `C:\Documents and Settings\username\My Documents`, `/Users/username/Documents`, or `/home/username/Documents`.
: <dfn enum-value>"downloads"</dfn>
:: Directory where downloaded files would typically be stored.
For example `C:\Documents and Settings\username\Downloads`, `/Users/username/Downloads`, or `/home/username/Downloads`.
: <dfn enum-value>"music"</dfn>
:: Directory where audio files would typically be stored.
For example `C:\Documents and Settings\username\My Documents\My Music`, `/Users/username/Music`, or `/home/username/Music`.
: <dfn enum-value>"pictures"</dfn>
:: Directory where photos and other still images would typically be stored.
For example `C:\Documents and Settings\username\My Documents\My Pictures`, `/Users/username/Pictures`, or `/home/username/Pictures`.
: <dfn enum-value>"videos"</dfn>
:: Directory where videos/movies would typically be stored.
For example `C:\Documents and Settings\username\My Documents\My Videos`, `/Users/username/Movies`, or `/home/username/Videos`.
<div algorithm>
To <dfn>determine the directory the picker will start in</dfn>, given an optional [=string=] |id|,
an optional {{StartInDirectory}} |startIn| and an [=environment settings object=] |environment|,
run the following steps:
1. If |id| given, and is not a [=valid path id=], then
[=throw=] a {{TypeError}}.
1. If |id|'s [=string/length=] is more than 32, then
[=throw=] a {{TypeError}}.
1. Let |origin| be |environment|'s [=environment settings object/origin=].
1. If |startIn| is a {{FileSystemHandle}} and |startIn| is not
[=FileSystemHandle/is in a bucket file system|in a bucket file system=]:
1. Let |entry| be the result of [=locating an entry=] given
|startIn|'s [=FileSystemHandle/locator=].
1. If |entry| is a [=file entry=], return the path of
|entry|'s [=file system entry/parent=] in the local file system.
1. If |entry| is a [=directory entry=], return
|entry|'s path in the local file system.
1. If |id| is non-empty:
1. If [=recently picked directory map=][|origin|] [=map/exists=]:
1. Let |path map| be [=recently picked directory map=][|origin|].
1. If |path map|[|id|] [=map/exists=], then return |path map|[|id|].
1. If |startIn| is a {{WellKnownDirectory}}:
1. Return a user agent defined path corresponding to the {{WellKnownDirectory}} value of |startIn|.
1. If |id| is not specified, or is an empty string:
1. If [=recently picked directory map=][|origin|] [=map/exists=]:
1. Let |path map| be [=recently picked directory map=][|origin|].
1. If |path map|[""] [=map/exists=], then return |path map|[""].
1. Return a default path in a user agent specific manner.
</div>
<div algorithm>
To <dfn>remember a picked directory</dfn>, given an optional [=string=] |id|,
a [=/file system entry=] |entry|, and an [=environment settings object=] |environment|,
run the following steps:
1. Let |origin| be |environment|'s [=environment settings object/origin=].
1. If [=recently picked directory map=][|origin|] does not [=map/exist=],
then set [=recently picked directory map=][|origin|] to an empty [=path id map=].
1. If |id| is not specified, let |id| be an empty string.
1. Set [=recently picked directory map=][|origin|][|id|] to the path on the local file system corresponding to |entry|,
if such a path can be determined.
</div>
## The {{showOpenFilePicker()}} method ## {#api-showopenfilepicker}
<div class="note domintro">
: [ |handle| ] = await window . {{showOpenFilePicker()}}
: [ |handle| ] = await window . {{showOpenFilePicker()|showOpenFilePicker}}({ {{OpenFilePickerOptions/multiple}}: false })
:: Shows a file picker that lets a user select a single existing file, returning a handle for
the selected file.
: handles = await window . {{showOpenFilePicker()|showOpenFilePicker}}({ {{OpenFilePickerOptions/multiple}}: true })
:: Shows a file picker that lets a user select multiple existing files, returning handles for
the selected files.
Additional options can be passed to {{showOpenFilePicker()}} to indicate the types of files
the website wants the user to select and the directory in which the
file picker will open. See [[#api-filepickeroptions]] for details.
</div>
<div algorithm>
The <dfn method for=Window>showOpenFilePicker(|options|)</dfn> method, when invoked, must run
these steps:
1. Let |environment| be <b>[=this=]</b>'s [=relevant settings object=].
1. Let |accepts options| be the result of [=process accept types|processing accept types=] given |options|.
1. Let |starting directory| be the result of
[=determine the directory the picker will start in|determining the directory the picker will start in=]
given |options|["{{FilePickerOptions/id}}"],
|options|["{{FilePickerOptions/startIn}}"], and |environment|.
1. Let |global| be |environment|'s [=environment settings object/global object=].
1. Verify that |environment| [=is allowed to show a file picker=].
1. Let |p| be [=a new promise=].
1. Run the following steps [=in parallel=]:
1. Optionally, wait until any prior execution of this algorithm has terminated.
1. Let |filePickerOptions| be an empty [=/map=].
1. Set |filePickerOptions|["multiple"] to |options|["{{OpenFilePickerOptions/multiple}}"].
1. Let |dismissed| be the result of [=WebDriver BiDi file dialog opened=] with null and |filePickerOptions|.
1. If |dismissed| is false:
1. Display a prompt to the user requesting that the user pick some files.
If |filePickerOptions|["multiple"] is false, there must be no more than one file selected;
otherwise any number may be selected.
The displayed prompt should let the user pick one of the |accepts options| to filter the list of displayed files.
Exactly how this is implemented, and what this prompt looks like is [=implementation-defined=].
When possible, this prompt should start out showing |starting directory|.
1. Wait for the user to have made their selection.
1. If |dismissed| is true or if the user dismissed the prompt without making a selection,
[=/reject=] |p| with an "{{AbortError}}" {{DOMException}} and abort.
1. Let |entries| be a [=/list=] of [=file entries=] representing the selected files or directories.
1. Let |result| be a empty [=/list=].
1. [=list/For each=] |entry| of |entries|:
1. If |entry| is deemed [=too sensitive or dangerous=] to be exposed to this website by the user agent:
1. Inform the user that the selected files or directories can't be exposed to this website.
1. At the discretion of the user agent,
either go back to the beginning of these [=in parallel=] steps,
or [=/reject=] |p| with an "{{AbortError}}" {{DOMException}} and abort.
1. Add a new {{FileSystemFileHandle}} associated with |entry| to |result|.
1. [=Remember a picked directory=] given
|options|["{{FilePickerOptions/id}}"], |entries|[0] and |environment|.
1. Perform the <a spec=html>activation notification</a> steps in |global|'s [=Window/browsing context=].
Note: This lets a website immediately perform operations on the returned handles that
might require user activation, such as requesting more permissions.
1. [=/Resolve=] |p| with |result|.
1. Return |p|.
</div>
## The {{Window/showSaveFilePicker()}} method ## {#api-showsavefilepicker}
<div class="note domintro">
: |handle| = await window . {{showSaveFilePicker()|showSaveFilePicker}}( |options| )
:: Shows a file picker that lets a user select a single file, returning a handle for
the selected file. The selected file does not have to exist already. If the selected
file does not exist a new empty file is created before this method returns, otherwise
the existing file is cleared before this method returned.
: |handle| = await window . {{showSaveFilePicker()|showSaveFilePicker}}({ {{SaveFilePickerOptions/suggestedName}}: "README.md" })
:: Shows a file picker with the suggested "README.md" file name pre-filled as the default file name to save as.
Additional |options| can be passed to {{showSaveFilePicker()}} to indicate the types of files
the website wants the user to select and the directory in which the
file picker will open. See [[#api-filepickeroptions]] for details.
</div>
Advisement: The {{SaveFilePickerOptions/suggestedName}} option was first introduced in Chrome 91.
<div algorithm>
The <dfn method for=Window>showSaveFilePicker(|options|)</dfn> method, when invoked, must run
these steps:
1. Let |environment| be <b>[=this=]</b>'s [=relevant settings object=].
1. Let |accepts options| be the result of [=process accept types|processing accept types=] given |options|.
1. Let |starting directory| be the result of
[=determine the directory the picker will start in|determining the directory the picker will start in=]
given |options|["{{FilePickerOptions/id}}"],
|options|["{{FilePickerOptions/startIn}}"] and |environment|.
1. Let |global| be |environment|'s [=environment settings object/global object=].
1. Verify that |environment| [=is allowed to show a file picker=].
1. Let |p| be [=a new promise=].
1. Run the following steps [=in parallel=]:
1. Optionally, wait until any prior execution of this algorithm has terminated.
1. Display a prompt to the user requesting that the user pick exactly one file.
The displayed prompt should let the user pick one of the |accepts options| to filter the list of displayed files.
Exactly how this is implemented, and what this prompt looks like is [=implementation-defined=].
If |accepts options| are displayed in the UI, the selected option should also be used to suggest an extension
to append to a user provided file name, but this is not required. In particular user agents are free to ignore
potentially dangerous suffixes such as those ending in `".lnk"` or `".local"`.
When possible, this prompt should start out showing |starting directory|.
If |options|["{{SaveFilePickerOptions/suggestedName}}"] [=map/exists=] and
is not null, the file picker prompt will be pre-filled with the
|options|["{{SaveFilePickerOptions/suggestedName}}"] as the default name
to save as. The interaction between the
{{SaveFilePickerOptions/suggestedName}} and |accepts options| is [=implementation-defined=].
If the {{SaveFilePickerOptions/suggestedName}} is deemed too dangerous, user agents should ignore or sanitize the
suggested file name, similar to the sanitization done when fetching something <a spec=html>as a download</a>.
Note: A user agent could for example pick whichever option in |accepts options| that matches
{{SaveFilePickerOptions/suggestedName}} as the default filter.
1. Wait for the user to have made their selection.
1. If the user dismissed the prompt without making a selection,
[=/reject=] |p| with an "{{AbortError}}" {{DOMException}} and abort.
1. Let |entry| be a [=file entry=] representing the selected file.
1. If |entry| is deemed [=too sensitive or dangerous=] to be exposed to this website by the user agent:
1. Inform the user that the selected files or directories can't be exposed to this website.
1. At the discretion of the user agent,
either go back to the beginning of these [=in parallel=] steps,
or [=/reject=] |p| with an "{{AbortError}}" {{DOMException}} and abort.
1. Set |entry|'s [=binary data=] to an empty [=byte sequence=].
1. Set |result| to a new {{FileSystemFileHandle}} associated with |entry|.
1. [=Remember a picked directory=] given
|options|["{{FilePickerOptions/id}}"], |entry| and |environment|.
1. Perform the <a spec=html>activation notification</a> steps in |global|'s [=Window/browsing context=].
Note: This lets a website immediately perform operations on the returned handles that
might require user activation, such as requesting more permissions.
1. [=/Resolve=] |p| with |result|.
1. Return |p|.
</div>
## The {{Window/showDirectoryPicker()}} method ## {#api-showdirectorypicker}
<div class="note domintro">
: |handle| = await window . {{Window/showDirectoryPicker()}}
: |handle| = await window . {{Window/showDirectoryPicker()}}({ {{DirectoryPickerOptions/mode}}: 'read' })
:: Shows a directory picker that lets the user select a single directory, returning a handle for
the selected directory if the user grants read permission.
: |handle| = await window . {{Window/showDirectoryPicker()}}({ {{DirectoryPickerOptions/mode}}: 'readwrite' })
:: Shows a directory picker that lets the user select a single directory, returning a handle for
the selected directory. The user agent can combine read and write permission requests on this handle into
one subsequent prompt.
The {{DirectoryPickerOptions/id}} and {{DirectoryPickerOptions/startIn}} fields behave
identically to the {{FilePickerOptions/id}} and {{FilePickerOptions/startIn}} fields, respectively.
See [[#api-filepickeroptions-starting-directory]] for details on how to use these fields.
</div>
<div algorithm>
The <dfn method for=Window>showDirectoryPicker(<var>options</var>)</dfn> method, when invoked, must run
these steps:
1. Let |environment| be <b>[=this=]</b>'s [=relevant settings object=].
1. Let |starting directory| be the result of [=determine the directory the picker will start in|determining the directory the picker will start in=]
given |options|["{{DirectoryPickerOptions/id}}"],
|options|["{{DirectoryPickerOptions/startIn}}"] and |environment|.
1. Let |global| be |environment|'s [=environment settings object/global object=].
1. Verify that |environment| [=is allowed to show a file picker=].
1. Let |p| be [=a new promise=].
1. Run the following steps [=in parallel=]:
1. Optionally, wait until any prior execution of this algorithm has terminated.
1. Let |filePickerOptions| be an empty [=/map=].
1. Set |filePickerOptions|["multiple"] to false.
1. Let |dismissed| be the result of [=WebDriver BiDi file dialog opened=] with null and |filePickerOptions|.
1. If |dismissed| is false:
1. Display a prompt to the user requesting that the user pick a directory.
When possible, this prompt should start out showing |starting directory|.
1. Wait for the user to have made their selection.
1. If |dismissed| is true or if the user dismissed the prompt without making a selection,
[=/reject=] |p| with an "{{AbortError}}" {{DOMException}} and abort.
1. Let |entry| be a [=directory entry=] representing the selected directory.
1. If |entry| is deemed [=too sensitive or dangerous=] to be exposed to this website by the user agent:
1. Inform the user that the selected files or directories can't be exposed to this website.
1. At the discretion of the user agent,
either go back to the beginning of these [=in parallel=] steps,
or [=/reject=] |p| with an "{{AbortError}}" {{DOMException}} and abort.
1. Set |result| to a new {{FileSystemDirectoryHandle}} associated with |entry|.
1. [=Remember a picked directory=] given
|options|["{{DirectoryPickerOptions/id}}"], |entry| and |environment|.
1. Let |desc| be a {{FileSystemPermissionDescriptor}}.
1. Set |desc|["{{PermissionDescriptor/name}}"] to
"{{PermissionName/file-system}}".
1. Set |desc|["{{FileSystemPermissionDescriptor/handle}}"] to |result|.
1. Set |desc|["{{FileSystemPermissionDescriptor/mode}}"] to
|options|["{{DirectoryPickerOptions/mode}}"].
1. Let |status| be the result of running <a spec=permissions>create a PermissionStatus</a> for |desc|.
1. Perform the <a spec=html>activation notification</a> steps in |global|'s [=Window/browsing context=].
1. [=Request permission to use=] |desc|.
1. Run the [=default permission query algorithm=] on |desc| and |status|.
1. If |status| is not "{{PermissionState/granted}}",
[=/reject=] |result| with a "{{AbortError}}" {{DOMException}} and abort.
1. Perform the <a spec=html>activation notification</a> steps in |global|'s [=Window/browsing context=].
1. [=/Resolve=] |p| with |result|.
1. Return |p|.
</div>
## Drag and Drop ## {#drag-and-drop}
<xmp class=idl>
partial interface DataTransferItem {
Promise<FileSystemHandle?> getAsFileSystemHandle();
};
</xmp>
During a <em>drag-and-drop operation</em>, dragged file and
directory items are associated with [=file entries=] and [=directory entries=]
respectively.
<div class="note domintro">
: |handle| = await item . {{getAsFileSystemHandle()}}
:: Returns a {{FileSystemFileHandle}} object if the dragged item is a file and a {{FileSystemDirectoryHandle}} object if the dragged item is a directory.
</div>
<div algorithm>
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
[=a promise resolved with=] `null`.
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=].
1. Run the following steps [=in parallel=]:
1. Let |entry| be the [=/file system entry=] representing the dragged file or directory.
1. If |entry| is a [=file entry=]:
1. Let |handle| be a {{FileSystemFileHandle}} associated with |entry|.
1. Else if |entry| is a [=directory entry=]:
1. Let |handle| be a {{FileSystemDirectoryHandle}} associated with |entry|.
1. [=/Resolve=] |p| with |entry|.
1. Return |p|.
</div>
<div class=example id=draganddrop-example>
Handling drag and drop of files and directories:
<xmp highlight=js>
elem.addEventListener('dragover', (e) => {
// Prevent navigation.
e.preventDefault();
});
elem.addEventListener('drop', async (e) => {
e.preventDefault();
const fileHandlesPromises = [...e.dataTransfer.items]
.filter(item => item.kind === 'file')
.map(item => item.getAsFileSystemHandle());
for await (const handle of fileHandlesPromises) {
if (handle.kind === 'directory') {
console.log(`Directory: ${handle.name}`);
} else {
console.log(`File: ${handle.name}`);
}
}
});
</xmp>
</div>
Issue: This currently does not block access to [=too sensitive or dangerous=] directories, to
be consistent with other APIs that give access to dropped files and directories. This is inconsistent
with the [=local file system handle factories=] though, so we might want to reconsider this.
# Accessibility Considerations # {#accessibility-considerations}
*This section is non-normative.*
When this specification is used to present information in the user interface,
implementors will want to follow the OS level accessibility guidelines for the platform.
# Privacy Considerations # {#privacy-considerations}
*This section is non-normative.*
This API does not give websites any more read access to data than the existing `<input type=file>`
and `<input type=file webkitdirectory>` APIs already do. Furthermore similarly to those APIs, all
access to files and directories is explicitly gated behind a file or directory picker.
There are however several major privacy risks with this new API:
## Users giving access to more, or more sensitive files than they intended. ## {#privacy-wide-access}
This isn't a new risk with this API, but user agents should try to make sure that users are aware
of what exactly they're giving websites access to. This is particularly important when giving
access to a directory, where it might not be immediately clear to a user just how many files
actually exist in that directory.
A related risk is having a user give access to particularly sensitive data. This
could include some of a user agent's configuration data, network cache or cookie store,
or operating system configuration data such as password files. To protect against this, user agents
are encouraged to restrict which directories a user is allowed to select in a directory picker,
and potentially even restrict which files the user is allowed to select. This will make it much
harder to accidentally give access to a directory that contains particularly sensitive data. Care
must be taken to strike the right balance between restricting what the API can access while still
having the API be useful. After all, this API intentionally lets the user use websites to interact
with some of their most private personal data.
Examples of directories that user agents might want to restrict as being
<dfn>too sensitive or dangerous</dfn> include:
* The directory or directories containing the user agent itself.
* Directories where the user agent stores [=storage|website storage=].
* Directories containing system files (such as `C:\Windows` on Windows).
* Directories such as `/dev/`, `/sys`, and `/proc` on Linux that would give access to low-level devices.
* A user's entire "home" directory.
Individual files and directories inside the home directory should still be allowed,
but user agents should not generally let users give blanket access to the entire directory.
* The default directory for downloads, if the user agent has such a thing.
Individual files inside the directory again should be allowed, but the whole directory would risk leaking more data than a user realizes.
* Files with names that end in `.lnk`, when selecting a file to write to. Writing to
these files on Windows is similar to creating symlinks on other operating systems,
and as such can be used to attempt to trick users into giving access to files they didn't intend to expose.
* Files with names that end in `.local`, when selecting a file to write to.
Windows uses these files to decide what DLLs to load, and as such writing to
these files could be used to cause code to be executed.
## Websites trying to use this API for tracking. ## {#privacy-tracking}
This API could be used by websites to track the user across clearing browsing
data. This is because, in contrast with existing file access APIs, user agents are
able to grant persistent access to files or directories and can re-prompt. In
combination with the ability to write to files, websites will be able to persist an
identifier on the users' disk. Clearing browsing data will not affect those files
in any way, making these identifiers persist through those actions.
This risk is somewhat mitigated by the fact that clearing browsing data will clear all handles
that a website had persisted (for example in IndexedDB),
so websites won't have any handles to re-prompt for permission after browsing data was cleared.
Furthermore user agents are encouraged to make it clear what files and directories a website has
access to, and to automatically expire permission grants except for particularly well trusted
origins (for example persistent permissions could be limited to "installed" web applications).
User agents also are encouraged to provide a way for users to revoke permissions granted.
Clearing browsing data is expected to revoke all permissions as well.
## First-party vs third-party contexts. ## {#privacy-third-party}
In third-party contexts (e.g. an iframe whose origin does not match that of the top-level frame)
websites can't gain access to data they don't already have access to. This includes both getting
access to new files or directories via the [=local file system handle factories=], as well as requesting
more permissions to existing handles via the {{requestPermission}} API.
Handles can also only be post-messaged to same-origin destinations. Attempts to send a handle to
a cross-origin destination will result in a {{MessagePort/messageerror}} event.
# Security Considerations # {#security-considerations}
*This section is non-normative.*
This API gives websites the ability to modify existing files on disk, as well as write to new
files. This has a couple of important security considerations:
## Malware ## {#security-malware}
This API could be used by websites to try to store and/or execute malware on the users system.
To mitigate this risk, this API does not provide any way to mark files as executable (on the other
hand files that are already executable likely remain that way, even after the files are modified
through this API). Furthermore user agents are encouraged to apply things like Mark-of-the-Web to
files created or modified by this API.
Finally, user agents are encouraged to verify the contents of files modified by this API via malware
scans and safe browsing checks, unless some kind of external strong trust relation already exists.
This of course has effects on the performance characteristics of this API.
## Ransomware attacks ## {#security-ransomware}
Another risk factor is that of ransomware attacks. The limitations described above regarding
blocking access to certain sensitive directories helps limit the damage such an attack can do.
Additionally user agents can grant write access to files at whatever granularity they deem
appropriate.
## Filling up a users disk ## {#filling-up-disk}
Other than files in a [=/bucket file system=], files written by this API are not subject
to [=storage quota=]. As such websites can fill up a users disk without being limited by
quota, which could leave a users device in a bad state (do note that even with storage that is
subject to [=storage quota=] it is still possible to fill up, or come close to filling up, a users
disk, since [=storage quota=] in general is not dependent on the amount of available disk
space).
Without this API websites can write data to disk not subject to quota limitations already
by triggering downloads of large files (potentially created client side, to not incur any network
overhead). While the presence of {{FileSystemWritableFileStream/truncate()}} and writing at a
potentially really large offset past the end of a file makes it much easier and lower cost to
create large files, on most file systems such files should not actually take up as much disk space as
most commonly used file systems support sparse files (and thus wouldn't actually store the NUL
bytes generated by resizing a file or seeking past the end of it).
Whatever mitigations user agents use to guard against websites filling up a disk via either
quota managed storage or the existing downloads mechanism should also be employed when websites
use this API to write to disk.
================================================
FILE: proposals/AccessHandle.md
================================================
# 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
<!-- START doctoc generated TOC please keep comment here to allow auto update -->
<!-- DON'T EDIT THIS SECTION, INSTEAD RE-RUN doctoc TO UPDATE -->
- [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)
- [Assurances on non-awaited consistency](#assurances-on-non-awaited-consistency)
- [Trying It Out](#trying-it-out)
- [Appendix](#appendix)
- [AccessHandle IDL](#accesshandle-idl)
- [References & acknowledgements](#references--acknowledgements)
<!-- END doctoc generated TOC please keep comment here to allow auto update -->
## 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
const accessHandle = await fileHandle.createAccessHandle();
await accessHandle.writable.getWriter().write(buffer);
const reader = accessHandle.readable.getReader({ mode: "byob" });
// Assumes seekable streams, and SharedArrayBuffer support are available
await reader.read(buffer, { at: 1 });
// Only in a worker context
const accessHandle = await fileHandle.createSyncAccessHandle();
const writtenBytes = accessHandle.write(buffer);
const readBytes = accessHandle.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 accessHandle1 = await fileHandle.createAccessHandle();
try {
const accessHandle2 = await fileHandle.createAccessHandle();
} catch (e) {
// This catch will always be executed, since there is an open access handle
}
await accessHandle1.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
### 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<File> getFile();
Promise<FileSystemWritableFileStream> createWritable(optional FileSystemCreateWritableOptions options = {});
Promise<FileSystemAccessHandle> createAccessHandle();
[Exposed=DedicatedWorker]
Promise<FileSystemSyncAccessHandle> createSyncAccessHandle();
};
[SecureContext]
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<undefined> truncate([EnforceRange] unsigned long long size);
// Returns the current size of the file.
Promise<unsigned long long> getSize();
// Persists the changes that have been written to disk
Promise<undefined> flush();
// Flushes and closes the streams, then releases the lock on the file
Promise<undefined> close();
};
[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();
};
dictionary FilesystemReadWriteOptions {
[EnforceRange] unsigned long long at;
};
```
## References & acknowledgements
Many thanks for valuable feedback and advice from:
Domenic Denicola, Marijn Kruisselbrink, Victor Costan
================================================
FILE: proposals/CloudIdentifier.md
================================================
# Cloud Identifier
## Authors:
* Alexander Hendrich (hendrich@chromium.org)
## Participate
* [Issue tracker](https://github.com/WICG/file-system-access/issues)
## Table of Contents
<!-- START doctoc generated TOC please keep comment here to allow auto update -->
<!-- DON'T EDIT THIS SECTION, INSTEAD RE-RUN doctoc TO UPDATE -->
- [Introduction](#introduction)
- [Sample usage](#sample-usage)
- [Demo page](#demo-page)
- [Use Cases](#use-cases)
- [Remote file handling](#remote-file-handling)
- [Before](#before)
- [After](#after)
- [De-duplication for online document editors](#de-duplication-for-online-document-editors)
- [Drag \& Drop into Mail](#drag--drop-into-mail)
- [Non-Goals](#non-goals)
- [Design](#design)
- [Web IDL](#web-idl)
- [Interaction with CSP client](#interaction-with-csp-client)
- [Registration](#registration)
- [Requesting a cloud identifier](#requesting-a-cloud-identifier)
- [Security and Privacy Considerations](#security-and-privacy-considerations)
- [Fingerprinting](#fingerprinting)
- [Modification via read-only permission](#modification-via-read-only-permission)
- [Contributors](#contributors)
- [Stakeholder Feedback / Opposition](#stakeholder-feedback--opposition)
<!-- END doctoc generated TOC please keep comment here to allow auto update -->
## Introduction
The objective of this API is to allow web applications to detect whether a `FileSystemHandle` they have acquired (obtained via file/directory picker or as a parameter of an opening flow as a registered file handler) belongs to a cloud-synced file/directory. If so, the web application receives a “cloud identifier” so that it can directly interact with the file/directory using the cloud storage provider’s (CSP) backend APIs.
A CSP can register as a local sync client to the browser, which in turn may ask the local sync client to provide a unique identifier for a given file/directory when requested.
### Sample usage
```javascript
const [fileHandle] = await window.showOpenFilePicker(pickerOpts);
const cloudIdentifiers = await fileHandle.getCloudIdentifiers();
if(cloudIdentifiers.length === 0) {
// File is not synced by any CSP
}
for(const cloudIdentifier of cloudIdentifiers) {
if(cloudIdentifier.providerName === 'drive.google.com') {
// retrieve/modify the file from Google Drive API using cloudIdentifier.id
} else if(cloudIdentifier.providerName === 'onedrive.live.com') {
// retrieve/modify the file from Microsoft OneDrive API using cloudIdentifier.id
}
}
```
#### Demo page
There is a [demo page](https://cloud-file-handling.glitch.me/) where this API can be tested behind a flag on ChromeOS. Sample usage of that demo page can also be seen with [this recording](https://drive.google.com/file/d/1nrAYOwp9w6JtsKx8XxHkHAVjHyvQG53o/view?usp=sharing).
### Use Cases
#### Remote file handling
Web applications offering app streaming or VDI might want to make a locally synced file available to the remote virtual machine where the app/desktop is being streamed from. For example, the user runs their image editor on a remote machine, launches the file picker to open image.png from the local client device, makes some changes and then saves the file with Ctrl+S.
##### Before

1. VDI web app requests and receives the file via `FileSystemFileHandle.getFile()`
2. CSP client app downloads the file from CSP server (if not already on disc)
3. VDI web app transfers the file’s content to VDI server, which creates it as local file on the server and opens the application for that file
4. VDI server sends any changes made to the file back to the VDI web app
5. VDI web app writes updated file contents via `FileSystemFileHandle.write()`, which is picked up by the CSP client app
6. CSP client app synchronizes the file by uploading it to the CSP server
In this scenario all transfers (arrows in diagram) transfer the entire file and the file has to be downloaded and uploaded from the local device’s network connection.
##### After

1. VDI web app requests and receives the file’s cloud identifier via `FileSystemFileHandle.getCloudIdentifiers()`
2. VDI web app sends the file’s cloud identifier to VDI server
3. VDI server requests and receives the file’s content from CSP server using the cloud identifier
4. VDI server sends any changes made to the file back to the CSP server
5. CSP client synchronizes updated file to local device (might be metadata only)
In this scenario only transfers (3) and (4) would actually transfer the full file, all the other transfers only move the cloud identifier. These actual file transfers also don’t move across the device’s network connection, but rather use the VDI server’s network connection to the CSP server, which should likely also have higher bandwidth.
This reduces network traffic and delays (especially with large files) and prevents version drift (file being modified somewhere else while a local client device is uploading).
#### De-duplication for online document editors
Web-based document editors can already open local files using existing file system APIs. In order to offer full functionality, including cross device support and some advanced editing/sharing features, the documents need to be uploaded to a cloud storage (Google Drive or Microsoft OneDrive or similar) before they can be edited. This leads to duplicate files for documents that were already stored in these locations.
With the proposed changes, the web application could check whether the given file handle is already synced by a cloud storage and generate identifiers for these without a duplicate upload.
#### Drag & Drop into Mail
Sharing files via mail used to be done using simple file attachments. Especially for large files, it is preferable to instead upload them to a CSP and then share an access link to that file to the recipient. Google Mail’s web application, for example, already does this by prompting the user to upload a file to Google Drive when attaching a large file.
With the proposed changes, the web application could already detect that the file is synced by cloud storage and generate a share link for that file’s cloud identifier without requiring the user to manually go through these steps or prompt the user to upload the file to cloud storage again.
### Non-Goals
This proposal does **not** plan to provide a way for web apps to
* provide permission to these files/directories.
* interact (fetch/modify/etc) with these files/directories.
* provide additional meta-data (e.g. sync status)
across various different CSP backend APIs.
This new web API just serves as bridge between the CSP's local sync clients and the web app to retrieve a unique identifier for a file/directory. Obtaining the required permissions on that file/directory or actually reading/writing contents to it via CSP backend API is delegated to the web app's own implementation for each CSP. The `getCloudIdentifiers()` might also return `FileSystemCloudIdentifier`s for CSPs the web app does not support.
## Design

1. Locally installed CSP client registers itself as a provider for certain directories with the browser
2. Web app receives `FileSystemHandle` for a file/directory (via File Handling opening flow, File System Access file/directory picker or drag & drop) and requests its cloud identifier(s) via `getCloudIdentifiers()`.
3. Browser checks whether this path has been registered as being synced by a local sync client
* If no, resolve promise with empty list
4. For each registered local sync client for that path, the browser will send a request to the registered local sync client's executable with the file’s/directory’s path
5. Local sync client requests a token for that path from the CSP
6. CSP can choose to generate a one-time token or stable identifier to later be used by the CSP’s backend APIs. Either way, the properties of the token are up to the CSP and completely opaque to the browser.
7. Local sync client responds to browser with the token
8. Browser gathers all responses from registered local sync clients. For all incoming responses, the browser will construct a cloud identifier consisting of the responding local sync client's registered identifier and the provided token. Once all registered local sync clients for that path have responded, the promise is resolved with a list of cloud identifiers. If the local sync client fails to respond within a reasonable time frame, the promise is resolved with all received cloud identifiers until then or empty list if none.
9. Web app can freely interact with CSP’s backend APIs on that file/directory. The web application is responsible for getting the right access permissions for these web APIs.
### Web IDL
This section describes the interface the web app would interact with.
```idl
dictionary FileSystemCloudIdentifier {
DOMString providerName;
DOMString id;
};
partial interface FileSystemHandle {
Promise<FileSystemCloudIdentifier[]> getCloudIdentifiers();
}
```
The new method
* extends `FileSystemHandle`, i.e. is available for files and directories.
* returns a list of `FileSystemCloudIdentifier`s since a single file/directory can be synced by multiple CSPs at the same time, although in most cases this list would only contain a single entry.
* returns an empty list if the file/directory is not synced by any CSPs or no CSP client responded in time.
### Interaction with CSP client
This web API requires the browser and the CSP’s local sync client to exchange information. This piece is not part of the official specification and up to individual browser’s to implement, but we can provide guidelines here as well.
#### Registration
The browser needs to be aware of
* which CSP client’s are available on the user’s computer
* which files/directories are synced by the CSP client
* where the CSP client executable is located
* the CSP’s identifier (e.g. “com.google.drive”)
Therefore, the CSP needs to provide a JSON file containing these details and register it with the browser at a known location (specific directory or registry key), similar to [Chrome extension’s native messaging host registration](https://developer.chrome.com/docs/apps/nativeMessaging/#native-messaging-host-location).
The provided file would look like this:
```json
{
"name": "com.foo-company.drive",
"path": "C:\\Program Files\\FooCompany\\cloud_storage_provider_helper.exe",
"synced_paths": [
"C:\\path_to_synced_directory\\",
"C:\\path_to_other_synced_directory\\",
]
}
```
#### Requesting a cloud identifier
Whenever a web app calls `getCloudIdentifiers()`, the browser will iterate through all registered CSP clients and filter for ones that cover the file’s/directory’s path as part of their `synced_paths`.
For these, the browser will launch the executable at `path` and transfer the web app’s origin and the requested file/directory path, either via command line argument or via stdin (preceded with a 32-bit message length).
The CSP client will then respond with the token for the given file/directory via stdout (preceded with a 32-bit message length).
Once the browser has received all responses (or some reasonable timeout has expired), the browser will bundle the individual tokens and the CSPs’ names together to return a cloud identifier.
It is up to each individual browser’s implementation on how they handle dynamic registrations, i.e. whether they re-read all the CSP registration files for each request and how long or if they cache them.
## Security and Privacy Considerations
### Fingerprinting
The browser has no control whether the CSP will provide one-time tokens or stable identifiers as part of their cloud identifier. If the CSP provides stable identifiers, the web application could use these as a [fingerprinting](https://www.w3.org/TR/fingerprinting-guidance/#dfn-active-fingerprinting) mechanism for the files/directories it has access to.
In theory, if a web application already has access to a `FileSystemHandle` the web application could already use other mechanisms to generate fingerprinting identifiers, but these are less stable:
* Hashing the file’s/directory’s contents -> identifier will change if file/directory content changes
* `FileSystemHandle.getUniqueId()` [[explainer](https://github.com/whatwg/fs/pull/46)] -> clearing browsing data will reset unique IDs
The web application would need repeated access to the same `FileSystemHandle` to perform this fingerprinting though. That means the user must either re-grant access to the same file or the web app stores the file handle in an IndexedDB, which can be cleared by the user though by clearing their browsing data.
It would also be up to the CSP whether they actually provide permanent tokens or temporary tokens.
### Modification via read-only permission
If a web application only has `read` [permission](https://wicg.github.io/file-system-access/#enumdef-filesystempermissionmode) to a `FileSystemHandle`, but has edit/write permission to that file via CSP backend APIs, it could still modify the cloud-stored file, which is then synced to the device and thereby modify the file.
In this case, the user has clearly granted write access to the CSP-backed file and by having a sync client, the user also allows the local files to be modified, so the change would actually not be surprising.
## Contributors
* Austin Sullivan (asully@chromium.org)
* Rob Beard (rbeard@google.com)
## Stakeholder Feedback / Opposition
* Web developer: [positive](https://github.com/WICG/file-system-access/pull/411#issuecomment-1609676416)
================================================
FILE: proposals/SuggestedNameAndDir.md
================================================
# Suggested file name and location
## Authors:
* Marijn Kruisselbrink
## Participate
* [Issue tracker](https://github.com/WICG/file-system-access/issues)
## Table of Contents
<!-- START doctoc generated TOC please keep comment here to allow auto update -->
<!-- DON'T EDIT THIS SECTION, INSTEAD RE-RUN doctoc TO UPDATE -->
- [Introduction](#introduction)
- [Goals and Use Cases](#goals-and-use-cases)
- [Save an existing file in a different directory](#save-an-existing-file-in-a-different-directory)
- [Save a file using a different name in the same directory](#save-a-file-using-a-different-name-in-the-same-directory)
- [Open a file from the same directory as a currently open file or project](#open-a-file-from-the-same-directory-as-a-currently-open-file-or-project)
- [Prompt a user to open a image (or video, or audio file)](#prompt-a-user-to-open-a-image-or-video-or-audio-file)
- [Remember different last-used directories for different purposes](#remember-different-last-used-directories-for-different-purposes)
- [Non-goals](#non-goals)
- [Proposed API](#proposed-api)
- [Specifying suggested file name to save as](#specifying-suggested-file-name-to-save-as)
- [Specifying starting directory based on an existing handle.](#specifying-starting-directory-based-on-an-existing-handle)
- [Specifying a well-known starting directory](#specifying-a-well-known-starting-directory)
- [Distinguishing the "purpose" of different file picker invocations.](#distinguishing-the-purpose-of-different-file-picker-invocations)
- [Detailed design discussion](#detailed-design-discussion)
- [Interaction of `suggestedName` and accepted file types](#interaction-of-suggestedname-and-accepted-file-types)
- [Considered alternatives](#considered-alternatives)
- [Interaction between `startIn` and `id`](#interaction-between-startin-and-id)
- [Considered alternatives](#considered-alternatives-1)
- [Start in directory vs start in parent of directory](#start-in-directory-vs-start-in-parent-of-directory)
- [Security considerations](#security-considerations)
- [Stakeholder Feedback / Opposition](#stakeholder-feedback--opposition)
- [References & acknowledgements](#references--acknowledgements)
<!-- END doctoc generated TOC please keep comment here to allow auto update -->
## Introduction
When initially shipping the File System Access API we shipped a bare minimum API
surface for showing file and directory pickers. We're now proposing adding a
couple of frequently requested (and commonly supported in other file picker
APIs) options to let websites provide a couple more opportunities to influence
the behavior of the file and directory pickers.
In particular this explainer addresses letting websites give suggestions for the
name and location of files and directories that are being saved or loaded.
## Goals and Use Cases
The overarching goal here is to provide websites the opportunity to provide the
file or directory picker implementation suggestions for both the initial
directory to show in the picker, and (in the case of save dialogs) the file name
to save a file as. Specifically to help address the following use cases:
### Save an existing file in a different directory
1. User opens a file from their local file system, or from some cloud provider.
2. User then picks the "Save As" option from the web application.
3. User saves the file in a different directory, but using the name the file
already had (without having to re-type the name).
### Save a file using a different name in the same directory
1. Users opens a file from their local file system.
2. User then picks the "Save As" option from the web application.
3. User saves the file using a different name, in the directory the original
file exists (without having to re-navigate to that directory).
### Open a file from the same directory as a currently open file or project
1. User is editing a file from their local file system.
2. User then picks the "Open" option from the web application to open another file.
3. User can pick files from the same directory the currently open file is in
without having to re-navigate to that directory.
### Prompt a user to open a image (or video, or audio file)
1. User picks "Insert image" in a web application
2. A file picker pops up starting out in the users "Pictures" folder.
### Remember different last-used directories for different purposes
1. User opens a document in a web application from directory A.
2. User inserts an image into the document, by selecting a file from directory B.
3. User opens a different document, from directory A without having to navigate
to directory A again.
4. Further image insertion operations should have their file pickers start out
in the directory of the last image insertion as well.
## Non-goals
All the above functionality is intended as hints/suggestions from websites to
the file picker implementation to improve the end user experience. However
implementations should not be required to always follow these suggestions. If
for example a suggested file name provided by the website is deemed too
dangerous to be allowed, implementations should be free to ignore the suggestion
(or adjust the suggested file name to not be dangerous anymore).
## Proposed API
### Specifying suggested file name to save as
```javascript
const file_ref = await self.showSaveFilePicker({
suggestedName: 'README.md',
types: [{
description: 'Markdown',
accept: {
'text/markdown': ['.md'],
},
}],
});
```
### Specifying starting directory based on an existing handle.
We propose adding a `startIn` option, that can be either a file or a directory
handle. If the passed in handle is a file handle, the picker will start out in
the parent directory of the file, while if the passed in handle is a directory
handle the picker will start out in the passed in directory itself.
Additionally, in a save file picker, if `startIn` is a file, and no explicit
`suggestedName` is also passed in, the name from the passed in file handle
will be treated as if it was passed as `suggestedName`.
This lets you use `startIn` for typical "Save as" UI flows:
```javascript
async function saveFileAs(file_handle) {
return await self.showSaveFilePicker({
startIn: file_handle
});
}
```
And is also useful for cases where you a user is likely to want to open a file
from the same directory as a currently open file or directory:
```javascript
async function openFileFromDirectory(project_dir) {
return await self.showOpenFilePicker({
startIn: project_dir
});
}
// Used when prompting the user to open a new file, starting out in the
// directory containing a currently open file.
async function openFileFromDirectoryContainingFile(open_file) {
return await self.showOpenFilePicker({
startIn: open_file
});
}
// Used for example in a flow where a website wants to prompt the user to open
// the directory containing the currently opened file (for example for file
// formats that contain relative paths to other files in the same directory).
async function openDirectoryContainingFile(open_file) {
return await self.showDirectoryPicker({
startIn: open_file
});
}
```
### Specifying a well-known starting directory
To support saving files in or opening files from certain well-known directories,
we also propose allowing passing certain string values to `startIn` to represent
these well-known directories. This would look something like:
```javascript
const file_ref = await self.showOpenFilePicker({
startIn: 'pictures'
});
```
The possible values for `startIn` would be:
- `desktop`: The user's Desktop directory, if such a thing exists.
- `documents`: Directory in which documents created by the user would typically be stored.
- `downloads`: Directory where downloaded files would typically be stored.
- `music`: Directory where audio files would typically be stored.
- `pictures`: Directory where photos and other still images would typically be stored.
- `videos`: Directory where videos/movies would typically be stored.
### Distinguishing the "purpose" of different file picker invocations.
Currently the Chrome implementation of the File System Access API remembers the
last used directory on a per-origin basis. To allow websites to remember
different directories for file pickers that are used for different purposes
(i.e. opening documents, exporting to other formats, inserting images) we
propose adding an `id` option to the file and directory picker methods. If an
`id` is specified the file picker implementation will remember a separate last
used directory for pickers with that same `id`:
```javascript
const file_ref1 = await self.showSaveFilePicker({
id: 'fruit'
});
const file_ref2 = await self.showSaveFilePicker({
id: 'flower'
});
// Picker should start out in the directory `file_ref1` was picked from.
const file_ref3 = await self.showOpenFilePicker({
id: 'fruit'
});
```
## Detailed design discussion
### Interaction of `suggestedName` and accepted file types
It is possible for `suggestedName` to be inconsistent with the file types a
website declared as being the accepted file types for a file picker. There are
a couple of cases worth highlighting here:
1. If `suggestedName` ends in a suffix that is specified for one of the accepted
file types, the file picker should default to that file type.
2. Otherwise, if `excludeAcceptAllOption` is false (or no explicit accepted file
types are provided), the "all files" options should be the default selected
file type in the file picker.
3. Finally, if neither of these are the case (i.e. the suggested file name does
not match any of the file types accepted by the dialog), the implementation
should behave as if `excludeAcceptAllOption` was set to false, and default
to that option.
#### Considered alternatives
An alternative to 3. would be to reject if no accepted file types match the
suggested file name.
Another alternative to 3. would be to append a suffix from the first accepted
file type to the suggested file name (or replace the extension from the
suggested file name with one that is accepted). This seems less desirable than 3
because this would mean that specifying an extension in the `suggestedName` is
optional as long as `excludeAcceptAllOption` is set to true, but then later
changing `excludeAcceptAllOption` to false would suddenly change behavior and
possibly break existing API usage.
### Interaction between `startIn` and `id`
Both these attributes influence what directory the file picker should start with,
as such it isn't immediately obvious what should happen if all are provided.
Our proposal is for it to not be an error to provide both `startIn` and
`id`. If a well-known directory is provide for `startIn`, `id` will take
precedence. That means that if a previously recorded path for the same `id`
exists, it will be used as starting directory, and only if no such path is
known will `startIn` be used.
On the other hand if `startIn` is a file or directory handle, it will take
precedence over `id`. In this case `startIn` specifies what directory the
file picker should start out in, and the ultimately picked directory will be
recorded as the last selected directory for the given `id`, such that future
invocations with that `id` but no `startIn` will use the directory.
#### Considered alternatives
We could reject if both a `startIn` option and `id` are provided, to ensure only
one option controls which directory to start in. There don't see to be many
downsides to allowing both though, as recording the last used directory for
future invocations of a file picker without a `startIn` option still seems
useful.
We could also have either `id` or `startIn` always take precedence over the
other. In some ways this might be less confusing, as it would always be clear
which one takes precedence, without having to know the type of what is passed to
`startIn`. We've chosen not to do so though. Using a well-known directory only
as a fallback mechanism for the `id` based directory matches what other similar
APIs do. Simultanously if a website explicitly specifies a concrete directory to
open the picker in by passing a file or directory handle to `startIn` we also
want to respect that. Hence the precedence depending on the type of the `startIn`
option.
### Start in directory vs start in parent of directory
An earlier version of this document had separate `startIn` and `startInParentOf`
options. This would enable websites to not only open file or directory pickers
in the same directory as a passed in directory handle, but also in the parent
directory of such a directory (without needing to have access to a handle for
the parent directory).
Having a single `startIn` option results in a simpler and easier to explain and
understand API, whil still supporting all the major use cases. Websites will be
able to start file or directory pickers in any directory they have a handle to,
as well as any directory for which they have a handle to a file in said
directory. The only hypothetical use case that isn't covered by this is for
cases where the website wants the user to select a sibling directory to a
previously selected directory, but we're not aware of any concrete use cases
where that would be beneficial.
### Security considerations
As mentioned in the non-goals section, all these attributes should be considered
suggestions or hints. This is particularly relevant for the `suggestedName`
option. The same concerns around writing to files ending in `.lnk` or `.local`
as mentioned in https://wicg.github.io/file-system-access/#privacy-wide-access
also apply to letting the website suggest a user save files with these names.
User agents should do similar sanitization as to how the `download` attribute
of `<a>` tags is processed in https://html.spec.whatwg.org/multipage/links.html#as-a-download.
## Stakeholder Feedback / Opposition
* Chrome : Positive, authoring this explainer
* Gecko : No signals
* WebKit : No signals
* Web developers : Positive, frequently requested (See #85, #144, #94 and #80.)
## References & acknowledgements
Many thanks for valuable feedback and advice from:
Austin Sullivan, Thomas Steiner
================================================
FILE: security-privacy-questionnaire.md
================================================
https://www.w3.org/TR/2019/NOTE-security-privacy-questionnaire-20190523/
### 2.1. What information might this feature expose to Web sites or other parties, and for what purposes is that exposure necessary?
This feature exposes files and directories the user explicitly selects to share with web sites with those web sites. This feature doesn't expose any more information than is already exposed via `<input type=file>` and `<input type=file webkitdirectory>` today.
However this feature does make it possible for browsers to extend the time web sites have access to this information. I.e. permission grants and handles received through this API could be persisted giving web sites access to the same files/directories on disk when a user later returns to the website (or when a service worker for the origin is processing an event). At least for the Chrome implementation, we're only planning on having these grants persist for installed PWAs. On the drive-by web, access will only be as long as the web site is open, requiring re-prompting in subsequent visits.
### 2.2. Is this specification exposing the minimum amount of information necessary to power the feature?
Yes, we're only exposing files and directories explicitly selected by the user. Of course, web sites could ask for access to a directory when all it needs is access to some files, but the same is already true today. At least as far as exposing information to web sites is concerned, this API doesn't expose any more information than existing APIs do today.
### 2.3. How does this specification deal with personal information or personally-identifiable information or information derived thereof?
No data is exposed without the user explicitly choosing what files or directories to expose to the web site.
### 2.4. How does this specification deal with sensitive information?
No data is exposed without the user explicitly choosing what files or directories to expose to the web site, so only sensitive data that the user explicitly decides to share via this API will be shared. Furthermore, this API is only exposed in secure contexts, and third-party iframes (i.e. iframes that are cross origin from the top-level frame) won't be able to show pickers or permission prompts and can only access data they were already granted access to from a top-level same origin frame.
### 2.5. Does this specification introduce new state for an origin that persists across browsing sessions?
Yes, this specification lets websites store handles they've gotten access to (via a file or directory picker) in IndexedDB. User agents could also persist the permission grants that go with these handles, but at least in the Chrome implementation, these permission grants will only be persistent for installed PWAs. The drive-by web will only have enough state to allow it to re-prompt for access, but the access itself won't be persistent.
Furthermore, the user will be able to clear storage (storage is file handles in IndexedDB) and/or revoke permissions to clear the state that was persisted, similarly to how other permissions work.
Websites can also store any state they like in files they get write access to via this API. Since files written to using this API are considered to be data owned by the user, not by the application, this state would not be cleared when clearing browser data. However access to this state would be removed. If a user later picks the same files or directories again to give the website access to them, the websites will regain access to whatever state they persisted.
Additionally, user agents could also choose to persist the last directory a file was picked from using this API on a per origin (and per purpose via the `FilePickerOption.id` option) basis. This state will not be exposed to the website, it only changes the UI that is presented to the user. A website will have no way of telling if a user picked a file in a certain directory because of this state or because the user manually navigated to the directory.
The `getUniqueId()` method will require a user agent to persist information (e.g. a salt) to provide unique identifiers for handles which are stable across browsing sessions, but which are invalidated once the user clears storage for the site. This state will not be exposed to the website.
The `getCloudIdentifiers()` method will request identifiers for a given file/directory handle from a cloud storage provider's sync client (usually an external service/application) and forward these to the requesting website. These identifiers may be stable and cannot be invalidated as part of this API.
### 2.6. What information from the underlying platform, e.g. configuration data, is exposed by this specification to an origin?
Anything that exists on disk in files could be exposed by the user to the web. However, user agents are encouraged to maintain a block list of certain directories with particularly sensitive files, and thus somewhat restrict which files and directories the user is allowed to select. For example, things like Chrome's "Profile" directory, and other platform configuration data directories are likely going to be on this block list.
The `getCloudIdentifiers()` method will request identifiers for a given file/directory handle from a cloud storage provider's sync client (usually an external service/application) and forward these to the requesting website.
Therefore, the requesting website can enumerate all those sync clients present on the user's machine that sync a file/directory the website has a handle to.
### 2.7. Does this specification allow an origin access to sensors on a user’s device
No, unless a device exposes such sensors as files or directories. User agents are encouraged to block access to such files or directories (for example `/dev` on linux like systems).
### 2.8. What data does this specification expose to an origin? Please also document what data is identical to data exposed by other features, in the same or different contexts.
The data this specification lets a user expose to an origin is identical to the data exposed via `<input type=file>` and `<input type=file webkitdirectory>`. The differences are in giving the origins the ability to re-prompt for access to files they previously had access to (if handles were stored in IndexedDB), and in the ability for origins to write back to the files (after explicit permission is granted for that).
### 2.9. Does this specification enable new script execution/loading mechanisms?
No.
### 2.10. Does this specification allow an origin to access other devices?
Not really. The exception would be devices that are exposed as files or directories by the platform. I.e. network shares or cloud storage sync clients could expose data on other devices in a way that looks like regular files or directories. The user agent could let the user pick these files or directories, thereby giving an origin implicit access to this other device. This API doesn't have any functionality to let a website enumerate all network shares on the local network, only explicitly selected files or directories can be accessed by an origin.
### 2.11. Does this specification allow an origin some measure of control over a user agent’s native UI?
The origin can pop up native file or directory pickers, and have some control over what appears inside that native UI (e.g. accepted file types, starting directory and suggested file names), but that control is very limited. The spec does put limitations on what is allowed as an accepted file type and suggested file name to limit the security impact of allowing websites to control the native UI. User agents are expected to employ similar mechanisms to sanitize the sugessted file names as are used to sanitize for example suggested file names in `<a download="foo.ln">` today.
### 2.12. What temporary identifiers might this this specification create or expose to the web?
The `getUniqueId()` method will create a temporary unique identifier for a given handle. This ID will become invalid if the user clears storage for the site.
### 2.13. How does this specification distinguish between behavior in first-party and third-party contexts?
It is expected that user agents do not allow third-party contexts to prompt for any kind of access using this API. I.e. third-party contexts can potentially access files or directories that their origin was already granted access to in a first-party context (by sharing handles via IndexedDB or postMessage), but can't trigger any new file/directory pickers or permission requests.
### 2.14. How does this specification work in the context of a user agent’s Private Browsing or "incognito" mode?
The feature will work mostly the same as in regular mode, except no handles or permission grants will be persistent. Web sites can use this API to store data to disk even in private browsing mode, but to later be able to read this data again (either from private browsing or regular mode), the user would have to explicitly re-pick the same file or directory.
### 2.15. Does this specification have a "Security Considerations" and "Privacy Considerations" section?
Yes.
### 2.16. Does this specification allow downgrading default security characteristics?
No.
### 2.17. What should this questionnaire have asked?
Perhaps something about how a feature might impact privacy and security in a bigger picture. Particularly this questionnaire focuses on all the ways it might make privacy or security worse. And while that is important, and while adding new capabilities like this looks scary, from a higher level perspective we do believe that this actually makes things better. Adding these capabilities to the web will lead to Web replacements for one-off native apps, resulting in a net benefit for user security & privacy.
================================================
FILE: w3c.json
================================================
{
"group": [80485]
, "contacts": ["marcoscaceres"]
, "repo-type": "cg-report"
}
gitextract_jv1ysorf/ ├── .github/ │ └── workflows/ │ └── pr-push.yml ├── .gitignore ├── .pr-preview.json ├── CODE_OF_CONDUCT.md ├── CONTRIBUTING.md ├── EXPLAINER.md ├── LICENSE.md ├── README.md ├── changes.md ├── index.bs ├── proposals/ │ ├── AccessHandle.md │ ├── CloudIdentifier.md │ └── SuggestedNameAndDir.md ├── security-privacy-questionnaire.md └── w3c.json
Condensed preview — 15 files, each showing path, character count, and a content snippet. Download the .json file or copy for the full structured content (131K chars).
[
{
"path": ".github/workflows/pr-push.yml",
"chars": 306,
"preview": "name: CI\non:\n pull_request: {}\n push:\n branches: [main]\n\njobs:\n main:\n name: Build, Validate and Deploy\n run"
},
{
"path": ".gitignore",
"chars": 11,
"preview": "/index.html"
},
{
"path": ".pr-preview.json",
"chars": 97,
"preview": "{\n \"src_file\": \"index.bs\",\n \"type\": \"bikeshed\",\n \"params\": {\n \"force\": 1\n }\n}\n"
},
{
"path": "CODE_OF_CONDUCT.md",
"chars": 186,
"preview": "# Code of Conduct\n\nAll documentation, code and communication under this repository are covered by the [W3C Code of Ethic"
},
{
"path": "CONTRIBUTING.md",
"chars": 814,
"preview": "# Web Platform Incubator Community Group\n\nThis repository is being used for work in the W3C Web Platform Incubator Commu"
},
{
"path": "EXPLAINER.md",
"chars": 15662,
"preview": "# What is all this?\n\nAt a high level what we're providing is several bits:\n\n1. A modernized version of the existing (but"
},
{
"path": "LICENSE.md",
"chars": 434,
"preview": "All Reports in this Repository are licensed by Contributors\nunder the\n[W3C Software and Document License](http://www.w3."
},
{
"path": "README.md",
"chars": 2128,
"preview": "# File System Access API\nView proposals in the [EXPLAINER](EXPLAINER.md) and the [spec](https://wicg.github.io/file-syst"
},
{
"path": "changes.md",
"chars": 3982,
"preview": "This file enumerates the changes that were made to the API surface between the Origin Trial as shipped in Chrome 83,\nand"
},
{
"path": "index.bs",
"chars": 54181,
"preview": "<pre class=metadata>\nTitle: File System Access\nShortname: file-system-access\nAbstract: This document extends the API in "
},
{
"path": "proposals/AccessHandle.md",
"chars": 11700,
"preview": "# AccessHandle Proposal\n\n## Authors:\n\n* Emanuel Krivoy (fivedots@chromium.org)\n* Richard Stotz (rstz@chromium.org)\n\n## P"
},
{
"path": "proposals/CloudIdentifier.md",
"chars": 13793,
"preview": "# Cloud Identifier\n\n## Authors:\n\n* Alexander Hendrich (hendrich@chromium.org)\n\n## Participate\n\n* [Issue tracker](https:/"
},
{
"path": "proposals/SuggestedNameAndDir.md",
"chars": 14274,
"preview": "# Suggested file name and location\n\n## Authors:\n\n* Marijn Kruisselbrink\n\n## Participate\n\n* [Issue tracker](https://githu"
},
{
"path": "security-privacy-questionnaire.md",
"chars": 9835,
"preview": "https://www.w3.org/TR/2019/NOTE-security-privacy-questionnaire-20190523/\n\n### 2.1. What information might this feature e"
},
{
"path": "w3c.json",
"chars": 97,
"preview": " {\n \"group\": [80485]\n, \"contacts\": [\"marcoscaceres\"]\n, \"repo-type\": \"cg-report\"\n}\n"
}
]
About this extraction
This page contains the full source code of the WICG/file-system-access GitHub repository, extracted and formatted as plain text for AI agents and large language models (LLMs). The extraction includes 15 files (124.5 KB), approximately 30.0k tokens. Use this with OpenClaw, Claude, ChatGPT, Cursor, Windsurf, or any other AI tool that accepts text input. You can copy the full output to your clipboard or download it as a .txt file.
Extracted by GitExtract — free GitHub repo to text converter for AI. Built by Nikandr Surkov.