diff --git a/.editorconfig b/.editorconfig new file mode 100644 index 0000000..625d610 --- /dev/null +++ b/.editorconfig @@ -0,0 +1,19 @@ +root = true + +[*] +end_of_line = lf +insert_final_newline = true +charset = utf-8 +indent_size = 2 +indent_style = space +trim_trailing_whitespace = true +max_line_length = 100 + +[Makefile] +indent_style = tab + +[*.bs] +indent_size = 1 + +[*.py] +indent_size = 4 diff --git a/.gitattributes b/.gitattributes new file mode 100644 index 0000000..2f2e77e --- /dev/null +++ b/.gitattributes @@ -0,0 +1,2 @@ +* text=auto +*.bs diff=html linguist-language=HTML diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml new file mode 100644 index 0000000..29d0c65 --- /dev/null +++ b/.github/workflows/build.yml @@ -0,0 +1,26 @@ +name: Build +on: + pull_request: + branches: + - main + push: + branches: + - main +jobs: + build: + name: Build + runs-on: ubuntu-20.04 + steps: + - uses: actions/checkout@v2 + with: + fetch-depth: 2 + # Note: `python` will also be this version, which various scripts depend on. + - uses: actions/setup-python@v2 + with: + python-version: 3.8 + # Note: `make deploy` will do a deploy dry run on PRs. + - run: make deploy + env: + SERVER: ${{ secrets.MARQUEE_SERVER }} + SERVER_PUBLIC_KEY: ${{ secrets.MARQUEE_PUBLIC_KEY }} + SERVER_DEPLOY_KEY: ${{ secrets.MARQUEE_DEPLOY_KEY }} diff --git a/.github/workflows/pr-push.yml b/.github/workflows/pr-push.yml deleted file mode 100644 index c25c388..0000000 --- a/.github/workflows/pr-push.yml +++ /dev/null @@ -1,16 +0,0 @@ -name: CI -on: - pull_request: {} - push: - branches: [main] - -jobs: - main: - name: Build, Validate and Deploy - runs-on: ubuntu-20.04 - steps: - - uses: actions/checkout@v2 - - uses: sidvishnoi/spec-prod@v1 - with: - GH_PAGES_BRANCH: gh-pages - VALIDATE_MARKUP: false diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..ef9c01c --- /dev/null +++ b/.gitignore @@ -0,0 +1,4 @@ +/fs.spec.whatwg.org/ +/deploy.sh +/index.html +/review.sh diff --git a/.pr-preview.json b/.pr-preview.json index 41eb560..3b9efb1 100644 --- a/.pr-preview.json +++ b/.pr-preview.json @@ -1,7 +1,9 @@ { - "src_file": "index.bs", - "type": "bikeshed", - "params": { - "force": 1 - } + "src_file": "index.bs", + "type": "bikeshed", + "params": { + "force": 1, + "md-status": "LS-PR", + "md-Text-Macro": "PR-NUMBER {{ pull_request.number }}" + } } diff --git a/CODE_OF_CONDUCT.md b/CODE_OF_CONDUCT.md deleted file mode 100644 index 04f41dc..0000000 --- a/CODE_OF_CONDUCT.md +++ /dev/null @@ -1,3 +0,0 @@ -# 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/). diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md deleted file mode 100644 index e8563f2..0000000 --- a/CONTRIBUTING.md +++ /dev/null @@ -1,23 +0,0 @@ -# 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. diff --git a/EXPLAINER.md b/EXPLAINER.md deleted file mode 100644 index 7061206..0000000 --- a/EXPLAINER.md +++ /dev/null @@ -1,364 +0,0 @@ -# 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 ``. - -# 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 = (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 = 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', 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. diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..f2dcda4 --- /dev/null +++ b/LICENSE @@ -0,0 +1,356 @@ +Copyright © WHATWG (Apple, Google, Mozilla, Microsoft). + +This work is licensed under a Creative Commons Attribution 4.0 International +License. To the extent portions of it are incorporated into source code, +such portions in the source code are licensed under the BSD 3-Clause License instead. + +- - - - + +Creative Commons Attribution 4.0 International Public License + +By exercising the Licensed Rights (defined below), You accept and agree +to be bound by the terms and conditions of this Creative Commons +Attribution 4.0 International Public License ("Public License"). To the +extent this Public License may be interpreted as a contract, You are +granted the Licensed Rights in consideration of Your acceptance of +these terms and conditions, and the Licensor grants You such rights in +consideration of benefits the Licensor receives from making the +Licensed Material available under these terms and conditions. + + +Section 1 -- Definitions. + + a. Adapted Material means material subject to Copyright and Similar + Rights that is derived from or based upon the Licensed Material + and in which the Licensed Material is translated, altered, + arranged, transformed, or otherwise modified in a manner requiring + permission under the Copyright and Similar Rights held by the + Licensor. For purposes of this Public License, where the Licensed + Material is a musical work, performance, or sound recording, + Adapted Material is always produced where the Licensed Material is + synched in timed relation with a moving image. + + b. Adapter's License means the license You apply to Your Copyright + and Similar Rights in Your contributions to Adapted Material in + accordance with the terms and conditions of this Public License. + + c. Copyright and Similar Rights means copyright and/or similar rights + closely related to copyright including, without limitation, + performance, broadcast, sound recording, and Sui Generis Database + Rights, without regard to how the rights are labeled or + categorized. For purposes of this Public License, the rights + specified in Section 2(b)(1)-(2) are not Copyright and Similar + Rights. + + d. Effective Technological Measures means those measures that, in the + absence of proper authority, may not be circumvented under laws + fulfilling obligations under Article 11 of the WIPO Copyright + Treaty adopted on December 20, 1996, and/or similar international + agreements. + + e. Exceptions and Limitations means fair use, fair dealing, and/or + any other exception or limitation to Copyright and Similar Rights + that applies to Your use of the Licensed Material. + + f. Licensed Material means the artistic or literary work, database, + or other material to which the Licensor applied this Public + License. + + g. Licensed Rights means the rights granted to You subject to the + terms and conditions of this Public License, which are limited to + all Copyright and Similar Rights that apply to Your use of the + Licensed Material and that the Licensor has authority to license. + + h. Licensor means the individual(s) or entity(ies) granting rights + under this Public License. + + i. Share means to provide material to the public by any means or + process that requires permission under the Licensed Rights, such + as reproduction, public display, public performance, distribution, + dissemination, communication, or importation, and to make material + available to the public including in ways that members of the + public may access the material from a place and at a time + individually chosen by them. + + j. Sui Generis Database Rights means rights other than copyright + resulting from Directive 96/9/EC of the European Parliament and of + the Council of 11 March 1996 on the legal protection of databases, + as amended and/or succeeded, as well as other essentially + equivalent rights anywhere in the world. + + k. You means the individual or entity exercising the Licensed Rights + under this Public License. Your has a corresponding meaning. + + +Section 2 -- Scope. + + a. License grant. + + 1. Subject to the terms and conditions of this Public License, + the Licensor hereby grants You a worldwide, royalty-free, + non-sublicensable, non-exclusive, irrevocable license to + exercise the Licensed Rights in the Licensed Material to: + + a. reproduce and Share the Licensed Material, in whole or + in part; and + + b. produce, reproduce, and Share Adapted Material. + + 2. Exceptions and Limitations. For the avoidance of doubt, where + Exceptions and Limitations apply to Your use, this Public + License does not apply, and You do not need to comply with + its terms and conditions. + + 3. Term. The term of this Public License is specified in Section + 6(a). + + 4. Media and formats; technical modifications allowed. The + Licensor authorizes You to exercise the Licensed Rights in + all media and formats whether now known or hereafter created, + and to make technical modifications necessary to do so. The + Licensor waives and/or agrees not to assert any right or + authority to forbid You from making technical modifications + necessary to exercise the Licensed Rights, including + technical modifications necessary to circumvent Effective + Technological Measures. For purposes of this Public License, + simply making modifications authorized by this Section 2(a) + (4) never produces Adapted Material. + + 5. Downstream recipients. + + a. Offer from the Licensor -- Licensed Material. Every + recipient of the Licensed Material automatically + receives an offer from the Licensor to exercise the + Licensed Rights under the terms and conditions of this + Public License. + + b. No downstream restrictions. You may not offer or impose + any additional or different terms or conditions on, or + apply any Effective Technological Measures to, the + Licensed Material if doing so restricts exercise of the + Licensed Rights by any recipient of the Licensed + Material. + + 6. No endorsement. Nothing in this Public License constitutes or + may be construed as permission to assert or imply that You + are, or that Your use of the Licensed Material is, connected + with, or sponsored, endorsed, or granted official status by, + the Licensor or others designated to receive attribution as + provided in Section 3(a)(1)(A)(i). + + b. Other rights. + + 1. Moral rights, such as the right of integrity, are not + licensed under this Public License, nor are publicity, + privacy, and/or other similar personality rights; however, to + the extent possible, the Licensor waives and/or agrees not to + assert any such rights held by the Licensor to the limited + extent necessary to allow You to exercise the Licensed + Rights, but not otherwise. + + 2. Patent and trademark rights are not licensed under this + Public License. + + 3. To the extent possible, the Licensor waives any right to + collect royalties from You for the exercise of the Licensed + Rights, whether directly or through a collecting society + under any voluntary or waivable statutory or compulsory + licensing scheme. In all other cases the Licensor expressly + reserves any right to collect such royalties. + + +Section 3 -- License Conditions. + +Your exercise of the Licensed Rights is expressly made subject to the +following conditions. + + a. Attribution. + + 1. If You Share the Licensed Material (including in modified + form), You must: + + a. retain the following if it is supplied by the Licensor + with the Licensed Material: + + i. identification of the creator(s) of the Licensed + Material and any others designated to receive + attribution, in any reasonable manner requested by + the Licensor (including by pseudonym if + designated); + + ii. a copyright notice; + + iii. a notice that refers to this Public License; + + iv. a notice that refers to the disclaimer of + warranties; + + v. a URI or hyperlink to the Licensed Material to the + extent reasonably practicable; + + b. indicate if You modified the Licensed Material and + retain an indication of any previous modifications; and + + c. indicate the Licensed Material is licensed under this + Public License, and include the text of, or the URI or + hyperlink to, this Public License. + + 2. You may satisfy the conditions in Section 3(a)(1) in any + reasonable manner based on the medium, means, and context in + which You Share the Licensed Material. For example, it may be + reasonable to satisfy the conditions by providing a URI or + hyperlink to a resource that includes the required + information. + + 3. If requested by the Licensor, You must remove any of the + information required by Section 3(a)(1)(A) to the extent + reasonably practicable. + + 4. If You Share Adapted Material You produce, the Adapter's + License You apply must not prevent recipients of the Adapted + Material from complying with this Public License. + + +Section 4 -- Sui Generis Database Rights. + +Where the Licensed Rights include Sui Generis Database Rights that +apply to Your use of the Licensed Material: + + a. for the avoidance of doubt, Section 2(a)(1) grants You the right + to extract, reuse, reproduce, and Share all or a substantial + portion of the contents of the database; + + b. if You include all or a substantial portion of the database + contents in a database in which You have Sui Generis Database + Rights, then the database in which You have Sui Generis Database + Rights (but not its individual contents) is Adapted Material; and + + c. You must comply with the conditions in Section 3(a) if You Share + all or a substantial portion of the contents of the database. + +For the avoidance of doubt, this Section 4 supplements and does not +replace Your obligations under this Public License where the Licensed +Rights include other Copyright and Similar Rights. + + +Section 5 -- Disclaimer of Warranties and Limitation of Liability. + + a. UNLESS OTHERWISE SEPARATELY UNDERTAKEN BY THE LICENSOR, TO THE + EXTENT POSSIBLE, THE LICENSOR OFFERS THE LICENSED MATERIAL AS-IS + AND AS-AVAILABLE, AND MAKES NO REPRESENTATIONS OR WARRANTIES OF + ANY KIND CONCERNING THE LICENSED MATERIAL, WHETHER EXPRESS, + IMPLIED, STATUTORY, OR OTHER. THIS INCLUDES, WITHOUT LIMITATION, + WARRANTIES OF TITLE, MERCHANTABILITY, FITNESS FOR A PARTICULAR + PURPOSE, NON-INFRINGEMENT, ABSENCE OF LATENT OR OTHER DEFECTS, + ACCURACY, OR THE PRESENCE OR ABSENCE OF ERRORS, WHETHER OR NOT + KNOWN OR DISCOVERABLE. WHERE DISCLAIMERS OF WARRANTIES ARE NOT + ALLOWED IN FULL OR IN PART, THIS DISCLAIMER MAY NOT APPLY TO YOU. + + b. TO THE EXTENT POSSIBLE, IN NO EVENT WILL THE LICENSOR BE LIABLE + TO YOU ON ANY LEGAL THEORY (INCLUDING, WITHOUT LIMITATION, + NEGLIGENCE) OR OTHERWISE FOR ANY DIRECT, SPECIAL, INDIRECT, + INCIDENTAL, CONSEQUENTIAL, PUNITIVE, EXEMPLARY, OR OTHER LOSSES, + COSTS, EXPENSES, OR DAMAGES ARISING OUT OF THIS PUBLIC LICENSE OR + USE OF THE LICENSED MATERIAL, EVEN IF THE LICENSOR HAS BEEN + ADVISED OF THE POSSIBILITY OF SUCH LOSSES, COSTS, EXPENSES, OR + DAMAGES. WHERE A LIMITATION OF LIABILITY IS NOT ALLOWED IN FULL OR + IN PART, THIS LIMITATION MAY NOT APPLY TO YOU. + + c. The disclaimer of warranties and limitation of liability provided + above shall be interpreted in a manner that, to the extent + possible, most closely approximates an absolute disclaimer and + waiver of all liability. + + +Section 6 -- Term and Termination. + + a. This Public License applies for the term of the Copyright and + Similar Rights licensed here. However, if You fail to comply with + this Public License, then Your rights under this Public License + terminate automatically. + + b. Where Your right to use the Licensed Material has terminated under + Section 6(a), it reinstates: + + 1. automatically as of the date the violation is cured, provided + it is cured within 30 days of Your discovery of the + violation; or + + 2. upon express reinstatement by the Licensor. + + For the avoidance of doubt, this Section 6(b) does not affect any + right the Licensor may have to seek remedies for Your violations + of this Public License. + + c. For the avoidance of doubt, the Licensor may also offer the + Licensed Material under separate terms or conditions or stop + distributing the Licensed Material at any time; however, doing so + will not terminate this Public License. + + d. Sections 1, 5, 6, 7, and 8 survive termination of this Public + License. + + +Section 7 -- Other Terms and Conditions. + + a. The Licensor shall not be bound by any additional or different + terms or conditions communicated by You unless expressly agreed. + + b. Any arrangements, understandings, or agreements regarding the + Licensed Material not stated herein are separate from and + independent of the terms and conditions of this Public License. + + +Section 8 -- Interpretation. + + a. For the avoidance of doubt, this Public License does not, and + shall not be interpreted to, reduce, limit, restrict, or impose + conditions on any use of the Licensed Material that could lawfully + be made without permission under this Public License. + + b. To the extent possible, if any provision of this Public License is + deemed unenforceable, it shall be automatically reformed to the + minimum extent necessary to make it enforceable. If the provision + cannot be reformed, it shall be severed from this Public License + without affecting the enforceability of the remaining terms and + conditions. + + c. No term or condition of this Public License will be waived and no + failure to comply consented to unless expressly agreed to by the + Licensor. + + d. Nothing in this Public License constitutes or may be interpreted + as a limitation upon, or waiver of, any privileges and immunities + that apply to the Licensor or You, including from the legal + processes of any jurisdiction or authority. + +- - - - + +BSD 3-Clause License + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions are met: + +1. Redistributions of source code must retain the above copyright notice, this + list of conditions and the following disclaimer. + +2. Redistributions in binary form must reproduce the above copyright notice, + this list of conditions and the following disclaimer in the documentation + and/or other materials provided with the distribution. + +3. Neither the name of the copyright holder nor the names of its + contributors may be used to endorse or promote products derived from + this software without specific prior written permission. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" +AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE +IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE +DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE +FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL +DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR +SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER +CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, +OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +- - - - diff --git a/LICENSE.md b/LICENSE.md deleted file mode 100644 index ca197c0..0000000 --- a/LICENSE.md +++ /dev/null @@ -1,10 +0,0 @@ -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) - diff --git a/Makefile b/Makefile new file mode 100644 index 0000000..5ab728b --- /dev/null +++ b/Makefile @@ -0,0 +1,27 @@ +SHELL=/bin/bash -o pipefail +.PHONY: local remote deploy review + +remote: index.bs + @ (HTTP_STATUS=$$(curl https://api.csswg.org/bikeshed/ \ + --output index.html \ + --write-out "%{http_code}" \ + --header "Accept: text/plain, text/html" \ + -F die-on=warning \ + -F md-Text-Macro="COMMIT-SHA LOCAL COPY" \ + -F file=@index.bs) && \ + [[ "$$HTTP_STATUS" -eq "200" ]]) || ( \ + echo ""; cat index.html; echo ""; \ + rm -f index.html; \ + exit 22 \ + ); + +local: index.bs + bikeshed spec index.bs index.html --md-Text-Macro="COMMIT-SHA LOCAL COPY" + +deploy: index.bs + curl --remote-name --fail https://resources.whatwg.org/build/deploy.sh + bash ./deploy.sh + +review: index.bs + curl --remote-name --fail https://resources.whatwg.org/build/review.sh + bash ./review.sh diff --git a/PULL_REQUEST_TEMPLATE.md b/PULL_REQUEST_TEMPLATE.md new file mode 100644 index 0000000..4a9c02e --- /dev/null +++ b/PULL_REQUEST_TEMPLATE.md @@ -0,0 +1,15 @@ + + +- [ ] At least two implementers are interested (and none opposed): + * … + * … +- [ ] [Tests](https://github.com/web-platform-tests/wpt) are written and can be reviewed and commented upon at: + * … +- [ ] [Implementation bugs](https://github.com/whatwg/meta/blob/main/MAINTAINERS.md#handling-pull-requests) are filed: + * Chrome: … + * Firefox: … + * Safari: … + +(See [WHATWG Working Mode: Changes](https://whatwg.org/working-mode#changes) for more details.) diff --git a/README.md b/README.md index 2ae7d11..5897eef 100644 --- a/README.md +++ b/README.md @@ -1,32 +1,42 @@ -# 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-nativefs](https://github.com/GoogleChromeLabs/browser-nativefs) - 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 System Standard + +This repository hosts the +[File System Standard](https://fs.spec.whatwg.org/). + +## Code of conduct + +We are committed to providing a friendly, safe, and welcoming environment for all. Please read and +respect the [WHATWG Code of Conduct](https://whatwg.org/code-of-conduct). + +## Contribution opportunities + +Folks notice minor and larger issues with the File System Standard all the time and we'd love your +help fixing those. Pull requests for typographical and grammar errors are also most welcome. + +We'd be happy to mentor you through this process. If you're interested and need help getting +started, leave a comment on the issue or ask around [on chat](https://whatwg.org/chat). + +## Pull requests + +In short, change `index.bs` and submit your patch, with a +[good commit message](https://github.com/whatwg/meta/blob/main/COMMITTING.md). Consider +reading through the [WHATWG FAQ](https://whatwg.org/faq) if you are new here. + +Please add your name to the Acknowledgments section in your first pull request, even for trivial +fixes. The names are sorted lexicographically. + +## Building "locally" + +For quick local iteration, run `make`. To verify your changes locally, run `make deploy`. See more +in the +[WHATWG Contributor Guidelines](https://github.com/whatwg/meta/blob/main/CONTRIBUTING.md#building). + +## Merge policy + +If you can commit to this repository, see the +[WHATWG Maintainer Guidelines](https://github.com/whatwg/meta/blob/main/MAINTAINERS.md). + +## Tests + +Tests can be found in the `fs/` directory of +[web-platform-tests/wpt](https://github.com/web-platform-tests/wpt). diff --git a/SuggestedNameAndDir.md b/SuggestedNameAndDir.md deleted file mode 100644 index 751000c..0000000 --- a/SuggestedNameAndDir.md +++ /dev/null @@ -1,321 +0,0 @@ -# Suggested file name and location - -## Authors: - -* Marijn Kruisselbrink - -## Participate - -* [Issue tracker](https://github.com/WICG/file-system-access/issues) - -## Table of Contents - - - - -- [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) - - - -## 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 `` 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 diff --git a/changes.md b/changes.md deleted file mode 100644 index 371905c..0000000 --- a/changes.md +++ /dev/null @@ -1,169 +0,0 @@ -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(); -``` diff --git a/index.bs b/index.bs index a020bec..7a2fb66 100644 --- a/index.bs +++ b/index.bs @@ -1,18 +1,10 @@
-Title: File System Access
-Shortname: file-system-access
-Abstract: This document defines a web platform API that enables 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: Marijn Kruisselbrink, Google, mek@chromium.org, w3cid 72440
-Group: WICG
-Repository: wicg/file-system-access
+Group: WHATWG
+H1: File System
+Shortname: fs
+Text Macro: TWITTER whatfilesystem
+Abstract: File System defines infrastructure for file systems as well as their API.
 Indent: 2
-Complain About: accidental-2119 yes, missing-example-ids yes
 Markup Shorthands: css no, markdown yes
 
@@ -21,7 +13,7 @@ spec:webidl; type:dfn; text:resolve
-urlPrefix: https://tc39.github.io/ecma262/; spec: ECMA-262
+urlPrefix: https://tc39.es/ecma262/; spec: ECMA-262
   type: dfn; text: realm; url: realm
 urlPrefix: https://storage.spec.whatwg.org/; spec: storage
   type: dfn; text: storage; url: site-storage
@@ -47,44 +39,37 @@ urlPrefix: https://storage.spec.whatwg.org/; spec: storage
 }
 
 
+
 # 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 ``
-and ``
-[[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.
-
-Additionally this API also makes 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. The entry point for this is the
-{{StorageManager/getDirectory()|navigator.storage.getDirectory()}} method. This
-is similar to the temporary file system as defined in earlier drafts of
-[[file-system-api|File API: Directories and System]].
+This document defines fundamental infrastructure for file system APIs. In addition, it defines an
+API that makes it possible for websites to get access to a file system directory without having to
+first prompt the user for access. This enables use cases where a website wants to save data to disk
+before a user has picked a location to save to, without forcing the website to use a completely
+different storage mechanism with a different API for such files. The entry point for this is the
+{{StorageManager/getDirectory()|navigator.storage.getDirectory()}} method.
+
 
 # Files and Directories # {#files-and-directories}
 
 ## Concepts ## {#concepts}
 
 An entry is either a [=file entry=] or a [=directory entry=].
+
+
+Each [=/entry=] has an associated query access algorithm, which takes "`read`"
+or "`readwrite`" mode and returns a {{PermissionState}}. Unless specified
+otherwise it returns "{{PermissionState/denied}}". The algorithm is allowed to throw.
+
+Each [=/entry=] has an associated request access algorithm, which takes
+"`read`" or "`readwrite`" mode and returns a {{PermissionState}}. Unless specified
+otherwise it returns "{{PermissionState/denied}}". The algorithm is allowed to throw.
+
+Note: Implementations that only implement this specification and not dependent specifications do not
+need to bother implementing [=/entry=]'s [=entry/query access=] and [=entry/request access=].
 
 Each [=/entry=] has an associated name (a [=string=]).
 
@@ -100,14 +85,6 @@ Issue: We should consider having further normative restrictions on file names th
 never be allowed using this API, rather than leaving it entirely up to underlying file
 systems.
 
-A valid suffix code point 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.
-
 A file entry additionally consists of
 binary data (a [=byte sequence=]) and a
 modification timestamp (a number representing the number of milliseconds since the Unix Epoch).
@@ -121,10 +98,7 @@ An [=/entry=]'s [=entry/parent=] is null if no such directory entry exists.
 
 Note: Two different [=/entries=] can represent the same file or directory on disk, in which
 case it is possible for both entries to have a different parent, or for one entry to have a
-parent while the other entry does not have a parent. Typically an entry does not have a parent
-if it was returned by {{StorageManager/getDirectory()|navigator.storage.getDirectory()}}
-or one of the [=local file system handle factories=],
-and an entry will have a parent in all other cases.
+parent while the other entry does not have a parent.
 
 [=/Entries=] can (but don't have to) be backed by files on the host operating system's local file system,
 so it is possible for the [=binary data=], [=modification timestamp=],
@@ -161,97 +135,9 @@ run the following steps:
 
 
 
-## Permissions ## {#permissions}
-
-The "file-system" [=powerful feature=]'s
-permission-related algorithms and types are defined as follows:
-
-: [=permission descriptor type=]
-:: {{FileSystemPermissionDescriptor}}, defined as:
-  
-  enum FileSystemPermissionMode {
-    "read",
-    "readwrite"
-  };
-
-  dictionary FileSystemPermissionDescriptor : PermissionDescriptor {
-    required FileSystemHandle handle;
-    FileSystemPermissionMode mode = "read";
-  };
-  
-
-: [=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 an [=/entry=] in an [=origin private file system=], - this descriptor's [=permission state=] must always be {{PermissionState/"granted"}}. - 1. Otherwise, if |entry|'s [=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 [=entry/parent=]. - 1. Otherwise, if |desc|.{{FileSystemPermissionDescriptor/mode}} is {{"readwrite"}}: - 1. Let |read state| be the [=permission state=] for a descriptor - with the same {{FileSystemPermissionDescriptor/handle}}, - but {{FileSystemPermissionDescriptor/mode}} = {{"read"}}. - 1. If |read state| is not {{PermissionState/"granted"}}, this descriptor's [=permission state=] - must be equal to |read state|. - -: [=permission request algorithm=] -::
- Given a {{FileSystemPermissionDescriptor}} |desc| and a {{PermissionStatus}} |status|, - run these steps: - - 1. Run the [=boolean permission query algorithm=] on |desc| and |status|. - 1. If |status|.{{PermissionStatus/state}} is not {{PermissionState/"prompt"}}, abort. - 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}}, throw a {{SecurityError}}. - 1. If |global| does not have [=transient activation=], throw a {{SecurityError}}. - 1. If |settings|'s [=environment settings object/origin=] is not [=same origin=] with |settings|'s [=top-level origin=], - throw a {{SecurityError}}. - 1. [=Request permission to use=] |desc|. - 1. Run the [=boolean permission query algorithm=] on |desc| and |status|. - - Issue(WICG/permissions-request#2): Ideally this user activation requirement would be defined upstream. - -
-To query file system permission -given a {{FileSystemHandle}} |handle| and a {{FileSystemPermissionMode}} |mode|, run these steps: - -1. Let |desc| be a {{FileSystemPermissionDescriptor}}. -1. Set |desc|.{{PermissionDescriptor/name}} to {{"file-system"}}. -1. Set |desc|.{{FileSystemPermissionDescriptor/handle}} to |handle|. -1. Set |desc|.{{FileSystemPermissionDescriptor/mode}} to |mode|. -1. Return |desc|'s [=permission state=]. - -
- -
-To request file system permission -given a {{FileSystemHandle}} |handle| and a {{FileSystemPermissionMode}} |mode|, run these steps: - -1. Let |desc| be a {{FileSystemPermissionDescriptor}}. -1. Set |desc|.{{PermissionDescriptor/name}} to {{"file-system"}}. -1. Set |desc|.{{FileSystemPermissionDescriptor/handle}} to |handle|. -1. Set |desc|.{{FileSystemPermissionDescriptor/mode}} to |mode|. -1. Let |status| be the result of running create a PermissionStatus for |desc|. -1. Run the [=permission request algorithm=] for the {{"file-system"}} feature, given |desc| and |status|. -1. Return |desc|'s [=permission state=]. - -
- -Issue(119): Currently {{FileSystemPermissionMode}} can only be {{"read"}} or {{"readwrite"}}. In -the future we might want to add a "write" mode as well to support write-only handles. - ## The {{FileSystemHandle}} interface ## {#api-filesystemhandle} -dictionary FileSystemHandlePermissionDescriptor { - FileSystemPermissionMode mode = "read"; -}; - enum FileSystemHandleKind { "file", "directory", @@ -263,9 +149,6 @@ interface FileSystemHandle { readonly attribute USVString name; Promise<boolean> isSameEntry(FileSystemHandle other); - - Promise<PermissionState> queryPermission(optional FileSystemHandlePermissionDescriptor descriptor = {}); - Promise<PermissionState> requestPermission(optional FileSystemHandlePermissionDescriptor descriptor = {}); }; @@ -276,9 +159,6 @@ the {{FileSystemHandle}} interface can all be associated with the same [=/entry=
{{FileSystemHandle}} objects are [=serializable objects=]. -Advisement: In the Origin Trial as available in Chrome 78, these objects are not yet serializable. -In Chrome 82 they are. - Their [=serialization steps=], given |value|, |serialized| and forStorage are: 1. Set |serialized|.\[[Origin]] to |value|'s [=relevant settings object=]'s [=environment settings object/origin=]. @@ -322,8 +202,6 @@ associated [=FileSystemHandle/entry=]. :: Returns true if |handle1| and |handle2| represent the same file or directory.
-Advisement: This method is first available in Chrome 82. -
The isSameEntry(|other|) method, when invoked, must run these steps: @@ -337,77 +215,6 @@ The isSameEntry(|other|) method, when inv
-### The {{FileSystemHandle/queryPermission()}} method ### {#api-filesystemhandle-querypermission} - -
- : |status| = await |handle| . {{FileSystemHandle/queryPermission()|queryPermission}}({ {{FileSystemHandlePermissionDescriptor/mode}} : {{"read"}} }) - : |status| = await |handle| . {{FileSystemHandle/queryPermission()}} - : |status| = (await navigator.{{Navigator/permissions}}.{{Permissions/query()|query}}({ {{PermissionDescriptor/name}} : "file-system", {{FileSystemPermissionDescriptor/handle}} : |handle| })).{{PermissionStatus/state}} - :: Queries the current state of the read permission of this handle. If this returns `"prompt"` - the website will have to call {{FileSystemHandle/requestPermission()}} before any - operations on the handle can be done. If this returns `"denied"` any operations will reject. - - Usually handles returned by the [=local file system handle factories=] will initially return `"granted"` for - their read permission state, however other than through the user revoking permission, a handle - retrieved from IndexedDB is also likely to return `"prompt"`. - - : |status| = await |handle| . {{FileSystemHandle/queryPermission()|queryPermission}}({ {{FileSystemHandlePermissionDescriptor/mode}} : {{"readwrite"}} }) - : |status| = (await navigator.{{Navigator/permissions}}.{{Permissions/query()|query}}({ {{PermissionDescriptor/name}} : "file-system", {{FileSystemPermissionDescriptor/handle}} : |handle|, {{FileSystemPermissionDescriptor/mode}} : {{"readwrite"}}}).{{PermissionStatus/state}} - :: Queries the current state of the write permission of this handle. If this returns `"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 `"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. -
- -Advisement: The integration with the permissions API's {{Permissions/query()}} method is not yet implemented in Chrome. - -
-The queryPermission(|descriptor|) 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 [=this=] and |descriptor|.{{FileSystemHandlePermissionDescriptor/mode}}. - 1. [=/Resolve=] |result| with |state|. -1. Return |result|. - -
- -### The {{FileSystemHandle/requestPermission()}} method ### {#api-filesystemhandle-requestpermission} - -
- : |status| = await |handle| . {{FileSystemHandle/requestPermission()|requestPermission}}({ {{FileSystemHandlePermissionDescriptor/mode}} : {{"read"}} }) - : |status| = await |handle| . {{FileSystemHandle/requestPermission()}} - :: If the state of the read permission of this handle is anything other than `"prompt"`, this - will return that state directly. If it is `"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}} : {{"readwrite"}} }) - :: If the state of the write permission of this handle is anything other than `"prompt"`, this - will return that state directly. If the status of the read permission of this handle is - `"denied"` this will return that. - - Otherwise the state of the write permission is `"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. -
- -
-The requestPermission(|descriptor|) 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 [=this=] and |descriptor|.{{FileSystemHandlePermissionDescriptor/mode}}. - If that throws an exception, [=reject=] |result| with that exception and abort. - 1. [=/Resolve=] |result| with |state|. -1. Return |result|. - -
- ## The {{FileSystemFileHandle}} interface ## {#api-filesystemfilehandle} @@ -427,9 +234,6 @@ A {{FileSystemFileHandle}}'s associated [=FileSystemHandle/entry=] must be a [=f {{FileSystemFileHandle}} objects are [=serializable objects=]. Their [=serialization steps=] and [=deserialization steps=] are the same as those for {{FileSystemHandle}}. -Advisement: In the Origin Trial as available in Chrome 78, these objects are not yet serializable. -In Chrome 82 they are. - ### The {{FileSystemFileHandle/getFile()}} method ### {#api-filesystemfilehandle-getfile} <div class="note domintro"> @@ -444,17 +248,17 @@ The <dfn method for=FileSystemFileHandle>getFile()</dfn> method, when invoked, m 1. Let |result| be [=a new promise=]. 1. Run the following steps [=in parallel=]: - 1. Let |permissionStatus| be the result of [=querying file system permission=] - given <b>[=this=]</b> and {{"read"}}. - 1. If |permissionStatus| is not {{PermissionState/"granted"}}, + 1. Let |access| be the result of running [=this=]'s [=FileSystemHandle/entry=]'s + [=entry/query access=] given "`read`". + 1. If |access| is not "{{PermissionState/granted}}", reject |result| with a {{NotAllowedError}} and abort. - 1. Let |entry| be <b>[=this=]</b>'s [=FileSystemHandle/entry=]. + 1. Let |entry| be [=this=]'s [=FileSystemHandle/entry=]. 1. Let |f| be a new {{File}}. 1. Set |f|'s <a spec=FileAPI>snapshot state</a> to the current state of |entry|. 1. Set |f|'s underlying byte sequence to a copy of |entry|'s [=binary data=]. 1. Initialize the value of |f|'s {{File/name}} attribute to |entry|'s [=entry/name=]. 1. Initialize the value of |f|'s {{File/lastModified}} attribute to |entry|'s [=file entry/modification timestamp=]. - 1. Initialize the value of |f|'s {{Blob/type}} attribute to an implementation defined value, based on for example |entry|'s [=entry/name=] and/or its file extension. + 1. Initialize the value of |f|'s {{Blob/type}} attribute to an [=implementation-defined=] value, based on for example |entry|'s [=entry/name=] or its file extension. Issue: The reading and snapshotting behavior needs to be better specified in the [[FILE-API]] spec, for now this is kind of hand-wavy. @@ -465,8 +269,6 @@ The <dfn method for=FileSystemFileHandle>getFile()</dfn> method, when invoked, m ### The {{FileSystemFileHandle/createWritable()}} method ### {#api-filesystemfilehandle-createwritable} -Advisement: In the Origin Trial as available in Chrome 82, createWritable replaces the createWriter method. - <div class="note domintro"> : |stream| = await |fileHandle| . {{FileSystemFileHandle/createWritable()}} : |stream| = await |fileHandle| . {{FileSystemFileHandle/createWritable()|createWritable}}({ {{FileSystemCreateWritableOptions/keepExistingData}}: true/false }) @@ -496,14 +298,14 @@ The <dfn method for=FileSystemFileHandle>createWritable(|options|)</dfn> method, 1. Let |result| be [=a new promise=]. 1. Run the following steps [=in parallel=]: - 1. Let |permissionStatus| be the result of [=requesting file system permission=] - given <b>[=this=]</b> and {{"readwrite"}}. + 1. Let |access| be the result of running [=this=]'s [=FileSystemHandle/entry=]'s + [=entry/request access=] given "`readwrite`". If that throws an exception, [=reject=] |result| with that exception and abort. - 1. If |permissionStatus| is not {{PermissionState/"granted"}}, + 1. If |access| is not "{{PermissionState/granted}}", reject |result| with a {{NotAllowedError}} and abort. - 1. Let |entry| be <b>[=this=]</b>'s [=FileSystemHandle/entry=]. + 1. Let |entry| be [=this=]'s [=FileSystemHandle/entry=]. 1. Let |stream| be the result of [=create a new FileSystemWritableFileStream|creating a new FileSystemWritableFileStream=] - for |entry| in <b>[=this=]</b>'s [=relevant realm=]. + for |entry| in [=this=]'s [=relevant realm=]. 1. If |options|.{{FileSystemCreateWritableOptions/keepExistingData}} is `true`: 1. Set |stream|.[=[[buffer]]=] to a copy of |entry|'s [=file entry/binary data=]. 1. [=/Resolve=] |result| with |stream|. @@ -544,12 +346,6 @@ A {{FileSystemDirectoryHandle}}'s associated [=FileSystemHandle/entry=] must be {{FileSystemDirectoryHandle}} objects are [=serializable objects=]. Their [=serialization steps=] and [=deserialization steps=] are the same as those for {{FileSystemHandle}}. -Advisement: In the Origin Trial as available in Chrome 78, these objects are not yet serializable. -In Chrome 82 they are. - -Advisement: In Chrome versions upto Chrome 85 `getFileHandle` and `getDirectoryHandle` where -called `getFile` and `getDirectory` instead. - ### Directory iteration ### {#api-filesystemdirectoryhandle-asynciterable} <div class="note domintro"> @@ -562,10 +358,6 @@ called `getFile` and `getDirectory` instead. No guarantees are given either way. </div> -Advisement: In Chrome this is currently implemented as a `directoryHandle.getEntries()` method that can be used in a `for await..of` loop. -This `getEntries()` method returns more or less the same async iterable as what is returned by `values()` in this specification. -The proper async iterable declaration is not yet implemented. - Issue(173): In the future we might want to add arguments to the async iterable declaration to support for example recursive iteration. @@ -573,10 +365,10 @@ support for example recursive iteration. The [=asynchronous iterator initialization steps=] for a {{FileSystemDirectoryHandle}} |handle| ant its async iterator |iterator| are: -1. Let |permissionStatus| be the result of [=querying file system permission=] - given |handle| and {{"read"}}. +1. Let |access| be the result of running |handle|'s [=FileSystemHandle/entry=]'s + [=entry/query access=] given "`read`". -1. If |permissionStatus| is not {{PermissionState/"granted"}}, +1. If |access| is not "{{PermissionState/granted}}", throw a {{NotAllowedError}}. 1. Set |iterator|'s <dfn for="FileSystemDirectoryHandle-iterator">past results</dfn> to an empty [=/set=]. @@ -591,10 +383,10 @@ and its async iterator |iterator|: 1. Let |directory| be |handle|'s [=FileSystemHandle/entry=]. -1. Let |permissionStatus| be the result of [=querying file system permission=] - given |handle| and {{"read"}}. +1. Let |access| be the result of running |handle|'s [=FileSystemHandle/entry=]'s + [=entry/query access=] given "`read`". -1. If |permissionStatus| is not {{PermissionState/"granted"}}, +1. If |access| is not "{{PermissionState/granted}}", reject |promise| with a {{NotAllowedError}} and return |promise|. 1. Let |child| be an [=/entry=] in |directory|'s [=directory entry/children=], @@ -650,15 +442,15 @@ must run these steps: 1. Run the following steps [=in parallel=]: 1. If |name| is not a [=valid file name=], [=/reject=] |result| with a {{TypeError}} and abort. - 1. Let |entry| be <b>[=this=]</b>'s [=FileSystemHandle/entry=]. + 1. Let |entry| be [=this=]'s [=FileSystemHandle/entry=]. 1. If |options|.{{FileSystemGetFileOptions/create}} is `true`: - 1. Let |permissionStatus| be the result of [=requesting file system permission=] - given <b>[=this=]</b> and {{"readwrite"}}. + 1. Let |access| be the result of running [=this=]'s [=FileSystemHandle/entry=]'s + [=entry/request access=] given "`readwrite`". If that throws an exception, [=reject=] |result| with that exception and abort. 1. Otherwise: - 1. Let |permissionStatus| be the result of [=querying file system permission=] - given <b>[=this=]</b> and {{"read"}}. - 1. If |permissionStatus| is not {{PermissionState/"granted"}}, + 1. Let |access| be the result of running [=this=]'s [=FileSystemHandle/entry=]'s + [=entry/query access=] given "`read`". + 1. If |access| is not "{{PermissionState/granted}}", reject |result| with a {{NotAllowedError}} and abort. 1. [=set/For each=] |child| of |entry|'s [=directory entry/children=]: @@ -668,7 +460,8 @@ must run these steps: 1. [=/Resolve=] |result| with a new {{FileSystemFileHandle}} whose [=FileSystemHandle/entry=] is |child| and abort. 1. If |options|.{{FileSystemGetFileOptions/create}} is `false`: 1. [=/Reject=] |result| with a {{NotFoundError}} and abort. - 1. Let |child| be a new [=file entry=]. + 1. Let |child| be a new [=file entry=] whose [=query access=] and [=request access=] algorithms + are those of |entry|. 1. Set |child|'s [=entry/name=] to |name|. 1. Set |child|'s [=binary data=] to an empty [=byte sequence=]. 1. Set |child|'s [=modification timestamp=] to the current time. @@ -711,15 +504,15 @@ invoked, must run these steps: 1. Run the following steps [=in parallel=]: 1. If |name| is not a [=valid file name=], [=/reject=] |result| with a {{TypeError}} and abort. - 1. Let |entry| be <b>[=this=]</b>'s [=FileSystemHandle/entry=]. + 1. Let |entry| be [=this=]'s [=FileSystemHandle/entry=]. 1. If |options|.{{FileSystemGetDirectoryOptions/create}} is `true`: - 1. Let |permissionStatus| be the result of [=requesting file system permission=] - given <b>[=this=]</b> and {{"readwrite"}}. + 1. Let |access| be the result of running [=this=]'s [=FileSystemHandle/entry=]'s + [=entry/request access=] given "`readwrite`". If that throws an exception, [=reject=] |result| with that exception and abort. 1. Otherwise: - 1. Let |permissionStatus| be the result of [=querying file system permission=] - given <b>[=this=]</b> and {{"read"}}. - 1. If |permissionStatus| is not {{PermissionState/"granted"}}, + 1. Let |access| be the result of running [=this=]'s [=FileSystemHandle/entry=]'s + [=entry/query access=] given "`read`". + 1. If |access| is not "{{PermissionState/granted}}", reject |result| with a {{NotAllowedError}} and abort. 1. [=set/For each=] |child| of |entry|'s [=directory entry/children=]: @@ -729,7 +522,8 @@ invoked, must run these steps: 1. [=/Resolve=] |result| with a new {{FileSystemDirectoryHandle}} whose [=FileSystemHandle/entry=] is |child| and abort. 1. If |options|.{{FileSystemGetFileOptions/create}} is `false`: 1. [=/Reject=] |result| with a {{NotFoundError}} and abort. - 1. Let |child| be a new [=directory entry=]. + 1. Let |child| be a new [=directory entry=] whose [=query access=] and [=request access=] + algorithms are those of |entry|. 1. Set |child|'s [=entry/name=] to |name|. 1. Set |child|'s [=directory entry/children=] to an empty [=/set=]. 1. [=set/Append=] |child| to |entry|'s [=directory entry/children=]. @@ -769,11 +563,11 @@ these steps: 1. Run the following steps [=in parallel=]: 1. If |name| is not a [=valid file name=], [=/reject=] |result| with a {{TypeError}} and abort. - 1. Let |entry| be <b>[=this=]</b>'s [=FileSystemHandle/entry=]. - 1. Let |permissionStatus| be the result of [=requesting file system permission=] - given <b>[=this=]</b> and {{"readwrite"}}. + 1. Let |entry| be [=this=]'s [=FileSystemHandle/entry=]. + 1. Let |access| be the result of running [=this=]'s [=FileSystemHandle/entry=]'s + [=entry/request access=] given "`readwrite`". If that throws an exception, [=reject=] |result| with that exception and abort. - 1. If |permissionStatus| is not {{PermissionState/"granted"}}, + 1. If |access| is not "{{PermissionState/granted}}", reject |result| with a {{NotAllowedError}} and abort. 1. [=set/For each=] |child| of |entry|'s [=directory entry/children=]: @@ -814,8 +608,6 @@ these steps: listing to highlight a file opened through a file picker in that directory listing. </div> -Advisement: This method is first available in Chrome 82. - <div class=example id=filesystemdirectoryhandle-resolve-example> <xmp highlight=js> // Assume we at some point got a valid directory handle. @@ -926,12 +718,10 @@ in a [=/Realm=] |realm|, perform the following steps: 1. Let |closeAlgorithm| be the following steps: 1. Let |closeResult| be [=a new promise=]. 1. Run the following steps [=in parallel=]: - 1. Let |permissionStatus| be the [=permission state=] for a {{FileSystemPermissionDescriptor}} - with {{FileSystemPermissionDescriptor/handle}} representing |stream|.[=[[file]]=], - and {{FileSystemPermissionDescriptor/mode}} = {{"readwrite"}}. - 1. If |permissionStatus| is not {{PermissionState/"granted"}}, + 1. Let |access| be the result of running |file|'s [=entry/query access=] given "`readwrite`". + 1. If |access| is not "{{PermissionState/granted}}", reject |closeResult| with a {{NotAllowedError}} and abort. - 1. Perform user agent-specific [=malware scans and safe browsing checks=]. + 1. Perform [=implementation-defined=] malware scans and safe browsing checks. If these checks fail, [=/reject=] |closeResult| with an {{AbortError}} and abort. 1. Set |stream|.[=[[file]]=]'s [=file entry/binary data=] to |stream|.[=[[buffer]]=]. If that throws an exception, [=/reject=] |closeResult| with that exception and abort. @@ -960,10 +750,9 @@ runs these steps: If this throws an exception, then return [=a promise rejected with=] that exception. 1. Let |p| be [=a new promise=]. 1. Run the following steps [=in parallel=]: - 1. Let |permissionStatus| be the [=permission state=] for a {{FileSystemPermissionDescriptor}} - with {{FileSystemPermissionDescriptor/handle}} representing |stream|.[=[[file]]=], - and {{FileSystemPermissionDescriptor/mode}} = {{"readwrite"}}. - 1. If |permissionStatus| is not {{PermissionState/"granted"}}, + 1. Let |access| be the result of running |stream|'s [=FileSystemWritableFileStream/[[file]]=]'s + [=entry/query access=] given "`readwrite`". + 1. If |access| is not "{{PermissionState/granted}}", reject |p| with a {{NotAllowedError}} and abort. 1. Let |command| be |input|.{{WriteParams/type}} if |input| is a {{WriteParams}}, and {{WriteCommandType/"write"}} otherwise. @@ -1141,684 +930,6 @@ steps: </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; -}; - -[SecureContext] -partial interface Window { - Promise<sequence<FileSystemFileHandle>> showOpenFilePicker(optional OpenFilePickerOptions options = {}); - Promise<FileSystemFileHandle> showSaveFilePicker(optional SaveFilePickerOptions options = {}); - Promise<FileSystemDirectoryHandle> showDirectoryPicker(optional DirectoryPickerOptions options = {}); -}; - - -The {{showOpenFilePicker()}}, {{showSaveFilePicker()}} and {{showDirectoryPicker()}} methods -are together known as the local file system handle factories. - -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 {{"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 {{readwrite}} -should be {{PermissionState/"granted"}}. - -
-To verify that an |environment| is allowed to show a file picker, run these steps: - -1. If |environment|'s [=environment settings object/origin=] is an [=opaque origin=], - return [=a promise rejected with=] a {{SecurityError}}. - -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}}. - -1. Let |global| be |environment|'s [=environment settings object/global object=]. - -1. If |global| does not have [=transient activation=], throw a {{SecurityError}}. - -
- -## File picker options ## {#api-filepickeroptions} - -### Accepted file types ### {#api-filepickeroptions-types} - -
-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 optional {{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 should 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. - -
-const options = {
-  {{FilePickerOptions/types}}: [
-    {
-      {{FilePickerAcceptType/description}}: 'Text Files',
-      {{FilePickerAcceptType/accept}}: {
-        'text/plain': ['.txt', '.text'],
-        'text/html': ['.html', '.htm']
-      }
-    },
-    {
-      {{FilePickerAcceptType/description}}: 'Images',
-      {{FilePickerAcceptType/accept}}: {
-        'image/*': ['.png', '.gif', '.jpeg', '.jpg']
-      }
-    }
-  ],
-};
-
- -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. - -
-const options = {
-  {{FilePickerOptions/types}}: [
-    {
-      {{FilePickerAcceptType/accept}}: {
-        'image/svg+xml': '.svg'
-      }
-    },
-  ],
-  {{FilePickerOptions/excludeAcceptAllOption}}: true
-};
-
- -
- -
-To process accept types, given {{FilePickerOptions}} |options|, -run these steps: - -1. Let |accepts options| be a empty [=/list=] of [=pairs=]. -1. [=list/For each=] |type| of |options|.{{FilePickerOptions/types}}: - 1. Let |description| be |type|.{{FilePickerAcceptType/description}}. - 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, throw a {{TypeError}}. - 1. If |parsedType|'s [=MIME type/parameters=] are not empty, 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 the following 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. 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, throw a {{TypeError}}. - -1. Return |accepts options|. - -
- -
-To validate a suffix |suffix|, run the following steps: - -1. If |suffix| does not [=string/starts with|start with=] ".", throw a {{TypeError}}. -1. If |suffix| contains any [=code points=] that are not [=valid suffix code points=], - throw a {{TypeError}}. -1. If |suffix| ends with ".", throw a {{TypeError}}. -1. If |suffix|'s [=string/length=] is more than 16, throw a {{TypeError}}. - -
- -### Starting Directory ### {#api-filepickeroptions-starting-directory} - -
-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). - -
-// 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 = {
-  {{FilePickerOptions/id}}: 'foo',
-};
-
- -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}} project_dir, the following will show -a file picker that starts out in that directory: - -
-// The picker will open to the directory of |project_dir| regardless of whether
-// 'foo' has a valid mapping.
-const options = {
-  {{FilePickerOptions/id}}: 'foo',
-  {{FilePickerOptions/startIn}}: |project_dir|,
-};
-
- -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. - -
-// 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 = {
-  {{FilePickerOptions/id}}: 'foo',  // Unmapped.
-  {{FilePickerOptions/startIn}}: {{WellKnownDirectory/"downloads"}},  // 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 = {
-  {{FilePickerOptions/id}}: 'foo',  // Maybe mapped. If so, start here.
-  {{FilePickerOptions/startIn}}: {{WellKnownDirectory/"downloads"}},  // Otherwise, start here.
-};
-
- -
- -Advisement: The {{FilePickerOptions/startIn}} and {{FilePickerOptions/id}} options were first introduced in Chrome 91. - -A user agent holds a recently picked directory map, which is a -[=map=] of [=/origins=] to [=path id maps=]. - -A path id map is a [=map=] of [=valid path ids=] to paths. - -A valid path id 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 WellKnownDirectory 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: - -
-: "desktop" -:: 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`. -: "documents" -:: 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`. -: "downloads" -:: Directory where downloaded files would typically be stored. - For example `C:\Documents and Settings\username\Downloads`, `/Users/username/Downloads`, or `/home/username/Downloads`. -: "music" -:: 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`. -: "pictures" -:: 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`. -: "videos" -:: 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`. - -
-To determine the directory the picker will start in, 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=], throw a {{TypeError}}. -1. If |id|'s [=string/length=] is more than 32, throw a {{TypeError}}. - -1. Let |origin| be |environment|'s [=environment settings object/origin=]. - -1. If |startIn| is a {{FileSystemHandle}}: - 1. Let |entry| be |startIn|'s [=FileSystemHandle/entry=]. - 1. If |entry| does not represent an [=/entry=] in an [=origin private file system=]: - 1. If |entry| is a [=file entry=], and a path on the local file system - corresponding to the parent directory if |entry| can be determined, - then return that path. - 1. If |entry| is a [=directory entry=], and a path on the local file system - corresponding to |entry| can be determined, - then return that path. - -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. - -
- -
-To remember a picked directory, given an optional [=string=] |id|, -an [=/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. - -
- -## The {{showOpenFilePicker()}} method ## {#api-showopenfilepicker} - -
- : [ |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. -
- -
-The showOpenFilePicker(|options|) method, when invoked, must run -these steps: - -1. Let |environment| be [=this=]'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 some files. - If |options|.{{OpenFilePickerOptions/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 the user dismissed the prompt without making a selection, - [=/reject=] |p| with an {{AbortError}} 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}} 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 activation notification 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|. - -
- -## The {{Window/showSaveFilePicker()}} method ## {#api-showsavefilepicker} - -
- : |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. -
- -Advisement: The {{SaveFilePickerOptions/suggestedName}} option was first introduced in Chrome 91. - -
-The showSaveFilePicker(|options|) method, when invoked, must run -these steps: - -1. Let |environment| be [=this=]'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}} is specified and 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 as a download. - - 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}} 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}} 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 activation notification 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|. - -
- -## The {{Window/showDirectoryPicker()}} method ## {#api-showdirectorypicker} - -
- : |handle| = await window . {{Window/showDirectoryPicker()}} - :: Shows a directory picker that lets the user select a single directory, returning a handle for - the selected directory. - - 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. -
- -
-The showDirectoryPicker(options) method, when invoked, must run -these steps: - -1. Let |environment| be [=this=]'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. 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 the user dismissed the prompt without making a selection, - [=/reject=] |p| with an {{AbortError}} 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}} 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. Perform the activation notification 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. - - Issue(89): Rather than requiring the website to prompt separately for a writable directory, - we should provide some kind of API to request a writable directory in one step. - - 1. [=/Resolve=] |p| with |result|. - -1. Return |p|. - -
- -## Drag and Drop ## {#drag-and-drop} - - -partial interface DataTransferItem { - Promise<FileSystemHandle?> getAsFileSystemHandle(); -}; - - -During a drag-and-drop operation, dragged file and -directory items are associated with [=file entries=] and [=directory entries=] -respectively. - -
- : |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. -
- -
- -The getAsFileSystemHandle() method steps are: - -1. If the {{DataTransferItem}} object is not in the read/write - mode or the read-only mode, return - [=a promise resolved with=] `null`. - -1. If the the drag data item kind is not File, - 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 [=/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|. - -
- -
-Handling drag and drop of files and directories: - -elem.addEventListener('dragover', (e) => { - // Prevent navigation. - e.preventDefault(); -}); -elem.addEventListener('drop', async (e) => { - // Prevent navigation. - e.preventDefault(); - - // Process all of the items. - for (const item of e.dataTransfer.items) { - // kind will be 'file' for file/directory entries. - if (item.kind === 'file') { - const entry = await item.getAsFileSystemHandle(); - if (entry.kind === 'file') { - handleFileEntry(entry); - } else if (entry.kind === 'directory') { - handleDirectoryEntry(entry); - } - } - } -}); - -
- -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. # Accessing the Origin Private File System # {#sandboxed-filesystem} @@ -1842,9 +953,6 @@ partial interface StorageManager { }; -Advisement: In Chrome this functionality was previously exposed as `FileSystemDirectoryHandle.getSystemDirectory({type: "sandbox"})`. -This new method is available as of Chrome 85. -
: |directoryHandle| = await navigator . storage . {{StorageManager/getDirectory()}} :: Returns the root directory of the origin private file system. @@ -1861,8 +969,9 @@ invoked, must run these steps: return [=a promise rejected with=] a {{SecurityError}}. 1. If |map|["root"] does not [=map/exist=]: - 1. Let |dir| be a new [=directory entry=]. - 1. Set |dir|'s [=entry/name=] to `""`. + 1. Let |dir| be a new [=directory entry=] whose [=query access=] and [=request access=] algorithms + always return "{{PermissionState/granted}}". + 1. Set |dir|'s [=entry/name=] to the empty string. 1. Set |dir|'s [=directory entry/children=] to an empty [=/set=]. 1. Set |map|["root"] to |dir|. @@ -1871,136 +980,14 @@ invoked, must run these steps:
-Note: In Chrome the directory entry returned by the above algorithm refers to the same storage as the -temporary -file system as used to be defined in [[file-system-api|File API: Directories and System]]. - -# 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 `` -and `` 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 -too sensitive or dangerous 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} +

Acknowledgments

-Other than files in the [=origin private 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). +

This standard is written by Marijn Kruisselbrink +(Google, mek@chromium.org). -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. +

This Living Standard includes material copied from W3C WICG's +File System Access, which is +available under the +W3C Software and Document License. diff --git a/security-privacy-questionnaire.md b/security-privacy-questionnaire.md deleted file mode 100644 index 008ce9c..0000000 --- a/security-privacy-questionnaire.md +++ /dev/null @@ -1,77 +0,0 @@ -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 `` and `` 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. - -### 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. - -### 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 `` and ``. 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 `` today. - -### 2.12. What temporary identifiers might this this specification create or expose to the web? - -None. - -### 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. diff --git a/w3c.json b/w3c.json deleted file mode 100644 index e63486d..0000000 --- a/w3c.json +++ /dev/null @@ -1,5 +0,0 @@ - { - "group": [80485] -, "contacts": ["marcoscaceres"] -, "repo-type": "cg-report" -}