├── .editorconfig ├── .gitattributes ├── .github ├── ISSUE_TEMPLATE │ ├── 0-new-issue.yml │ ├── 1-new-feature.yml │ └── config.yml └── workflows │ └── build.yml ├── .gitignore ├── .pr-preview.json ├── LICENSE ├── Makefile ├── PULL_REQUEST_TEMPLATE.md ├── README.md ├── index.bs ├── proposals ├── FileSystemObserver.md ├── MovingNonOpfsFiles.md ├── MultipleReadersWriters.md └── Remove.md └── review-drafts ├── 2022-03.bs ├── 2022-09.bs ├── 2023-03.bs └── 2023-09.bs /.editorconfig: -------------------------------------------------------------------------------- 1 | root = true 2 | 3 | [*] 4 | end_of_line = lf 5 | insert_final_newline = true 6 | charset = utf-8 7 | indent_size = 2 8 | indent_style = space 9 | trim_trailing_whitespace = true 10 | max_line_length = 100 11 | 12 | [Makefile] 13 | indent_style = tab 14 | 15 | [*.md] 16 | max_line_length = off 17 | 18 | [*.bs] 19 | indent_size = 1 20 | 21 | [*.py] 22 | indent_size = 4 23 | -------------------------------------------------------------------------------- /.gitattributes: -------------------------------------------------------------------------------- 1 | * text=auto 2 | *.bs diff=html linguist-language=HTML 3 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/0-new-issue.yml: -------------------------------------------------------------------------------- 1 | name: New issue 2 | description: File a new issue against the File System Standard. 3 | body: 4 | - type: markdown 5 | attributes: 6 | value: | 7 | Before filling out this form, please familiarize yourself with the [Code of Conduct](https://whatwg.org/code-of-conduct). You might also find the [FAQ](https://whatwg.org/faq) and [Working Mode](https://whatwg.org/working-mode) useful. 8 | 9 | If at any point you have questions, please reach out to us on [Chat](https://whatwg.org/chat). 10 | - type: textarea 11 | attributes: 12 | label: "What is the issue with the File System Standard?" 13 | validations: 14 | required: true 15 | - type: markdown 16 | attributes: 17 | value: "Thank you for taking the time to improve the File System Standard!" 18 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/1-new-feature.yml: -------------------------------------------------------------------------------- 1 | name: New feature 2 | description: Request a new feature in the File System Standard. 3 | labels: ["addition/proposal", "needs implementer interest"] 4 | body: 5 | - type: markdown 6 | attributes: 7 | value: | 8 | Before filling out this form, please familiarize yourself with the [Code of Conduct](https://whatwg.org/code-of-conduct), [FAQ](https://whatwg.org/faq), and [Working Mode](https://whatwg.org/working-mode). They help with setting expectations and making sure you know what is required. The FAQ ["How should I go about proposing new features to WHATWG standards?"](https://whatwg.org/faq#adding-new-features) is especially relevant. 9 | 10 | If at any point you have questions, please reach out to us on [Chat](https://whatwg.org/chat). 11 | - type: textarea 12 | attributes: 13 | label: "What problem are you trying to solve?" 14 | validations: 15 | required: true 16 | - type: textarea 17 | attributes: 18 | label: "What solutions exist today?" 19 | - type: textarea 20 | attributes: 21 | label: "How would you solve it?" 22 | - type: textarea 23 | attributes: 24 | label: "Anything else?" 25 | - type: markdown 26 | attributes: 27 | value: "Thank you for taking the time to improve the File System Standard!" 28 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/config.yml: -------------------------------------------------------------------------------- 1 | blank_issues_enabled: false 2 | contact_links: 3 | - name: Chat 4 | url: https://whatwg.org/chat 5 | about: Please do reach out with questions and feedback! 6 | - name: Stack Overflow 7 | url: https://stackoverflow.com/ 8 | about: If you're having trouble building a web page, this is not the right repository. Consider asking your question on Stack Overflow instead. 9 | -------------------------------------------------------------------------------- /.github/workflows/build.yml: -------------------------------------------------------------------------------- 1 | name: Build 2 | 3 | on: 4 | pull_request: 5 | branches: 6 | - main 7 | push: 8 | branches: 9 | - main 10 | workflow_dispatch: 11 | 12 | jobs: 13 | build: 14 | name: Build 15 | runs-on: ubuntu-22.04 16 | steps: 17 | - uses: actions/checkout@v3 18 | with: 19 | fetch-depth: 2 20 | - uses: actions/setup-python@v4 21 | with: 22 | python-version: "3.11" 23 | - run: pip install bikeshed && bikeshed update 24 | # Note: `make deploy` will do a deploy dry run on PRs. 25 | - run: make deploy 26 | env: 27 | SERVER: ${{ secrets.MARQUEE_SERVER }} 28 | SERVER_PUBLIC_KEY: ${{ secrets.MARQUEE_PUBLIC_KEY }} 29 | SERVER_DEPLOY_KEY: ${{ secrets.MARQUEE_DEPLOY_KEY }} 30 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | /fs.spec.whatwg.org/ 2 | /deploy.sh 3 | /index.html 4 | -------------------------------------------------------------------------------- /.pr-preview.json: -------------------------------------------------------------------------------- 1 | { 2 | "src_file": "index.bs", 3 | "type": "bikeshed", 4 | "params": { 5 | "force": 1, 6 | "md-status": "LS-PR", 7 | "md-Text-Macro": "PR-NUMBER {{ pull_request.number }}" 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright © WHATWG (Apple, Google, Mozilla, Microsoft). 2 | 3 | This work is licensed under a Creative Commons Attribution 4.0 International 4 | License. To the extent portions of it are incorporated into source code, 5 | such portions in the source code are licensed under the BSD 3-Clause License instead. 6 | 7 | - - - - 8 | 9 | Creative Commons Attribution 4.0 International Public License 10 | 11 | By exercising the Licensed Rights (defined below), You accept and agree 12 | to be bound by the terms and conditions of this Creative Commons 13 | Attribution 4.0 International Public License ("Public License"). To the 14 | extent this Public License may be interpreted as a contract, You are 15 | granted the Licensed Rights in consideration of Your acceptance of 16 | these terms and conditions, and the Licensor grants You such rights in 17 | consideration of benefits the Licensor receives from making the 18 | Licensed Material available under these terms and conditions. 19 | 20 | 21 | Section 1 -- Definitions. 22 | 23 | a. Adapted Material means material subject to Copyright and Similar 24 | Rights that is derived from or based upon the Licensed Material 25 | and in which the Licensed Material is translated, altered, 26 | arranged, transformed, or otherwise modified in a manner requiring 27 | permission under the Copyright and Similar Rights held by the 28 | Licensor. For purposes of this Public License, where the Licensed 29 | Material is a musical work, performance, or sound recording, 30 | Adapted Material is always produced where the Licensed Material is 31 | synched in timed relation with a moving image. 32 | 33 | b. Adapter's License means the license You apply to Your Copyright 34 | and Similar Rights in Your contributions to Adapted Material in 35 | accordance with the terms and conditions of this Public License. 36 | 37 | c. Copyright and Similar Rights means copyright and/or similar rights 38 | closely related to copyright including, without limitation, 39 | performance, broadcast, sound recording, and Sui Generis Database 40 | Rights, without regard to how the rights are labeled or 41 | categorized. For purposes of this Public License, the rights 42 | specified in Section 2(b)(1)-(2) are not Copyright and Similar 43 | Rights. 44 | 45 | d. Effective Technological Measures means those measures that, in the 46 | absence of proper authority, may not be circumvented under laws 47 | fulfilling obligations under Article 11 of the WIPO Copyright 48 | Treaty adopted on December 20, 1996, and/or similar international 49 | agreements. 50 | 51 | e. Exceptions and Limitations means fair use, fair dealing, and/or 52 | any other exception or limitation to Copyright and Similar Rights 53 | that applies to Your use of the Licensed Material. 54 | 55 | f. Licensed Material means the artistic or literary work, database, 56 | or other material to which the Licensor applied this Public 57 | License. 58 | 59 | g. Licensed Rights means the rights granted to You subject to the 60 | terms and conditions of this Public License, which are limited to 61 | all Copyright and Similar Rights that apply to Your use of the 62 | Licensed Material and that the Licensor has authority to license. 63 | 64 | h. Licensor means the individual(s) or entity(ies) granting rights 65 | under this Public License. 66 | 67 | i. Share means to provide material to the public by any means or 68 | process that requires permission under the Licensed Rights, such 69 | as reproduction, public display, public performance, distribution, 70 | dissemination, communication, or importation, and to make material 71 | available to the public including in ways that members of the 72 | public may access the material from a place and at a time 73 | individually chosen by them. 74 | 75 | j. Sui Generis Database Rights means rights other than copyright 76 | resulting from Directive 96/9/EC of the European Parliament and of 77 | the Council of 11 March 1996 on the legal protection of databases, 78 | as amended and/or succeeded, as well as other essentially 79 | equivalent rights anywhere in the world. 80 | 81 | k. You means the individual or entity exercising the Licensed Rights 82 | under this Public License. Your has a corresponding meaning. 83 | 84 | 85 | Section 2 -- Scope. 86 | 87 | a. License grant. 88 | 89 | 1. Subject to the terms and conditions of this Public License, 90 | the Licensor hereby grants You a worldwide, royalty-free, 91 | non-sublicensable, non-exclusive, irrevocable license to 92 | exercise the Licensed Rights in the Licensed Material to: 93 | 94 | a. reproduce and Share the Licensed Material, in whole or 95 | in part; and 96 | 97 | b. produce, reproduce, and Share Adapted Material. 98 | 99 | 2. Exceptions and Limitations. For the avoidance of doubt, where 100 | Exceptions and Limitations apply to Your use, this Public 101 | License does not apply, and You do not need to comply with 102 | its terms and conditions. 103 | 104 | 3. Term. The term of this Public License is specified in Section 105 | 6(a). 106 | 107 | 4. Media and formats; technical modifications allowed. The 108 | Licensor authorizes You to exercise the Licensed Rights in 109 | all media and formats whether now known or hereafter created, 110 | and to make technical modifications necessary to do so. The 111 | Licensor waives and/or agrees not to assert any right or 112 | authority to forbid You from making technical modifications 113 | necessary to exercise the Licensed Rights, including 114 | technical modifications necessary to circumvent Effective 115 | Technological Measures. For purposes of this Public License, 116 | simply making modifications authorized by this Section 2(a) 117 | (4) never produces Adapted Material. 118 | 119 | 5. Downstream recipients. 120 | 121 | a. Offer from the Licensor -- Licensed Material. Every 122 | recipient of the Licensed Material automatically 123 | receives an offer from the Licensor to exercise the 124 | Licensed Rights under the terms and conditions of this 125 | Public License. 126 | 127 | b. No downstream restrictions. You may not offer or impose 128 | any additional or different terms or conditions on, or 129 | apply any Effective Technological Measures to, the 130 | Licensed Material if doing so restricts exercise of the 131 | Licensed Rights by any recipient of the Licensed 132 | Material. 133 | 134 | 6. No endorsement. Nothing in this Public License constitutes or 135 | may be construed as permission to assert or imply that You 136 | are, or that Your use of the Licensed Material is, connected 137 | with, or sponsored, endorsed, or granted official status by, 138 | the Licensor or others designated to receive attribution as 139 | provided in Section 3(a)(1)(A)(i). 140 | 141 | b. Other rights. 142 | 143 | 1. Moral rights, such as the right of integrity, are not 144 | licensed under this Public License, nor are publicity, 145 | privacy, and/or other similar personality rights; however, to 146 | the extent possible, the Licensor waives and/or agrees not to 147 | assert any such rights held by the Licensor to the limited 148 | extent necessary to allow You to exercise the Licensed 149 | Rights, but not otherwise. 150 | 151 | 2. Patent and trademark rights are not licensed under this 152 | Public License. 153 | 154 | 3. To the extent possible, the Licensor waives any right to 155 | collect royalties from You for the exercise of the Licensed 156 | Rights, whether directly or through a collecting society 157 | under any voluntary or waivable statutory or compulsory 158 | licensing scheme. In all other cases the Licensor expressly 159 | reserves any right to collect such royalties. 160 | 161 | 162 | Section 3 -- License Conditions. 163 | 164 | Your exercise of the Licensed Rights is expressly made subject to the 165 | following conditions. 166 | 167 | a. Attribution. 168 | 169 | 1. If You Share the Licensed Material (including in modified 170 | form), You must: 171 | 172 | a. retain the following if it is supplied by the Licensor 173 | with the Licensed Material: 174 | 175 | i. identification of the creator(s) of the Licensed 176 | Material and any others designated to receive 177 | attribution, in any reasonable manner requested by 178 | the Licensor (including by pseudonym if 179 | designated); 180 | 181 | ii. a copyright notice; 182 | 183 | iii. a notice that refers to this Public License; 184 | 185 | iv. a notice that refers to the disclaimer of 186 | warranties; 187 | 188 | v. a URI or hyperlink to the Licensed Material to the 189 | extent reasonably practicable; 190 | 191 | b. indicate if You modified the Licensed Material and 192 | retain an indication of any previous modifications; and 193 | 194 | c. indicate the Licensed Material is licensed under this 195 | Public License, and include the text of, or the URI or 196 | hyperlink to, this Public License. 197 | 198 | 2. You may satisfy the conditions in Section 3(a)(1) in any 199 | reasonable manner based on the medium, means, and context in 200 | which You Share the Licensed Material. For example, it may be 201 | reasonable to satisfy the conditions by providing a URI or 202 | hyperlink to a resource that includes the required 203 | information. 204 | 205 | 3. If requested by the Licensor, You must remove any of the 206 | information required by Section 3(a)(1)(A) to the extent 207 | reasonably practicable. 208 | 209 | 4. If You Share Adapted Material You produce, the Adapter's 210 | License You apply must not prevent recipients of the Adapted 211 | Material from complying with this Public License. 212 | 213 | 214 | Section 4 -- Sui Generis Database Rights. 215 | 216 | Where the Licensed Rights include Sui Generis Database Rights that 217 | apply to Your use of the Licensed Material: 218 | 219 | a. for the avoidance of doubt, Section 2(a)(1) grants You the right 220 | to extract, reuse, reproduce, and Share all or a substantial 221 | portion of the contents of the database; 222 | 223 | b. if You include all or a substantial portion of the database 224 | contents in a database in which You have Sui Generis Database 225 | Rights, then the database in which You have Sui Generis Database 226 | Rights (but not its individual contents) is Adapted Material; and 227 | 228 | c. You must comply with the conditions in Section 3(a) if You Share 229 | all or a substantial portion of the contents of the database. 230 | 231 | For the avoidance of doubt, this Section 4 supplements and does not 232 | replace Your obligations under this Public License where the Licensed 233 | Rights include other Copyright and Similar Rights. 234 | 235 | 236 | Section 5 -- Disclaimer of Warranties and Limitation of Liability. 237 | 238 | a. UNLESS OTHERWISE SEPARATELY UNDERTAKEN BY THE LICENSOR, TO THE 239 | EXTENT POSSIBLE, THE LICENSOR OFFERS THE LICENSED MATERIAL AS-IS 240 | AND AS-AVAILABLE, AND MAKES NO REPRESENTATIONS OR WARRANTIES OF 241 | ANY KIND CONCERNING THE LICENSED MATERIAL, WHETHER EXPRESS, 242 | IMPLIED, STATUTORY, OR OTHER. THIS INCLUDES, WITHOUT LIMITATION, 243 | WARRANTIES OF TITLE, MERCHANTABILITY, FITNESS FOR A PARTICULAR 244 | PURPOSE, NON-INFRINGEMENT, ABSENCE OF LATENT OR OTHER DEFECTS, 245 | ACCURACY, OR THE PRESENCE OR ABSENCE OF ERRORS, WHETHER OR NOT 246 | KNOWN OR DISCOVERABLE. WHERE DISCLAIMERS OF WARRANTIES ARE NOT 247 | ALLOWED IN FULL OR IN PART, THIS DISCLAIMER MAY NOT APPLY TO YOU. 248 | 249 | b. TO THE EXTENT POSSIBLE, IN NO EVENT WILL THE LICENSOR BE LIABLE 250 | TO YOU ON ANY LEGAL THEORY (INCLUDING, WITHOUT LIMITATION, 251 | NEGLIGENCE) OR OTHERWISE FOR ANY DIRECT, SPECIAL, INDIRECT, 252 | INCIDENTAL, CONSEQUENTIAL, PUNITIVE, EXEMPLARY, OR OTHER LOSSES, 253 | COSTS, EXPENSES, OR DAMAGES ARISING OUT OF THIS PUBLIC LICENSE OR 254 | USE OF THE LICENSED MATERIAL, EVEN IF THE LICENSOR HAS BEEN 255 | ADVISED OF THE POSSIBILITY OF SUCH LOSSES, COSTS, EXPENSES, OR 256 | DAMAGES. WHERE A LIMITATION OF LIABILITY IS NOT ALLOWED IN FULL OR 257 | IN PART, THIS LIMITATION MAY NOT APPLY TO YOU. 258 | 259 | c. The disclaimer of warranties and limitation of liability provided 260 | above shall be interpreted in a manner that, to the extent 261 | possible, most closely approximates an absolute disclaimer and 262 | waiver of all liability. 263 | 264 | 265 | Section 6 -- Term and Termination. 266 | 267 | a. This Public License applies for the term of the Copyright and 268 | Similar Rights licensed here. However, if You fail to comply with 269 | this Public License, then Your rights under this Public License 270 | terminate automatically. 271 | 272 | b. Where Your right to use the Licensed Material has terminated under 273 | Section 6(a), it reinstates: 274 | 275 | 1. automatically as of the date the violation is cured, provided 276 | it is cured within 30 days of Your discovery of the 277 | violation; or 278 | 279 | 2. upon express reinstatement by the Licensor. 280 | 281 | For the avoidance of doubt, this Section 6(b) does not affect any 282 | right the Licensor may have to seek remedies for Your violations 283 | of this Public License. 284 | 285 | c. For the avoidance of doubt, the Licensor may also offer the 286 | Licensed Material under separate terms or conditions or stop 287 | distributing the Licensed Material at any time; however, doing so 288 | will not terminate this Public License. 289 | 290 | d. Sections 1, 5, 6, 7, and 8 survive termination of this Public 291 | License. 292 | 293 | 294 | Section 7 -- Other Terms and Conditions. 295 | 296 | a. The Licensor shall not be bound by any additional or different 297 | terms or conditions communicated by You unless expressly agreed. 298 | 299 | b. Any arrangements, understandings, or agreements regarding the 300 | Licensed Material not stated herein are separate from and 301 | independent of the terms and conditions of this Public License. 302 | 303 | 304 | Section 8 -- Interpretation. 305 | 306 | a. For the avoidance of doubt, this Public License does not, and 307 | shall not be interpreted to, reduce, limit, restrict, or impose 308 | conditions on any use of the Licensed Material that could lawfully 309 | be made without permission under this Public License. 310 | 311 | b. To the extent possible, if any provision of this Public License is 312 | deemed unenforceable, it shall be automatically reformed to the 313 | minimum extent necessary to make it enforceable. If the provision 314 | cannot be reformed, it shall be severed from this Public License 315 | without affecting the enforceability of the remaining terms and 316 | conditions. 317 | 318 | c. No term or condition of this Public License will be waived and no 319 | failure to comply consented to unless expressly agreed to by the 320 | Licensor. 321 | 322 | d. Nothing in this Public License constitutes or may be interpreted 323 | as a limitation upon, or waiver of, any privileges and immunities 324 | that apply to the Licensor or You, including from the legal 325 | processes of any jurisdiction or authority. 326 | 327 | - - - - 328 | 329 | BSD 3-Clause License 330 | 331 | Redistribution and use in source and binary forms, with or without 332 | modification, are permitted provided that the following conditions are met: 333 | 334 | 1. Redistributions of source code must retain the above copyright notice, this 335 | list of conditions and the following disclaimer. 336 | 337 | 2. Redistributions in binary form must reproduce the above copyright notice, 338 | this list of conditions and the following disclaimer in the documentation 339 | and/or other materials provided with the distribution. 340 | 341 | 3. Neither the name of the copyright holder nor the names of its 342 | contributors may be used to endorse or promote products derived from 343 | this software without specific prior written permission. 344 | 345 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" 346 | AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE 347 | IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE 348 | DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE 349 | FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL 350 | DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR 351 | SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER 352 | CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, 353 | OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE 354 | OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 355 | 356 | - - - - 357 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | SHELL=/bin/bash -o pipefail 2 | .PHONY: local remote deploy 3 | 4 | remote: index.bs 5 | @ (HTTP_STATUS=$$(curl https://api.csswg.org/bikeshed/ \ 6 | --output index.html \ 7 | --write-out "%{http_code}" \ 8 | --header "Accept: text/plain, text/html" \ 9 | -F die-on=warning \ 10 | -F md-Text-Macro="COMMIT-SHA LOCAL COPY" \ 11 | -F file=@index.bs) && \ 12 | [[ "$$HTTP_STATUS" -eq "200" ]]) || ( \ 13 | echo ""; cat index.html; echo ""; \ 14 | rm -f index.html; \ 15 | exit 22 \ 16 | ); 17 | 18 | local: index.bs 19 | bikeshed spec index.bs index.html --md-Text-Macro="COMMIT-SHA LOCAL-COPY" 20 | 21 | deploy: index.bs 22 | curl --remote-name --fail https://resources.whatwg.org/build/deploy.sh 23 | bash ./deploy.sh 24 | -------------------------------------------------------------------------------- /PULL_REQUEST_TEMPLATE.md: -------------------------------------------------------------------------------- 1 | 6 | 7 | - [ ] At least two implementers are interested (and none opposed): 8 | * … 9 | * … 10 | - [ ] [Tests](https://github.com/web-platform-tests/wpt) are written and can be reviewed and commented upon at: 11 | * … 12 | - [ ] [Implementation bugs](https://github.com/whatwg/meta/blob/main/MAINTAINERS.md#handling-pull-requests) are filed: 13 | * Chromium: … 14 | * Gecko: … 15 | * WebKit: … 16 | - [ ] [MDN issue](https://github.com/whatwg/meta/blob/main/MAINTAINERS.md#handling-pull-requests) is filed: … 17 | - [ ] The top of this comment includes a [clear commit message](https://github.com/whatwg/meta/blob/main/COMMITTING.md) to use. 18 | 19 | (See [WHATWG Working Mode: Changes](https://whatwg.org/working-mode#changes) for more details.) 20 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | This repository hosts the [File System Standard](https://fs.spec.whatwg.org/). 2 | 3 | ## Code of conduct 4 | 5 | We are committed to providing a friendly, safe, and welcoming environment for all. Please read and respect the [Code of Conduct](https://whatwg.org/code-of-conduct). 6 | 7 | ## Contribution opportunities 8 | 9 | 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. 10 | 11 | Issues labeled ["good first issue"](https://github.com/whatwg/fs/labels/good%20first%20issue) are a good place to get a taste for editing the File System Standard. Note that we don't assign issues and there's no reason to ask for availability either, just provide a pull request. 12 | 13 | If you are thinking of suggesting a new feature, read through the [FAQ](https://whatwg.org/faq) and [Working Mode](https://whatwg.org/working-mode) documents to get yourself familiarized with the process. 14 | 15 | We'd be happy to help you with all of this [on Chat](https://whatwg.org/chat). 16 | 17 | ## Pull requests 18 | 19 | In short, change `index.bs` and submit your patch, with a [good commit message](https://github.com/whatwg/meta/blob/main/COMMITTING.md). 20 | 21 | Please add your name to the Acknowledgments section in your first pull request, even for trivial fixes. The names are sorted lexicographically. 22 | 23 | To ensure your patch meets all the necessary requirements, please also see the [Contributor Guidelines](https://github.com/whatwg/meta/blob/main/CONTRIBUTING.md). Editors of the File System Standard are expected to follow the [Maintainer Guidelines](https://github.com/whatwg/meta/blob/main/MAINTAINERS.md). 24 | 25 | ## Tests 26 | 27 | Tests are an essential part of the standardization process and will need to be created or adjusted as changes to the standard are made. Tests for the File System Standard can be found in the `fs/` directory of [`web-platform-tests/wpt`](https://github.com/web-platform-tests/wpt). 28 | 29 | A dashboard showing the tests running against browser engines can be seen at [wpt.fyi/results/fs](https://wpt.fyi/results/fs). 30 | 31 | ## Building "locally" 32 | 33 | For quick local iteration, run `make`; this will use a web service to build the standard, so that you don't have to install anything. See more in the [Contributor Guidelines](https://github.com/whatwg/meta/blob/main/CONTRIBUTING.md#building). 34 | -------------------------------------------------------------------------------- /proposals/FileSystemObserver.md: -------------------------------------------------------------------------------- 1 | # The FileSystemObserver Interface 2 | 3 | ## Authors 4 | 5 | * [Austin Sullivan](asully@chromium.org) (Google) 6 | 7 | ## Participate 8 | 9 | * [Issue tracker](https://github.com/whatwg/fs/issues) 10 | 11 | ## Introduction 12 | 13 | The file system is a shared resource that can be modified from several contexts. A [Bucket File System](https://fs.spec.whatwg.org/#sandboxed-filesystem) spans numerous [agents](https://tc39.es/ecma262/#sec-agents) - tabs, workers, etc - within the same [storage key](https://storage.spec.whatwg.org/#storage-keys). The local file system also spans across origins and other applications on the host operating system. 14 | 15 | For a given agent to know about modifications to the file system - made either by itself or from some external context - it can currently poll the file system to detect changes. This is inefficient and does not scale well. 16 | 17 | This explainer proposes a `FileSystemObserver` interface which will much more easily allow a website to be notified of changes to the file system. 18 | 19 | ## Goals 20 | 21 | * Simplify application logic and improve the ergonomics of watching file paths 22 | * Improve the efficiency of watching file paths on the local file system 23 | * Provide best-effort information of changes to the local file system 24 | * Guarantee consistent behavior across platforms with regards to the contents of a file system change record for a corresponding change to a Bucket File System 25 | 26 | ## Non-Goals 27 | 28 | * Expose any information to the web that isn’t already exposed 29 | * Expand the permissions of a website as a result of a file system change 30 | * Provide notification of changes that occur outside the scope of a `FileSystemObserver` connection (e.g. before the `FileSystemObserver` is created or after the tab is closed) 31 | * Guarantee that all file system changes which occur while a `FileSystemObserver` is connected are reported. See [Guaranteeing that Changes are Not Missed](#guaranteeing-that-changes-are-not-missed) 32 | * Guarantee consistent behavior across platforms with regards to the contents of a file system change record for a corresponding change to the local file system. See [Cross Platform Compatibility](#cross-platform-compatibility) 33 | 34 | ## Use Cases 35 | 36 | * [Binding a UI element](https://web.dev/indexeddb-uidatabinding/) to the contents of a file 37 | * Notifying the main thread of file system changes from a worker or another tab 38 | * Syncing file system changes to a server 39 | * [Implementing an in-memory](https://github.com/WICG/indexed-db-observers/blob/gh-pages/EXAMPLES.md#maintaining-an-in-memory-data-cache) cache to speed up file system operations 40 | 41 | ## Key Scenarios 42 | 43 | ### Observing Changes to a File 44 | 45 | To implement a website that [binds a UI element](https://web.dev/indexeddb-uidatabinding/) to the contents of the file, the website must watch for changes to the file in order to trigger corresponding changes to the UI. Currently, the website has two options for watching file system changes: 46 | 47 | * Set up a [`BroadcastChannel`](https://developer.mozilla.org/en-US/docs/Web/API/BroadcastChannel) and add hooks to broadcast a message on every file system operation. Note that while this approach is plausible for tracking changes to the Bucket File System, it is oblivious to changes made to the local file system external to your origin 48 | * Poll the file system. This is the only way to track changes made external to your origin 49 | 50 | The example below shows a rudimentary implementation of file system polling. The website can read the last-modified timestamp of the file through the [`File`](https://developer.mozilla.org/en-US/docs/Web/API/File) interface. The value of `pollInterval` strongly dictates both the resource consumption (if it's too frequent) and responsiveness (if it's not frequent enough) of the website. 51 | 52 | ```javascript 53 | // Current approach to detect changes, using polling 54 | 55 | while (true) { 56 | await sleep(pollInterval); 57 | const file = await fileHandle.getFile(); 58 | const timestamp = file.lastModified; 59 | if (timestamp > lastKnownTimestamp) { 60 | lastKnownTimestamp = timestamp; 61 | await readFileAndUpdateUI(file); 62 | } 63 | } 64 | ``` 65 | 66 | A `FileSystemObserver` allows changes to the file to be observed with much more simple application logic, without requiring the website author to consider the resource consumption vs. responsiveness tradeoff. When the observed file changes, the website will receive a `FileSystemChangeRecord` including details about the file system change. 67 | 68 | ```javascript 69 | // Same as above, but using the proposed FileSystemObserver 70 | 71 | const callback = async (records, observer) => { 72 | // Will be run when the observed file changes. 73 | 74 | // The change record includes a handle detailing which file has 75 | // changed, which in this case corresponds to the observed handle. 76 | const changedFileHandle = records[0].changedHandle; 77 | assert(await fileHandle.isSameEntry(changedFileHandle)); 78 | 79 | // Since we're observing changes to a file, the `root` of the change 80 | // record also corresponds to the observed file. 81 | assert(await fileHandle.isSameEntry(records[0].root)); 82 | 83 | readChangesAndUpdateUI(changedFileHandle); 84 | } 85 | 86 | const observer = new FileSystemObserver(callback); 87 | await observer.observe(fileHandle); 88 | ``` 89 | 90 | This is especially useful for changes made via the [`FileSystemSyncAccessHandle`](https://fs.spec.whatwg.org/#api-filesystemsyncaccesshandle) interface - a high-performance file primitive that can only be used from a [dedicated worker](https://developer.mozilla.org/en-US/docs/Web/API/Web_Workers_API). The website can create a `FileSystemObserver` on the main thread to listen to changes made from the worker. 91 | 92 | ```javascript 93 | // Using an observer on the main thread to listen to changes made by a worker 94 | 95 | // index.js 96 | const callback = (records, observer) => { 97 | // Will be run when there are changes to observed files. 98 | readChangesAndUpdateUI(records); 99 | } 100 | 101 | const observer = new FileSystemObserver(callback); 102 | await observer.observe(fileHandle); 103 | askWorkerToWriteSomeData(fileHandle, someData); 104 | 105 | // worker.js 106 | const syncAccessHandle = await dbFileHandle.createSyncAccessHandle(); 107 | syncAccessHandle.write(someData); // Triggers a change record 108 | ``` 109 | 110 | ### Observing Changes to a Directory 111 | 112 | Unlike files, directories contain children - and some of these children are themselves directories which contain yet more children. Given that there exist use cases for both non-recursive (i.e. first-level) and recursive directory watches and that the [resource consumption](#resource-constraints) of recursive directory watches can be exponentially higher than non-recursive directory watches on some platforms, the proposed API allows for directory watches to be either non-recursive or recursive. 113 | 114 | #### Observing a Directory Non-Recursively 115 | 116 | A user grants a web image editor access to a folder containing some photos. When the user adds a new photo to the folder, they expect the website to detect the presence of the new file. 117 | 118 | Checking for updates to a directory requires enumerating each of the entries within the directory to compare its last-modified timestamp to the last-known timestamp for the file. If the folder is large, there may not exist a value of `pollInterval` which provides the desired responsiveness 119 | 120 | ```javascript 121 | // Current approach to detect changes to a directory's children, using polling 122 | 123 | let timestamps = {}; // { fileName: timestamp, ... } 124 | 125 | while (true) { 126 | await sleep(pollInterval); 127 | // Enumerate the directory and check timestamps for each child 128 | for await (const handle of directoryHandle.values()) { 129 | checkIfChanged(handle); 130 | // ... 131 | } 132 | // ... 133 | } 134 | ``` 135 | 136 | Note that if it was possible to read the last-modified time for a directory (see https://github.com/whatwg/fs/issues/12), this could be optimized by only enumerating the directory if its timestamp has changed. However, detecting _which_ file was added would still require enumerating the directory. 137 | 138 | The `FileSystemObserver` interface allows the website to be responsive to changes even for large directories which are not feasible to poll. 139 | 140 | ```javascript 141 | // Same as above, but using a FileSystemObserver 142 | 143 | const callback = (records, observer) => { 144 | // Non-recursively watching a directory will only report changes to 145 | // immediate children of the observed directory. 146 | handleChanges(records); 147 | } 148 | 149 | const observer = new FileSystemObserver(callback); 150 | const options = { recursive: false }; // Default is false. 151 | await observer.observe(directoryHandle, options); 152 | ``` 153 | 154 | #### Observing a Directory Recursively 155 | 156 | A user grants a web IDE access to the source folder of a local repository. If the user makes changes to files or directories in the repository via some other application on the machine (e.g. with Vim) while the web application is running, they expect the web application’s UI to show a “dirty” indicator. 157 | 158 | There are a number of issues when attempting to track these changes by polling the file system: 159 | 160 | * Tracking changes to subdirectories exacerbates the challenges of scaling polling. It is even more likely there will not exist a polling interval which provides the desired responsiveness 161 | * Recursively enumerating a directory is currently not trivial. See https://github.com/whatwg/fs/issues/15 162 | * Checking whether a directory (or file, for that matter) exists is currently not trivial. See https://github.com/whatwg/fs/issues/80 163 | * Checking the last-modified timestamp of a directory is currently impossible. See https://github.com/whatwg/fs/issues/12 164 | 165 | Passing the `recursive: true` option to the `FileSystemObserver.observe()` method’s `options` dictionary expands the scope of the observation to include changes within subdirectories. Here again, the website can be responsive to changes even while recursively watching large directories. 166 | 167 | ```javascript 168 | // Detecting changes to a directory recursively with a FileSystemObserver 169 | 170 | const callback = (records, observer) => { 171 | // Recursively watching a directory will report changes to both 172 | // children and all subdirectories of the watched directory. 173 | for (const record of records) { 174 | markDirty(record); 175 | } 176 | } 177 | 178 | const observer = new FileSystemObserver(callback); 179 | const options = { recursive: true }; 180 | await observer.observe(directoryHandle, options); 181 | ``` 182 | 183 | In this example, the `markDirty()` method can utilize numerous pieces of information from the `FileSystemChangeRecord` to more efficiently update the dirty indicator. See the [tentative IDL](#tentative-idl). 184 | 185 | ```javascript 186 | // Implementation of the markDirty() function in the example above 187 | 188 | async function markDirty(record) { 189 | // Decide how to mark the file dirty according to the 190 | // `FileSystemChangeType` included in each file system change record. 191 | switch (record.type) { 192 | case 'appeared': 193 | // `record.root` is the handle passed to `observe()`. Note that 194 | // the File System specification does not expose the concept of an 195 | // absolute path, so understanding a file system change is 196 | // inherently relative to some directory. 197 | markAppeared(record.root, record.relativePathComponents); 198 | break; 199 | case 'disappeared': 200 | // The relative path of the changed handle may be more useful than 201 | // the handle itself, since the file no longer exists. 202 | markDisappeared(record.root, record.relativePathComponents); 203 | break; 204 | case 'modified': 205 | // A handle to the changed path may be more useful than its 206 | // relative path if reading from the file is necessary to 207 | // understand the change. 208 | // 209 | // Note that records with the 'modified' change type may be noisy 210 | // (e.g. overwriting file contents with the same data) so it's 211 | // necessary to check whether the file actually changed. 212 | if (await checkIfChanged(record.changedHandle)) { 213 | markModified(record.root, record.relativePathComponents); 214 | } 215 | break; 216 | case 'moved': 217 | // `record.relativePathMovedFrom` is used exclusively for 'moved' 218 | // records, to indicate the previous path of the moved file. 219 | markMoved(record.root, record.relativePathMovedFrom, 220 | record.relativePathComponents); 221 | break; 222 | case 'unknown': 223 | // Unknown change event(s) may have been missed. 224 | if (await checkIfChanged(record.changedHandle)) { 225 | markChanged(record.root, record.relativePathComponents); 226 | } 227 | break; 228 | case 'errored': 229 | // Watching paths on the local file system may fail unexpectedly. 230 | // After receiving a record with an 'errored' change type, we will 231 | // not receive any more change records from this observer. 232 | // Unobserve this handle. You may then consider re-observing the 233 | // handle, though that may fail if the issue was not transient. 234 | observer.unobserve(record.root); 235 | // ... 236 | break; 237 | } 238 | // ... 239 | } 240 | ``` 241 | 242 | #### Gotchas When Observing a Directory on the Local File System 243 | 244 | Given the platform-specific differences, change records for directory observations of the local file system may or may not include information about which file within the directory has changed, or the type of change. See [Cross Platform Compatibility](#cross-platform-compatibility). 245 | 246 | For example, in the [Observing a Directory Non-Recursively](#observing-a-directory-non-recursively) example above, the `handleChanges()` function may need to account for these platform-specific differences if `directoryHandle` corresponds to a directory on the local file system. 247 | 248 | At best, the website will receive a detailed change record containing the type of change and a handle to the affected path. At worst, the website receives a more generic change record that still requires the website to enumerate the directory to figure out which child changed. Note that this is still an improvement over polling, since the directory enumeration can be kicked off on-demand from the `FileSystemObserverCallback`, rather than needing to poll for changes. 249 | 250 | ```javascript 251 | // Implementation of the handleChanges() function in the example above 252 | 253 | async function handleChanges(records) { 254 | // The `root` of the change record always corresponds to the directory 255 | // handle passed to the `observe()` method. 256 | assert(await fileHandle.isSameEntry(record[0].root)); 257 | 258 | let sawFileCreatedRecord = false; 259 | for (const record of records) { 260 | // The `changedHandle` of the change record corresponds to the 261 | // file path on which the change has occurred. Alternatively, the 262 | // file path itself - relative to `root` - is accessible via the 263 | // `relativePathComponents` attribute. 264 | const changedHandle = record.changedHandle; 265 | 266 | // Take advantage of file-level notifications, if available. 267 | if (changedHandle.kind === 'file' && record.type === 'appeared') { 268 | sawFileCreatedRecord = true; 269 | readNewFile(changedHandle); 270 | } 271 | } 272 | 273 | // Otherwise fall back to enumerating the observed directory. 274 | // Only necessary for directories on the local file system. 275 | if (!sawFileCreatedRecord) { 276 | enumerateThroughDirectoryToFindAddedFile(records[0].root); 277 | } 278 | // ... 279 | } 280 | ``` 281 | 282 | ## Design Discussion 283 | 284 | ### Guaranteeing that Changes are Not Missed 285 | 286 | In general, once the promise from `observer.observe(handle)` is resolved, `observer` will report all changes to 'handle' for as long as the observer is connected. However, it is not possible to _guarantee_ that all file system changes will be observed. Changes may be missed for the following reasons: 287 | 288 | * Changes made external to a centralized browser process may race with `FileSystemObserver` setup or disconnect. This applies to both renderer processes making changes to a Bucket File System and other processes on the system making changes to the local file system. See [Signaling Changes Made via a `FileSystemSyncAccessHandle`](#signaling-changes-made-via-a-`filesystemsyncaccesshandle`) for an example 289 | * Changes to the local file system may not always trigger a consumable (to the user agent) notification. See [When to Signal Local File System Writes](#when-to-signal-local-file-system-writes) for an example 290 | * Observing the local file system may fail for unexpected reasons 291 | 292 | ### Avoiding Exposing Implementation Details of the File System Specification 293 | 294 | A `FileSystemObserver` should not reveal details of the user agent's implementation of the File System specification. For example, 295 | 296 | * Change records should never be triggered for writes to a swap file created by `FileSystemFileHandle.createWritable()` 297 | * For a `FileSystemHandle.move()` within the scope of a directory observation, the operation should trigger a single `“moved”` change record, regardless of whether the operation was atomic under the hood 298 | 299 | ### Handling Changes Made Outside the Lifetime of a `FileSystemObserver` 300 | 301 | A `FileSystemObserver` should only report changes which occur while the observer is connected and the website has an open tab. [Historical modifications to the file](https://developer.apple.com/documentation/coreservices/1443980-fseventstreamcreate#parameters:~:text=watched%20for%20modifications.-,sinceWhen,-The%20service%20will) or modifications which occur while the tab is closed or in an otherwise non-”[fully active](https://html.spec.whatwg.org/multipage/document-sequences.html#fully-active)” state should not be reported. 302 | 303 | Likewise, changes which occur before an observer is created should not be reported, though this behavior is not strictly guaranteed since file system changes may race with `FileSystemObserver` setup. 304 | 305 | A `FileSystemObserver` is not [serializable](https://html.spec.whatwg.org/multipage/structured-data.html#serializable) and therefore cannot be persisted across browsing sessions. Websites which wish to watch the same files on each session may store serializable `FileSystemHandle` and `FileSystemObserverObserveOptions` objects in IndexedDB, then create a `FileSystemObserver` and configure it from these objects on page reload. 306 | 307 | ### Interactions with Back/forward Cache 308 | 309 | If changes occurred while the page was not fully active, and the page becomes active again (i.e. back/forward cache), then user agents may use the `"unknown"` `FileSystemChangeType` to indicate that _changes_ have occurred. Specific types and ordering of changes should not be exposed but indicating that some changes have occurred could be useful to the website to perform any special handling. 310 | 311 | ### Signaling Changes Made via a `FileSystemSyncAccessHandle` 312 | 313 | It is assumed that a user agent’s implementation of the `FileSystemObserver` interface will involve coordinating with a centralized browser process. However, unlike most web storage APIs, reading and writing files with a `FileSystemSyncAccessHandle` is commonly implemented largely without coordinating with a centralized browser process. This is critical to the exceptional performance characteristics of this interface. `write()` or `truncate()` operations on a `FileSystemSyncAccessHandle` should trigger a file system change record, but requiring round-trip IPC to complete before synchronously returning would be detrimental to performance. 314 | 315 | This has some side effects when it comes to [guaranteeing that changes are not missed](#guaranteeing-that-changes-are-not-missed) - specifically, that signaling to the centralized browser process that a write occurred could race with a `FileSystemObserver` disconnecting, resulting in a file system change being missed. In the example below, since `FileSystemSyncAccessHandle.write()` does not wait for an acknowledgement from the centralized browser process before synchronously returning, it is not possible to synchronize the write and disconnection of the observer using locks. 316 | 317 | ```javascript 318 | // Writes by a FileSystemSyncAccessHandle just before an observer is 319 | // disconnected may not trigger corresponding change records 320 | 321 | // main.js - Start observing a file 322 | const observer = new FileSystemObserver(callback); 323 | await observer.observe(fileHandle); 324 | 325 | // worker.js - Create a writable handle to the file 326 | const syncAccessHandle = await fileHandle.createSyncAccessHandle(); 327 | 328 | // If these statements execute at approximately the same time, will a 329 | // file system change be recorded? 330 | 331 | // worker.js 332 | syncAccessHandle.write(buffer); 333 | // main.js 334 | observer.disconnect(); 335 | ``` 336 | 337 | Note that [if the interface had a `takeRecords()` method](#add-a-takerecords-method), `FileSystemSyncAccessHandle.close()`could be used to synchronize disconnection of the observer. This should be considered as a future addition. 338 | 339 | ### Watching the Local File System 340 | 341 | #### Cross-Platform Compatibility 342 | 343 | Each operating system has its own mechanisms for observing file system changes. This proposal aims to offer a mostly-platform-neutral set of information about a change to the local file system based on the roughly-lowest-common-denominator set of information available from common modern operating systems. 344 | 345 | However, given the cross-platform differences, this proposal does not attempt to specify how exactly a change notification from the operating system maps to a file system change record. Consider the following scenario: 346 | 347 | ```javascript 348 | const callback = (records, observer) => { 349 | // What change record will be triggered when the file is created? 350 | // -> 1: { type: "appeared", relativePathComponents: ["file.txt"], ... } 351 | // 2: { type: "appeared", relativePathComponents: [], ... } 352 | // 3: { type: "modified", relativePathComponents: [], ... } 353 | } 354 | 355 | const observer = new FileSystemObserver(callback); 356 | await observer.observe(directoryHandle, { recursive: true }); 357 | await directoryHandle.getFileHandle('file.txt', { create: true }); 358 | ``` 359 | 360 | User agents should attempt to include the most precise information as it can reasonably obtain in the file system change record. In this example, the change record is most useful if it details that a specific file has been added (i.e. option 1) as opposed to mentioning just that the parent directory’s contents were modified - which would require the website to iterate through the directory to figure out which file has changed, and how. 361 | 362 | All changes to a Bucket File System should deterministically map to a precise file system change record. In this example, the `getFileHandle()` call should result in a change record with a `”appeared”` change type and describe the change as occurring on the created file. 363 | 364 | However, this level of detail is not realistic on all platforms for local file system changes. For example, Linux has no native support for recursive watches. As such, the details of a file system change record for a given change to the local file system should be regarded as best-effort. In the example below, the user agent may report either a `”appeared”` change type describing the created file, a `”appeared”` change type describing a creation within the observed directory, or a `”modified”` change type describing that the directory contents were modified. 365 | 366 | ```javascript 367 | const callback = (records, observer) => { 368 | // What change record will be triggered when the file is created? 369 | // ?? 1: { type: "appeared", relativePathComponents: ["file.txt"], ... } 370 | // ?? 2: { type: "appeared", relativePathComponents: [], ... } 371 | // ?? 3: { type: "modified", relativePathComponents: [], ... } 372 | } 373 | 374 | const observer = new FileSystemObserver(callback); 375 | await observer.observe(directoryHandle, { recursive: true }); 376 | // Now, create the file from outside the web 377 | // (e.g. open a terminal locally, navigate to the directory 378 | // corresponding to `directoryHandle`, then `touch file.txt`) 379 | ``` 380 | 381 | #### When to Signal Local File System Writes 382 | 383 | Writing to a file on the local file system generally looks like the following: 384 | 385 | 1. Open a file descriptor for writing (i.e. [`open()`](https://man7.org/linux/man-pages/man2/open.2.html) with write flags) 386 | 2. Issue (possibly numerous) writes to the file descriptor (i.e. [`write()`](https://man7.org/linux/man-pages/man2/write.2.html)) 387 | 3. Close the file descriptor (i.e.[` close()`](https://man7.org/linux/man-pages/man2/close.2.html)) 388 | 389 | Given the differences between the mechanisms for observing file system changes on each operating system, this proposal does not specify for which steps above change records should be relayed to JavaScript. Note that this _is_ specified when observing changes to the Bucket File System. See [Signaling Changes Made via a `FileSystemSyncAccessHandle`](#signaling-changes-made-via-a-`filesystemsyncaccesshandle`). 390 | 391 | Triggering a change record for each `write()` call is likely to be quite noisy, which could negatively affect performance. 392 | 393 | Meanwhile, triggering change record only on `close()` may both provide false positives (if the file descriptor is closed without writing anything) and false negatives (if the file descriptor is not closed while the `FileSystemObserver` is active). 394 | 395 | As such, websites should expect that file system change records with a `"modified"` type corresponding to files on the local file system may be noisy. 396 | 397 | #### Signaling Changes to File and Directory Metadata 398 | 399 | On some platforms, it may not be possible to distinguish modifications to file _contents_ from modifications to file _attributes_. For example, on Windows the [`FILE_NOTIFY_INFORMATION`](https://learn.microsoft.com/en-us/windows/win32/api/winnt/ns-winnt-file_notify_information) struct, which describes the changes found by the [ReadDirectoryChangesW](https://learn.microsoft.com/en-us/windows/win32/api/winbase/nf-winbase-readdirectorychangesw) function, lumps both modifications to file contents and attributes into the [`FILE_ACTION_MODIFIED`](https://learn.microsoft.com/en-us/windows/win32/api/winnt/ns-winnt-file_notify_information#members) `Action` type. 400 | 401 | As such, the `“modified”` file system change type is intentionally vague. As noted [above](#when-to-signal-local-file-system-writes), websites should already expect that file system change records with a `"modified"` type corresponding to files on the local file system may be noisy. This may include changes to file metadata which are not observable from the web. 402 | 403 | Currently, the only metadata available via the File System Access API is the metadata provided by the [`File`](https://developer.mozilla.org/en-US/docs/Web/API/File) interface: the MIME type, size, and last-modified timestamp. This may change in the future, however. See https://github.com/whatwg/fs/issues/12. 404 | 405 | #### Permission Considerations 406 | 407 | Using a `FileSystemObserver` should not give a website access to any files which it otherwise wouldn’t be able to access - nor should it relay information about a file which would otherwise be unknowable (e.g. by polling). 408 | 409 | For example, consider a website which is recursively observing the directory `foo/` on the local file system. If `foo/file.txt` is moved to `bar/file.txt`, the website can no longer access the file. Accordingly, the website should receive a change record suggesting that `foo/file.txt`has been removed, rather than describing the inaccessible location it has been moved to. 410 | 411 | Attempting to observe a file or directory without [`“read”`](https://wicg.github.io/file-system-access/#dom-filesystempermissionmode-read) permission will fail. If read access to the watched file or directory is lost, a change record with an `“errored”` type may be triggered. 412 | 413 | #### Resource Constraints 414 | 415 | Watching a large number of paths can consume scarce resources (e.g. memory usage, file descriptors). This is particularly true on Linux, which consumes file descriptors and watch descriptors to watch local file paths and which has no native support for recursive watches. 416 | 417 | Accordingly, user agents may add restrictions to how many files or directories a website can watch. 418 | 419 | #### Performance Considerations 420 | 421 | Applications which use the `FileSystemObserver` interface in place of polling the file system will almost certainly see a decrease in CPU usage as the result of no longer needing to burn CPU cycles polling. Whether this results in noticeable performance improvements likely depends on how this compares to the increased resource consumption (see above) seen on some platforms. 422 | 423 | Meanwhile, dispatching a potentially large number of events to websites that might not even be in the foreground could have significant performance impacts. Accordingly, user agents may choose to coalesce or rate-limit events. This API notably does _not_ require the user agent to guarantee a maximum latency for which events from the file system will be relayed to JavaScript. 424 | 425 | #### Fingerprinting Risk 426 | 427 | Multiple websites may be observing overlapping sets of files on the local file system. If change records for these files are dispatched at exactly the same time, this could be used to fingerprint a user even in incognito mode. A website with write access to local files could already determine that a user in incognito mode is the same as a user not in incognito, but by using a `FileSystemObserver` this becomes easier even with read-only access. 428 | 429 | Accordingly, user agents may add noise to changes reported from the local file system to reduce the fingerprinting risk of overlapping file system observations. 430 | 431 | #### Privacy Considerations 432 | 433 | As noted in [Handling Changes Made Outside the Lifetime of a `FileSystemObserver`](#handling-changes-made-outside-the-lifetime-of-a-`filesystemobserver`), a `FileSystemObserver` should only report changes which occur while the website has a [fully active](https://html.spec.whatwg.org/multipage/document-sequences.html#fully-active) page. 434 | 435 | ## Alternatives Considered 436 | 437 | ### Require Developers to Manually Implement File Path Watching 438 | 439 | The proposed API could be polyfilled from JavaScript, with varying degrees of complexity depending on whether you care to observe changes from outside your origin. 440 | 441 | For many websites, this API will “just” be an ergonomic improvement. For websites regularly polling the file system - particularly recursively watching changes to a directory, which may currently be prohibitively expensive - this API is expected to result in significant performance and behavioral improvements. 442 | 443 | ### Alternatives to the Observer Pattern 444 | 445 | #### Use an EventTarget-Style Interface 446 | 447 | Web Platform Design Principles [broadly recommend creating APIs that use Events rather than an Observer pattern](https://w3ctag.github.io/design-principles/#events-vs-observers). In this case, an Observer pattern is more appropriate since: 448 | 449 | * The callback may be [triggered recursively](https://w3ctag.github.io/design-principles/#guard-against-recursion) if a file is modified in response to a file system change 450 | * An Observer pattern allows the user agent to batch changes if necessary. For example, during a "git pull" operation that causes a flood of file system changes, these changes could be batched rather than firing the callback dozens of times 451 | 452 | #### Use an Async Iterable Interface 453 | 454 | [Deno.watchFS](https://deno.land/api@v1.27.0?s=Deno.watchFs) uses an async iterable to relay file system changes. 455 | 456 | ```javascript 457 | for await (const event of watcher) { 458 | // Do something with `event` 459 | } 460 | ``` 461 | 462 | Async iterables are generally used for iterating over a set of objects, such as [the contents of a directory](https://fs.spec.whatwg.org/#filesystemdirectoryhandle). This does not feel like the right paradigm for this use case. 463 | 464 | ### Allow Only One FileSystemHandle to be Observed per FileSystemObserver 465 | 466 | This proposal is modeled off of other “observer” interfaces on the web (e.g. [`MutationObserver`](https://dom.spec.whatwg.org/#interface-mutationobserver), [`IntersectionObserver`](https://www.w3.org/TR/intersection-observer/#intersectionobserver), [`PressureObserver`](https://www.w3.org/TR/compute-pressure/#the-pressureobserver-object)...) which all support observing multiple things per observer. One could ask if following that pattern is worth following here. 467 | 468 | For now, there doesn’t seem to be a strong argument for limiting a `FileSystemObserver` to observe only one `FileSystemHandle`. Barring such an argument, it seems reasonable to match the behavior of other observer interfaces and allow multiple observations per observer. Observing the same handle multiple times would overwrite any existing observation to the handle - [matching the behavior of `MutationObserver`](https://developer.mozilla.org/en-US/docs/Web/API/MutationObserver/observe#reusing_mutationobservers). 469 | 470 | A potential use case in support of multiple observations per observer is to get all pending changes at once - say, on tab closure. If only one `FileSystemHandle` could be observed per `FileSystemObserver`, this would require iterating through all observers to collect these changes. However, this would also require [a `takeRecords()` method](#add-a-takerecords-method), which isn't (yet) proposed here. 471 | 472 | ### Make `FileSystemObserverCallback` Pass a [Record](https://webidl.spec.whatwg.org/#idl-record) Keyed by the Observed Handle 473 | 474 | In some cases, this may result in more performant and ergonomic code, since determining whether a specific file or directory was modified becomes a map lookup rather than a full list traversal. 475 | 476 | For many cases, though, this would result in more boilerplate code. Further, it may be confusing whether the key refers to the handle which changed or the handle that was passed to `observe()`, which may be different if watching a directory recursively. 477 | 478 | Use cases which perform different actions based on the handle that was passed to `observe()`may create many observers, each with its own callback, rather than calling `observe()` many times on the same observer. 479 | 480 | ### Signal Changes Made via a `FileSystemSyncAccessHandle` Only on Close 481 | 482 | The [Signaling Changes Made via a `FileSystemSyncAccessHandle`](#signaling-changes-made-via-a-`filesystemsyncaccesshandle`) section above proposes signaling file system change records for each modifying operation. Alternatively, a single change record could be signaled when a `FileSystemSyncAccessHandle` which wrote to or truncated a file is closed. Note that this behavior may exist on some platforms when observing changes to the local file system. See details [above](#when-to-signal-local-file-system-writes). 483 | 484 | In this approach, a `FileSystemSyncAccessHandle` which does not write to or truncate a file would not trigger any change record on close. 485 | 486 | ```javascript 487 | // An alternative strategy to signaling changes made by a sync 488 | // access handle: only trigger a file system change record on close() 489 | 490 | const readHandle = await fileHandle.createSyncAccessHandle(); 491 | readHandle.read(buffer); // Does not modify the file 492 | readHandle.close(); // Do not trigger a change record 493 | 494 | const writeHandle = await fileHandle.createSyncAccessHandle(); 495 | writeHandle.write(buffer); // (likely) modifies the file 496 | writeHandle.close(); // Trigger a change record 497 | ``` 498 | 499 | This strategy leads to some unintuitive behavior. For example, consider this scenario: 500 | 501 | ```javascript 502 | // Unintutive behavior if sync access handles only triggered 503 | // file system change records on close() 504 | 505 | // Create and write to a file using a FileSystemSyncAccessHandle 506 | const syncAccessHandle = await fileHandle.createSyncAccessHandle(); 507 | syncAccessHandle.write(buffer); 508 | 509 | // Start observing the file 510 | const observer = new FileSystemObserver(callback); 511 | await observer.observe(fileHandle); 512 | 513 | // Closing the FileSystemSyncAccessHandle triggers a "modified" 514 | // change record, even though the write occurred before observation 515 | // started. 516 | syncAccessHandle.close(); 517 | ``` 518 | 519 | Meanwhile, the reverse behavior is also true: 520 | 521 | ```javascript 522 | // More unintutive behavior if sync access handles only triggered 523 | // file system change records on close() 524 | 525 | // Start observing a file 526 | const observer = new FileSystemObserver(callback); 527 | await observer.observe(fileHandle); 528 | 529 | // Create and write to the file using a FileSystemSyncAccessHandle 530 | const syncAccessHandle = await fileHandle.createSyncAccessHandle(); 531 | syncAccessHandle.write(buffer); 532 | 533 | observer.disconnect(); 534 | 535 | // No change record will be triggered, even though the write occurred 536 | // before the observer disconnected. 537 | syncAccessHandle.close(); 538 | ``` 539 | 540 | ## Future Improvements 541 | 542 | ### Support Filtering Change Records 543 | 544 | As currently proposed, a `FileSystemObserver` will notify of all file system changes which fall within the scope of the observation. However, it may be useful to filter change records before they’re forwarded to JavaScript, which would reduce noise and may provide some performance benefit. 545 | 546 | This proposal leaves the door open to add some or all of these filters later on. 547 | 548 | #### Change Source 549 | 550 | In the same way that `BroadcastChannel` does not send a message to the object that sent the message, the context which is modifying the file system often would prefer not to hear about changes that it itself is making. Likewise, there are some use cases where this is explicitly useful, such as journaling all changes to a given file. To support this use case, the website could optionally specify a filter in the options dictionary to subscribe only to changes from a given set of contexts: 551 | 552 | ```javascript 553 | // Future improvement: filtering by the source of a file system change 554 | 555 | enum FileSystemChangeSource { 556 | "self", // The change was made by the current agent 557 | "storagekey", // The change was made by this storage key 558 | "other" // The change was made by some other context 559 | }; 560 | 561 | // Observe only changes made from outside your storage key 562 | const options = { filters: [{ sources: ['other'] }]}; 563 | // Or, listen only to changes not made by the observing context 564 | // (like BroadcastChannel) 565 | const options = { exclusionFilters: [{ sources: ['self'] }]}; 566 | 567 | await observer.observe(fileHandle, options); 568 | ``` 569 | 570 | Using the options currently available to a website for tracking changes as a guide for granularity, a `FileSystemChangeRecord` could include a `source` without exposing any information to the web that isn’t already exposed. 571 | 572 | * Tracking changes by `“self”` can be achieved by logging file system operations as they’re performed by the current context 573 | * Tracking changes by `“storagekey”` can be achieved via `BroadcastChannel`, as described above. This could also be extended to include _which_ other tab modified a file 574 | * Tracking changes by `“other”` can be achieved by polling the file system, as described above 575 | 576 | #### Changed Path 577 | 578 | This option only makes sense for recursive observations, though may be a cause for confusion given [cross-platform differences](#cross-platform-compatibility). 579 | 580 | ```javascript 581 | // Future improvement: filtering the path of a file system change 582 | 583 | // Ignore changes to specific paths 584 | const options = { 585 | recursive: true, 586 | exclusionFilters: [{ paths: [['.git'], ['node_modules']] }] 587 | }; 588 | await observer.observe(repoRootDirHandle, options); 589 | ``` 590 | 591 | #### Change Type 592 | 593 | It’s unclear whether this would be useful - especially [cross-platform differences](#cross-platform-compatibility) - but I note it here for completeness. 594 | 595 | ### Signaling Changes to File Lock State 596 | 597 | Consider a website running a [SQLite-over-Wasm](https://sqlite.org/wasm/doc/trunk/index.md) database backed by a Bucket File System in a dedicated worker, which should be notified of database changes from the main thread as soon as the database file is unlocked. 598 | 599 | Having explicit `“locked”` and `”unlocked”` change types could allow websites to listen explicitly for changes to file lock state and attempt to acquire a lock once the file becomes unlocked. 600 | 601 | ```javascript 602 | // Future improvement: "locked" and "unlocked" file system change types 603 | 604 | // index.js 605 | const callback = (records, observer) => { 606 | if (records.some((record) => record.type === 'unlocked')) 607 | // Attempt to read back recent changes, or acquire a new lock 608 | } 609 | 610 | const observer = new FileSystemObserver(callback); 611 | await observer.observe(dbFileHandle); 612 | askWorkerToWriteSomeData(dbFileHandle, someData); 613 | 614 | // worker.js 615 | const syncAccessHandle = await dbFileHandle.createSyncAccessHandle(); 616 | syncAccessHandle.write('the associated file handle is locked'); 617 | syncAccessHandle.close(); // Releases the lock 618 | ``` 619 | 620 | For now, it’s unclear whether this would be useful. A `“modified”` event may be a good enough proxy for an `“unlocked”` event in many cases (though notably not when using a `FileSystemSyncAccessHandle`). We can always add these change types later on. 621 | 622 | ### Add a `takeRecords()` Method 623 | 624 | This method could be useful when disconnecting the observer to immediately fetch all pending file system change records. See an example use case [above](#allow-only-one-filesystemhandle-to-be-observed-per-filesystemobserver). This seems to be of limited usefulness, at least for now, given that we [cannot guarantee that changes are not missed](#guaranteeing-that-changes-are-not-missed) and that [changes can be made outside the lifetime of the observer](#handling-changes-made-outside-the-lifetime-of-a-`filesystemobserver`). 625 | 626 | 627 | ### Add a `listObservations()` Method 628 | 629 | This method could be useful to see which files and directories are being watched. For now it’s unclear whether this is needed, or what this should return for errored watches. 630 | 631 | ## Stakeholder Feedback / Opposition 632 | 633 | * Developers: Strongly positive 634 | * https://github.com/WICG/file-system-access/issues/72 635 | * https://github.com/whatwg/fs/issues/123 636 | * https://github.com/w3c/IndexedDB/issues/51 637 | * Gecko: Positive 638 | * https://github.com/mozilla/standards-positions/issues/942#issuecomment-2113526096 639 | * WebKit: No signals 640 | 641 | ## Appendix 642 | 643 | ### Tentative IDL 644 | 645 | ```javascript 646 | interface FileSystemObserver { 647 | constructor(FileSystemObserverCallback callback); 648 | Promise observe(FileSystemHandle handle, 649 | optional FileSystemObserverObserveOptions options = {}); 650 | void unobserve(FileSystemHandle handle); 651 | void disconnect(); 652 | }; 653 | 654 | callback FileSystemObserverCallback = void ( 655 | sequence records, 656 | FileSystemObserver observer 657 | ); 658 | 659 | enum FileSystemChangeType { 660 | "appeared", 661 | "disappeared", 662 | "modified", 663 | "moved", 664 | "unknown", // Change types are not known 665 | "errored" // This observation is no longer valid 666 | }; 667 | 668 | dictionary FileSystemObserverObserveOptions { 669 | bool recursive = false; 670 | }; 671 | 672 | interface FileSystemChangeRecord { 673 | // The handle that was passed to FileSystemObserver.observe 674 | readonly attribute FileSystemHandle root; 675 | // The handle affected by the file system change 676 | readonly attribute FileSystemHandle changedHandle; 677 | // The path of `changedHandle` relative to `root` 678 | readonly attribute FrozenArray relativePathComponents; 679 | // The type of change 680 | readonly attribute FileSystemChangeType type; 681 | // Former location of a moved handle. Used only when type === "moved" 682 | readonly attribute FrozenArray? relativePathMovedFrom; 683 | }; 684 | ``` 685 | -------------------------------------------------------------------------------- /proposals/MovingNonOpfsFiles.md: -------------------------------------------------------------------------------- 1 | # Moving Non-OPFS Files 2 | 3 | ## Authors 4 | 5 | * Austin Sullivan (asully@chromium.org) 6 | 7 | ## Participate 8 | 9 | * [Issue tracker](https://github.com/whatwg/fs/issues) 10 | 11 | ## Introduction 12 | 13 | When launching [SyncAccessHandles](https://github.com/whatwg/fs/pull/21), we launched `FileSystemFileHandle.move()` for files within the [Origin Private File System](https://web.dev/file-system-access/#accessing-files-optimized-for-performance-from-the-origin-private-file-system) (OPFS). Moving of files outside of the OPFS and moving directories at all are not yet supported. 14 | 15 | This explainer proposes allowing the `FileSystemFileHandle.move()` method to move files that do not live in the Origin Private File System, i.e. user-visible files on the device. 16 | 17 | ## Goals 18 | 19 | * Allow for all files to be efficiently renamed (rename is considered a subset of move) 20 | * Allow for all files to be moved to a different directory 21 | * Allow files to be moved within filesystems, while avoiding setting a regrettable precedent of supporting moves from anywhere to anywhere 22 | * Improve the ergonomics of the API 23 | 24 | ## Non-goals 25 | 26 | * Support moving files between the OPFS and the local file system 27 | * Support moving files between the local machine and a remote (not locally mounted) file system 28 | 29 | ## Improve the ergonomics of the API 30 | 31 | Currently, moving or renaming a file requires three steps: 32 | 33 | 1. Obtain 34 | 2. Copy 35 | 3. Delete 36 | 37 | Each of these three steps in is brittle: 38 | 39 | 1. Obtaining write access to the target file requires either: 40 | 1. Obtaining access the parent directory (which can be a hassle, because the API [does not have an easy way to get the parent](https://github.com/whatwg/fs/issues/38) of a handle and may not be possible if the parent directory is restricted, such as `Downloads/` or `Documents/`), then calling `getFileHandle(‘target.txt’, { create:true })` to create the target file, or 41 | 2. Calling `showSaveFilePicker({ startIn: sourceHandle, suggestedName: ‘target.txt’ })` and hoping the user selects the correct file 42 | 2. Copying the file contents is painfully slow for large files, may fail if the disk runs out of space, and may result in a partially-written file 43 | 3. Take care not to remove the source before you’ve confirmed that the target file was written in its entirety. If step 2 fails, the disk may have done all this work only to have to remove the partially-written target file and report to the user that the rename failed. This is a poor user experience 44 | 45 | The `move()` method drastically improves the ergonomics of the API. 46 | 47 | Before: 48 | 49 | ```javascript 50 | // Prompt the user to select a target file, suggested to be 51 | // 'target.txt', with "readwrite" access 52 | const targetHandle = await window.showSaveFilePicker({ startIn: sourceHandle, suggestedName: 'target.txt'}); 53 | 54 | // Copy the contents of the source file to the target file 55 | const sourceFile = await sourceHandle.getFile(); 56 | const writable = await targetHandle.createWritable(); 57 | await sourceFile.stream().pipeTo(writable); 58 | 59 | // Remove the source file if none of the steps above failed 60 | await sourceHandle.remove(); 61 | ``` 62 | 63 | After: 64 | 65 | ```javascript 66 | // Rename the file (may require user activation) 67 | await handle.move('target.txt'); 68 | ``` 69 | 70 | ## Use cases 71 | 72 | ### Rename a file 73 | 74 | A user is editing a large video file on the local disk with a video editing application and wants to rename the file from `old.mp4` to `new.mp4`. 75 | 76 | Currently, this requires all three steps above. For large files, step 2 is particularly troublesome. 77 | 78 | The `move()` method turns this into an efficient one-liner. Note that user activation may be required if the site does not have write access to the target file. 79 | 80 | ### Move a file to a new directory within the same file system 81 | 82 | A web photo editor wants to move a file from `Photos/IMG_20230123_123456789.jpg` to `Documents/MyVacation/beach.jpg`. 83 | 84 | Once write permission to the destination directory is acquired, `move()` replaces steps 2 and 3 from above. 85 | 86 | ### Move a file from an external drive to the local file system 87 | 88 | A web photo editor wants to move a file from `external_drive/IMG_20230123_123456789.jpg` to `Documents/MyVacation/beach.jpg`. 89 | 90 | Once write permission to the destination directory is acquired, `move()` replaces steps 2 and 3 from above. 91 | 92 | Under the hood, this is a create + copy + delete. But that’s for the underlying operating system to implement - not the browser. It’s possible this results in a partial write (e.g. if the drive is disconnected or runs out of space), but since the site has write access to the destination directory it may have a chance to remove the partially-written file. 93 | 94 | ## What about moving files from the OPFS to user visible directories, or vice-versa? 95 | 96 | Previous proposals included support for best-effort moves of files and directories across file systems. We are not pursuing this at this time. 97 | 98 | ### What challenges exist with moving files out of the OPFS? 99 | 100 | * Files in the OPFS [have few restrictions on the allowed characters in their names](https://github.com/web-platform-tests/wpt/blob/4981b40a9b00f87091c417e096e40c327b9407ed/fs/script-tests/FileSystemDirectoryHandle-getFileHandle.js#L18-L44), while [local files are limited by what’s allowed on the underlying operating system](https://fs.spec.whatwg.org/#valid-file-name). 101 | * Files written within the OPFS are not subject to security checks, while [user agents are encouraged](https://wicg.github.io/file-system-access/#security-malware) to perform safe browsing checks and apply the Mark-of-the-Web to local files created or modified by this API. Note that this challenge is significantly more daunting for directory moves. 102 | 103 | These challenges are all resolvable, but not without a compelling use case for OPFS <-> local file moves. 104 | 105 | ### What about exporting files from the OPFS to the local file system? 106 | 107 | A common use case cited in earlier proposals was to “export” a file from the OPFS to the local file system. In practice, we expect most sites just want to _copy_ the file to the local file system and retain a copy in the OPFS. 108 | 109 | Besides, since files within the OPFS may or may not correspond to “actual” files on disk, this move operation is likely to be a create + copy + delete anyways and would likely come with marginal performance gains, if any. 110 | 111 | For now, we don’t believe this is a use case which requires built-in API support. Developers can create + copy + delete (as they do today - see the code snippet above). 112 | 113 | ### What if I have a compelling use case? 114 | 115 | If a compelling use case comes along, we can always reconsider this decision and add support later. It’s much easier to add functionality to the web platform than to remove it. To make your case, please file an issue on the spec at . 116 | 117 | ### What about moving files between OPFS instances? 118 | 119 | The [Storage Buckets API](https://wicg.github.io/storage-buckets/explainer.html) will allow a site to have multiple Origin Private File System instances (whoops, [that name aged poorly](http://go/gh/whatwg/fs/issues/92)). Since that feature is still in incubation, we are not considering this at this time. 120 | 121 | ### What if I try it anyways? 122 | 123 | The promise will be rejected with a `InvalidModificationError` `DOMException`. 124 | 125 | ## What about moving files from the local file system to a remote machine, or vice-versa? 126 | 127 | The File System specification frequently mentions “the underlying file system.” If the file does not correspond to a file on the underlying file system, the user agent may reject the move operation with a `NotSupportedError` `DOMException`. 128 | 129 | Note that remote file systems may be mounted as directories on the local file system. The user agent is encouraged to support this use case, since the underlying operating system should be able to handle the move. The recommended rule of thumb is: if you can `mv` it you can `move()` it. 130 | 131 | ## What About Directory Moves? 132 | 133 | We would still like to support directory moves. Please read the [”What is a FileSystemHandle?”](https://github.com/whatwg/fs/issues/59) issue on GitHub for more context on why we’re punting on this for now. 134 | 135 | ## Security Considerations 136 | 137 | ### Overwriting Existing Files 138 | 139 | A site may overwrite an existing file only if it already explicitly has write access to the file being overwritten. Otherwise, the move will be rejected with a permission error. 140 | 141 | See [this doc](https://docs.google.com/document/d/1U6C6YvGtdwzw264xi7eXz26jha7vvT8d-WdwgnH7Ufw/edit?usp=sharing&resourcekey=0-OAo3LNSx9--4n8f_kNx6Vg) for more context. 142 | 143 | ### Security Checks 144 | 145 | User agents are recommended to perform security checks on files moved within the local file system. 146 | 147 | ### Permission Checks 148 | 149 | File moves will have the following requirements: 150 | 151 | * For cross-directory moves: 152 | * Write permission to the file being moved **is required** 153 | * Write permission to the destination directory **is required** 154 | * Write permission to the source directory **is not required** 155 | * For renames (moves within the parent directory): 156 | * Write permission to the file being moved **is required** 157 | * Write permission to the parent directory **is not required** 158 | * However, user activation is required if write permission is not granted to the destination file 159 | 160 | Previously, we had discussed requiring write permission to both the source and destination directories. However, while this may seem the more conservative option, it incentivizes sites to ask for permission to more than they otherwise would and is not an option in many cases (especially on ChromeOS). See the [Alternatives Considered](https://docs.google.com/document/d/1yMWkT9FAF0ohBRv_dzAcOpoNlWJ0n9n48K6_UQD-HVs/edit#heading=h.ulr5fzcm9d8k) section for more context. 161 | 162 | ### File Locking 163 | 164 | Moving a file will require obtaining an exclusive lock to both the source and destination files. For example, if a source or destination file has an open `FileSystemSyncAccessHandle` or `FileSystemWritableFileStream`, it cannot be moved. 165 | 166 | For files outside of the OPFS, these are cross-site locks. For example, if site A is actively writing to file `Y`, site B’s` Y.move(Z)` request will be denied with a "file locked" error. While this is technically a cross-site interaction, we do not foresee any security concerns with this behavior because: 167 | 168 | * A site will only encounter a file locked by another site if the user has explicitly granted access to the same file on multiple sites 169 | * A site can tell that the file is locked, but nothing more (i.e. not by whom) 170 | 171 | ## Alternatives Considered 172 | 173 | ### Support moving files files from anywhere to anywhere 174 | 175 | The web is an expansive platform that operates on all file systems. We do not want to set a precedent that the browser \_must\_ support moving \_any\_ file (or directory) from any one place to another. 176 | 177 | #### Support moving files to/from the OPFS 178 | 179 | See [What about moving files from the OPFS to user visible directories, or vice-versa?](#what-about-moving-files-from-the-opfs-to-user-visible-directories-or-vice-versa) 180 | 181 | ### Support only moving files which live on the same underlying file system 182 | 183 | See the [Move a file from an external drive to the local file system](#move-a-file-from-an-external-drive-to-the-local-file-system) use case. While this may be a create + copy + delete under the hood, from the browser’s perspective it’s just an `mv`. 184 | 185 | ### Require write permission to the parent directory for renames 186 | 187 | Requiring write access to the parent directory might feel like a conservative choice, but this: 188 | 189 | * may not be possible if the file lives in a blocked directory, such as `Downloads/`. This is especially significant on ChromeOS, where most files end up in the `MyFiles` directory 190 | * may incentivize sites to request access to directories rather than specific files (giving the site more access than they would otherwise ask for) 191 | * seems like an awfully big gap in the API, since any file the site has access to without the parent cannot be renamed (which is the case for most files saved via the `showSaveFilePicker()` API) 192 | 193 | Contrast that with the downsides of _not_ requiring write access to the parent directory: 194 | 195 | * A site may discover the names of siblings by brute-forcing file renames (while holding user activation) and listening for promise rejections 196 | 197 | However, the site has no way to access these siblings without showing a picker. This is a low-reward exercise. The privacy risk of incentivizing sites to request a directory picker seems much greater. 198 | 199 | ### Require write permission to the source directory 200 | 201 | From the perspective of the source directory, a cross-directory move looks the same as `remove()` (i.e. the file disappears). `remove()` does not require write access to the parent, so this is not a concern. 202 | 203 | ### Always allow overwriting files 204 | 205 | We cannot allow a site to overwrite files which it does not explicitly have write access to. 206 | 207 | ### Never allow overwriting files 208 | 209 | Emulating POSIX (which allows overwriting files) within the OPFS is a compelling use case for this. See . 210 | 211 | ### Do not support cross-site locks 212 | 213 | This would allow multiple sites to take their own exclusive locks to a given file. While this would prevent sites from encountering “file locked by another site” behavior, it would also erode the guarantees of an “exclusive” lock. 214 | 215 | ### Only support renaming 216 | 217 | This would not support cross-directory same-file-system moves, which makes the API significantly less useful to applications such as web-based IDEs. 218 | 219 | ## Stakeholder Feedback / Opposition 220 | 221 | * Developers: Strongly positive 222 | * 223 | * Gecko: No signals 224 | * WebKit: No signals 225 | -------------------------------------------------------------------------------- /proposals/MultipleReadersWriters.md: -------------------------------------------------------------------------------- 1 | # New Locking Scheme to Enable Multiple Readers and Writers 2 | 3 | ## Authors 4 | 5 | * Daseul Lee (dslee@chromium.org) 6 | 7 | ## Participate 8 | 9 | * [Issue Tracker](https://github.com/whatwg/fs/issues) 10 | * [Discussion Forum](https://github.com/whatwg/fs/issues/34) 11 | 12 | ## Introduction 13 | 14 | Currently, only one instance of [FileSystemSyncAccessHandle](https://fs.spec.whatwg.org/#api-filesystemsyncaccesshandle) may be open at a time, given a [file system entry](https://fs.spec.whatwg.org/#entry). This explainer proposes a new locking scheme and API changes to support multiple readers and writers for `FileSystemSyncAccessHandle` and an exclusive writer for `FileSystemWritableFileStream`. 15 | 16 | Introducing new locking modes for [FileSystemSyncAccessHandle](https://fs.spec.whatwg.org/#api-filesystemsyncaccesshandle) and [FileSystemWritableFileStream](https://fs.spec.whatwg.org/#api-filesystemwritablefilestream) allows opening either multiple readers/writers or an exclusive writer to a file entry, depending on the application's use case. 17 | 18 | ``` 19 | handle.createSyncAccessHandle({ mode: 'read-only' }); 20 | handle.createSyncAccessHandle({ mode: 'readwrite-unsafe' }); 21 | 22 | handle.createWritable({ mode: 'exclusive' }); 23 | ``` 24 | 25 | ## Goals 26 | 27 | * Support multiple readers and writers for `FileSystemSyncAccessHandle` 28 | * Support exclusive writer for `FileSystemWritableFileStream` 29 | * Ensure operations which modify a file or directory cannot clobber each other 30 | 31 | ## Non-goals 32 | 33 | * Opening `FileSystemSyncAccessHandle` with no restrictions 34 | * Support Posix-like file locking primitives 35 | 36 | ## Motivating Use Cases 37 | 38 | ### Write Once, Read Many 39 | A text editor app provides version control, and keeps metadata as a local file in the Bucket File System. This metadata file has a map of version history, so the app does not want this file to be written while it’s being read to load the file contents from a correct version, from multiple tabs. 40 | Using “read-only” mode, `FileSystemSyncAccessHandle` can be opened for this metadata file, and any overwriting would be disallowed. The app would not need to provide its own locking mechanism. 41 | 42 | ### Caching large files 43 | An image editor app wants to store large media files in the Bucket File System using `FileSystemSyncAccessHandle` so that image files do not need to be downloaded again when the user opens the app from multiple tabs. 44 | 45 | Currently, each time the app is open, it needs to coordinate closing the existing `FileSystemSyncAccessHandle` and opening a new one via a dedicated worker. 46 | 47 | With multiple readers, the site can easily load images from multiple tabs, without additional network cost. 48 | 49 | ### Mitigate performance overhead from asynchronous open 50 | A database ported to Wasm wants to mitigate performance issues caused by the asynchronous `createSyncAccessHandle()` method. 51 | 52 | For use cases with a known set of files, the performance cost of going async from Wasm can be mitigated if multiple `FileSystemSyncAccessHandle` are opened up front. After bearing the one-time cost of these asynchronous calls, the application can interact with the files fully synchronously. 53 | 54 | ### Allow applications to provide its own granular access scheme for FileSystemSyncAccessHandles 55 | An application may want to define its own locking mechanism, such as byte-range locking, to provide more granular access. 56 | 57 | This is currently not possible as only one `FileSystemSyncAccessHandle` is allowed at a time. With multiple handles, advanced applications can provide data access protection via its own implementation of lock or [SharedArrayBuffers](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/SharedArrayBuffer) with [Atomics](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Atomics). 58 | 59 | ### Provide an alternative to the last-writer-wins behavior of FileSystemWritableFileStream 60 | Currently, multiple `FileSystemWritableFileStream` writers can be opened at the same time, but only the last writer wins, as the data gets flushed when the writer is closed. Some applications using `FileSystemWritableFileStream` to write local files may prefer to have API-provided locking behavior. 61 | 62 | Introducing an “exclusive” mode for `FileSystemWritableFileStream` would prevent writers clobbering each other. 63 | 64 | ## Modes of Creating a FileSystemSyncAccessHandle 65 | Currently, opening multiple `FileSystemSyncAccessHandle` fails with a `NoModificationAllowedError`, taking an exclusive lock on a file entry. 66 | 67 | Allowing concurrent access to a file could provide flexibility and performance improvement for apps. In some cases, sites may need to do read-only operation and still prefer to have the data access protection provided by the API (i.e. site from another tab is not writing to the same file, while it is being read). In other cases, a site may be okay with concurrent writes and could deal with the data race at the application-level. To accommodate different use cases, a new mode will be specified when opening a `FileSystemSyncAccessHandle`. 68 | 69 | ``` 70 | enum FileSystemSyncAccessHandleMode { 71 | "readwrite", 72 | "read-only", 73 | "readwrite-unsafe", 74 | }; 75 | 76 | dictionary FileSystemCreateSyncAccessHandleOptions { 77 | FileSystemSyncAccessHandleMode mode = "readwrite"; 78 | }; 79 | 80 | interface FileSystemFileHandle : FileSystemHandle { 81 | ... 82 | Promise createSyncAccessHandle( 83 | optional FileSystemCreateSyncAccessHandleOptions options = {}); 84 | }; 85 | 86 | interface FileSystemSyncAccessHandle { 87 | ... 88 | readonly attribute USVString mode; // available via attribute 89 | }; 90 | ``` 91 | 92 | `readwrite` mode 93 | * Once open, any methods on `FileSystemSyncAccessHandle` are allowed. 94 | * Only one instance of `FileSystemSyncAccessHandle` is allowed. 95 | * This mode is the current behavior that allows safe data access, therefore is the default mode. 96 | 97 | `read-only` mode (multiple readers) 98 | * Once open, only read-like methods on `FileSystemSyncAccessHandle` are allowed: `read()`, `getSize()`, `close()` 99 | * Multiple instances of `FileSystemSyncAccessHandle` may be created as long as all of them are in `read-only` mode. 100 | 101 | `readwrite-unsafe` mode (multiple writers) 102 | * Once open, any methods on `FileSystemSyncAccessHandle` are allowed. 103 | * Multiple instances of `FileSystemSyncAccessHandle` may be created as long as all of them are in `readwrite-unsafe` mode. 104 | 105 | The current behavior is preserved by keeping the `readwrite` option as the default, which only allows one instance at a time. If a site needs to open multiple sync access handles but does not need to perform writes, then the `read-only` option should be used. Finally, the last option `readwrite-unsafe` allows multiple instances as well as both read and write. In this case, writes can be racy if performed from multiple tabs, and sites would need to provide their own locking scheme. 106 | 107 | See the examples below: 108 | 109 | ```js 110 | // 'readwrite' SyncAccessHandle is open; no other lock-requiring operations are allowed until the handle is closed. 111 | const accessHandle = await handle.createSyncAccessHandle({mode: 'readwrite'}); 112 | accessHandle.write(buffer); // successful 113 | accessHandle.read(buffer); // successful 114 | 115 | await handle.createSyncAccessHandle({mode: 'readwrite'}); // throws NoModificationAllowedError 116 | await handle.createSyncAccessHandle({mode: 'read-only'}); // throws NoModificationAllowedError 117 | await handle.createSyncAccessHandle({mode: 'readwrite-unsafe'}); // throws NoModificationAllowedError 118 | ``` 119 | 120 | ```js 121 | // 'read-only' SyncAccessHandle is open; besides opening another read-only SyncAccessHandle, 122 | // no other lock-requiring operations are allowed until the handle is closed. 123 | const accessHandle1 = await handle.createSyncAccessHandle({mode: 'read-only'}); 124 | accessHandle1.write(buffer); // throws NoModificationAllowedError 125 | accessHandle1.read(buffer); // successful 126 | const accessHandle2 = await handle.createSyncAccessHandle({mode: 'read-only'}); 127 | 128 | await handle.createSyncAccessHandle({mode: 'readwrite'}); // throws NoModificationAllowedError 129 | await handle.createSyncAccessHandle({mode: 'readwrite-unsafe'}); // throws NoModificationAllowedError 130 | 131 | accessHandle1.close(); // only one lock released 132 | await handle.createSyncAccessHandle({mode: 'readwrite'}); // still throws NoModificationAllowedError 133 | 134 | accessHandle2.close(); // all locks released 135 | await handle.createSyncAccessHandle({mode: 'readwrite'}); // successful 136 | ``` 137 | 138 | ```js 139 | // 'readwrite-unsafe' SyncAccessHandle is open; besides opening another 'readwrite-unsafe' SyncAccessHandle, 140 | // no other lock-requiring operations are allowed until the handle is closed. 141 | const accessHandle1 = await handle.createSyncAccessHandle({mode: 'readwrite-unsafe'}); 142 | accessHandle1.write(buffer); // successful 143 | accessHandle1.read(buffer); // successful 144 | const accessHandle2 = await handle.createSyncAccessHandle({mode: 'readwrite-unsafe'}); 145 | 146 | await handle.createSyncAccessHandle({mode: 'readwrite'}); // throws NoModificationAllowedError 147 | await handle.createSyncAccessHandle({mode: 'readonly'}); // throws NoModificationAllowedError 148 | 149 | accessHandle1.close(); // only one lock released 150 | await handle.createSyncAccessHandle({mode: 'readwrite'}); // still throws NoModificationAllowedError 151 | 152 | accessHandle2.close(); // all locks released 153 | await handle.createSyncAccessHandle({mode: 'readwrite'}); // successful 154 | ``` 155 | 156 | ## Modes of Creating a FileSystemWritableFileStream 157 | Unlike `FileSystemSyncAccessHandle`, many instances of `FileSystemWritableFileStream` can be created per file entry. To provide an option for an exclusive writer, a similar mode will be added to `FileSystemCreateWritableOptions`. 158 | 159 | ``` 160 | enum FileSystemWritableFileStreamMode { 161 | "exclusive", // Only one writer can exist at a time 162 | "siloed", // Each writer created will have its own swap file 163 | }; 164 | 165 | dictionary FileSystemCreateWritableOptions { 166 | ... 167 | FileSystemWritableFileStreamMode mode; 168 | }; 169 | ``` 170 | 171 | ## Changes to Locking Scheme 172 | 173 | ### Locking Within Primitive & Across Primitive/Operations 174 | In the File System Access API, there are two lock types: “shared” and “exclusive”. Acquiring a lock is required for performing operations on a [FileSystemHandle](https://fs.spec.whatwg.org/#api-filesystemhandle) and for using file primitives: `FileSystemSyncAccessHandle` and `FileSystemWritableFileStream`. As the name suggests, at most one exclusive lock can be taken at a time given a file entry, and multiple shared locks can be taken at a time. `FileSystemWritableFileStream` requires a shared lock, while the rest requires an exclusive lock. 175 | 176 | ``` 177 | FileSystemAccessFileHandle.createWritable()* Shared 178 | FileSystemAccessFileHandle.createSyncAccessHandle()* Exclusive 179 | FileSystemAccessFileHandle/DirectoryHandle.move()** Exclusive 180 | FileSystemAccessFileHandle/DirectoryHandle.remove()** Exclusive 181 | FileSystemAccessDirectoryHandle.removeEntry()** Exclusive 182 | ``` 183 | 184 | \* The lock is released when the file primitive is closed. 185 | 186 | \** The lock is released when the operation completes. 187 | 188 | To support multiple readers and writers, simply switching to the shared lock for both `readonly` and `readwrite-unsafe` modes does not work because there is no way to enforce the exclusivity between different access modes. And what if `FileSystemWritableFileStream` is open, holding the shared lock already? It may cause unexpected behavior if the site intended to create multiple readers of `FileSystemSyncAccessHandle` for safe reading. 189 | 190 | There are two dimensions to consider: 191 | 192 | 1) Across different types of primitives and operations 193 | * Only one type of primitive can be opened at a time 194 | * Any modifying operation (that requires a lock, as listed above) on a `FileSystemHandle` is allowed only if there is no open primitive, and vice versa. 195 | 2) Within the primitive 196 | * A “mode” specifies whether to allow only one or multiple instances of the same primitive type, and how they may be used 197 | 198 | In the new locking scheme, these two dimensions will be specified as “lock type” and “lock mode”. Lock type refers to the type of primitive or operation, in order to enforce exclusive lock between them. Lock mode refers to whether the access to a file entry could be shared within the same type of primitive. 199 | 200 | For example, read-only `FileSystemSyncAccessHandle` would take LockType of “sync-access-handle”, which prevents other primitives or operations to perform, and LockMode of “shared-read-only”, which lets multiple readers of `FileSystemSyncAccessHandle` to be created. 201 | 202 | ```js 203 | const accessHandle1 = await handle.createSyncAccessHandle({mode: 'read-only'}); 204 | const accessHandle2 = await handle.createSyncAccessHandle({mode: 'read-only'}); 205 | 206 | await handle.move('target.txt'); // throws NoModificationAllowedError 207 | await handle.createWritable(); // throws NoModificationAllowedError 208 | 209 | accessHandle1.close(); 210 | accessHandle2.close(); 211 | 212 | await handle.move('target.txt'); // successful 213 | await handle.createWritable(); // successful 214 | ``` 215 | 216 | ### Preventing Modification of Parents 217 | A file should not be able to be moved or removed if it has an open `FileSystemSyncAccessHandle` or `FileSystemWritableFileStream`. Accordingly, the file’s parents should not be movable or removable, either. 218 | 219 | ```js 220 | const childHandle = parentHandle.getFileHandle("foo.txt"); 221 | const childAccessHandle = await childHandle.createSyncAccessHandle({mode: 'readwrite'}); 222 | 223 | await parentHandle.remove(); // throws NoModificationAllowedError 224 | childAccessHandle.close(); 225 | await parentHandle.remove(); // successful 226 | ``` 227 | 228 | ### Interactions with BFCache 229 | A page may still hold a file system lock when it enters the BFCache. A fully active page could then be made aware of a BFCached page if there is contention between locks they hold. 230 | 231 | To keep BFCache enabled when a site uses the File System Access API, a BFCached page must be evicted on locking contention with a fully active page (whether or not it is of the same origin). Otherwise, a file system lock held by a page will not affect the page's eligibility for BFCache. This allows the site to have the performance gains of BFCache up until it would be made aware of the BFCache. 232 | 233 | ## Alternatives Considered 234 | 235 | ### Not Locking File Entry 236 | Not locking a file entry is one way to support multiple readers and writers. The argument for this is that in the Bucket File System, an origin is the only one accessing its own local files and could choose to provide its own locking mechanism, without help from the browser. However, this would make it much easier to improperly use this API; web developers often [prefer protective behaviors from the file system](https://github.com/whatwg/fs/issues/34#issuecomment-1248731620). 237 | 238 | ### Specifying lock, write and access modes separately 239 | whatwg/fs#19 suggests specifying lock, write and access modes separately (vs. one type of mode, associating access and lock behavior together). 240 | 241 | This approach provides more flexibility for applications in wanting to choose a specific behavior. However, some combinations are not valid, such as “exclusive + read-only”. Also, exposing the concept of lock at the API-level might be confusing to users. Multiple `FileSystemWritableFileStream` writers are already allowed, so does “shared” `FileSystemSyncAccessHandle` mean that it holds a shared lock within `FileSystemSyncAccessHandle` primitive type, or across all File System Access primitives? This ambiguity could be confusing to the end users. Finally, it’s not clear how a “shared” access mode would interact with ”atomic-from-copy” write mode (i.e. swap file). 242 | 243 | ### Support multiple readers, but not multiple writers 244 | A concern around multiple writers is error-prone usage, resulting in racy writes. Applications would be responsible for preventing and/or handling data races caused by concurrent writes, presumably using SharedArrayBuffer and Atomics. 245 | 246 | On the other hand, the main benefits and arguments for multiple writers are the following: 247 | 248 | * It’s very common for web apps to be run on multiple tabs 249 | * [Mitigate performance overhead from asynchronous open](#mitigate-performance-overhead-from-asynchronous-open) 250 | * [Emscripten intends to use both multiple readers and writers](https://github.com/whatwg/fs/issues/34#issuecomment-1212609690) 251 | 252 | ### Defining all operations as readonly or readwrite 253 | As a way to redesign the locking scheme that supports multiple readers, whatwg/fs#34 suggests defining all operations of File System Access API as either “readonly” or “readwrite”, and allow them to be queued. For example, if a file entry does not have any operation, a read or write operation can start. If there is a read operation, only another read operation can start but a write operation will wait; if there is a write operation, all other operations will wait. This idea was dismissed as it would introduce breaking changes to the API, by adding a lock to `FileSystemFileHandle.getFile()`, which currently does not have any locking restrictions. In addition, `getFile()` returns a readable `File` object. Unlike `FileSystemSyncAccessHandles`, `FileSystemWritableFileStreams`, or file-modifying methods such as `move()`, a `File` object does not have a clear open and close lifetime. It is not clear when the lock could be released. Finally, it would also close the door for supporting multiple writers in the future. 254 | 255 | ### Byte-range locking with one writer 256 | Instead of allowing multiple writers, the API could provide byte-range locking on a single writer. However, this would add a lot of complexity for something with an unclear impact on performance. “readwrite-unsafe” mode will allow sites to implement their own byte-range locking, if they wish to have more granular locking control. 257 | 258 | ### Not Locking Parents 259 | Without parent directory locking, a directory could be moved or removed while there is an open writer, which may not be the application's intention. Locking parents is currently unspecified. Recently, whatwg/fs/pull/96 specifies that FileSystemHandle is associated with a file path. Therefore, it makes sense to assume that applications intend to edit the file at the path that the `FileSystemSyncAccessHandle` was initially opened at. 260 | 261 | ## Stakeholder Feedback / Opposition 262 | * Developers: [Positive](https://github.com/whatwg/fs/issues/34) 263 | * Gecko: [Positive with regards to allowing multiple FileSystemSyncAccessHandles. Stance on the shape of this specific proposal is not yet known.](https://github.com/whatwg/fs/issues/34) 264 | * Webkit: [Positive with regards to allowing multiple read-only FileSystemSyncAccessHandles. Stance on the shape of this specific proposal is not yet known.](https://github.com/whatwg/fs/issues/34) 265 | -------------------------------------------------------------------------------- /proposals/Remove.md: -------------------------------------------------------------------------------- 1 | # The FileSystemHandle.remove() method 2 | 3 | # Authors: 4 | 5 | * Austin Sullivan (asully@chromium.org) 6 | 7 | ## Participate 8 | 9 | * [Issue tracker](https://github.com/whatwg/fs/issues) 10 | 11 | ## Introduction 12 | 13 | This explainer proposes a "remove self" method for a `FileSystemHandle`. 14 | 15 | Currently, it is not possible to remove a file or directory given its handle. 16 | You must obtain the handle of the parent directory, which there is no 17 | straightforward way to do and may not be possible in some cases, and call 18 | `FileSystemDirectoryHandle.removeEntry()`. 19 | 20 | ## Goals 21 | 22 | * Allow removal of any entry a site has write access to 23 | * Avoid surprises by matching the behavior and API shape of 24 | `FileSystemDirectoryHandle.removeEntry()` 25 | 26 | ## Use Cases 27 | 28 | ### Removing a handle selected via showSaveFilePicker() 29 | 30 | It's quite common for a site to obtain a file handle from 31 | `showSaveFilePicker()`, but then decide not to save after all, and want 32 | to delete the file. 33 | 34 | Currently, this requires obtaining write access to the parent directory and 35 | calling `removeEntry()` on the file. However, files selected from 36 | `showSaveFilePicker()` are often in the Downloads/ or Documents/ folders, which 37 | we do not allow the site to acquire directory handles to. 38 | 39 | ```javascript 40 | // Acquire a file handle to save some data 41 | const handle = await window.showSaveFilePicker(); 42 | // Write some data to the file 43 | const writable = await handle.createWritable(); 44 | await writable.write(contents); 45 | 46 | // ... some time later ... 47 | 48 | // Nevermind - remove the file 49 | await handle.remove(); 50 | ``` 51 | 52 | ### Allow applications to clear data not managed by the browser 53 | 54 | One use case of the File System Access API is for a site to show a directory 55 | picker to a location where the user would like its application data stored. 56 | Unlike other storage mechanisms provided by the browser, files on the user's 57 | machine are not tracked by the browser's quota system (meaning it can't be 58 | evicted), nor will it be cleared when the user clears site data. 59 | 60 | The `id` and `startIn` fields can be specified to suggest the directory in 61 | which the file picker opens. See 62 | [details in the spec](https://wicg.github.io/file-system-access/#api-filepickeroptions-starting-directory). 63 | 64 | There some significant downsides to this approach, most notably the inability 65 | to use the `FileSystemSyncAccessHandle` interface for non-OPFS files. 66 | Additionally, if a well-behaving application wants to clear all its associated 67 | data, it currently cannot remove the root of the directory. 68 | 69 | ```javascript 70 | // Application asks "Where shall I save my data?" 71 | // User selects a new directory: /user/blah/AwesomeAppData/ 72 | const dirHandle = await window.showDirectoryPicker(); 73 | 74 | // ... some time later ... 75 | 76 | // User asks "Please clear my data" 77 | 78 | // Before: /user/blah/AwesomeAppData/ can be emptied, but the application 79 | // _cannot_ remove the directory itself 80 | await dirHandle.removeEntry({ recursive: true }); 81 | // After: /user/blah/AwesomeAppData/ is removed 82 | await dirHandle.remove({ recursive: true }); 83 | ``` 84 | 85 | ### Improve ergonomics of the API 86 | 87 | Currently, removing an entry requires not only write access to the parent 88 | directory, but the parent directory itself. This can be a hassle, especially 89 | because the API [does not have an easy way to get the parent](https://github.com/whatwg/fs/issues/38) 90 | of a handle. 91 | 92 | ```javascript 93 | // Given `handle` that I want to remove… 94 | 95 | // Before: Somehow acquire the parent directory. Hopefully you've kept around 96 | // its root. You'll need to: 97 | // - resolve the handle to the root to get the intermediate path components 98 | const pathComponents = await root.resolve(handle); 99 | // - create the directory handle for each intermediate directory 100 | let parent = root; 101 | for (const component of pathComponents) 102 | parent = await parent.GetDirectoryHandle(component); 103 | // - finally, remove based on the handle's name 104 | await parent.removeEntry(handle.name); 105 | 106 | // After: just remove the handle 107 | await handle.remove(); 108 | ``` 109 | 110 | ## Security Considerations 111 | 112 | * Removing a file or directory requires write access to the associated entry. 113 | For example, files selected via `showOpenFilePicker()` are read-only by 114 | default and will not be removable unless the user explicitly grants write 115 | access to the entry 116 | * Recursive directory removal is currently possible via the `removeEntry()` 117 | method of the `FileSystemDirectoryHandle` 118 | * This method allows for removal of the root entry selected from the file 119 | picker, but since applications are 120 | [not able to obtain a handle to sensitive directories](https://github.com/WICG/file-system-access/blob/main/security-privacy-questionnaire.md#26-what-information-from-the-underlying-platform-eg-configuration-data-is-exposed-by-this-specification-to-an-origin) 121 | in the first place, this root entry is guaranteed not to be considered 122 | sensitive 123 | 124 | ## Stakeholder Feedback / Opposition 125 | 126 | * Developers: [Positive](https://github.com/WICG/file-system-access/issues/214) 127 | * Gecko: [Positive](https://github.com/WICG/file-system-access/pull/283#issuecomment-1036085470) 128 | * WebKit: No signals 129 | -------------------------------------------------------------------------------- /review-drafts/2022-03.bs: -------------------------------------------------------------------------------- 1 | 12 | 13 | 16 | 17 |
 18 | urlPrefix: https://tc39.es/ecma262/; spec: ECMA-262
 19 |   type: dfn; text: realm; url: realm
 20 | urlPrefix: https://storage.spec.whatwg.org/; spec: storage
 21 |   type: dfn; text: storage; url: site-storage
 22 | 
23 | 24 | 43 | 44 | 45 | # Introduction # {#introduction} 46 | 47 | *This section is non-normative.* 48 | 49 | This document defines fundamental infrastructure for file system APIs. In addition, it defines an 50 | API that makes it possible for websites to get access to a file system directory without having to 51 | first prompt the user for access. This enables use cases where a website wants to save data to disk 52 | before a user has picked a location to save to, without forcing the website to use a completely 53 | different storage mechanism with a different API for such files. The entry point for this is the 54 | {{StorageManager/getDirectory()|navigator.storage.getDirectory()}} method. 55 | 56 | 57 | # Files and Directories # {#files-and-directories} 58 | 59 | ## Concepts ## {#concepts} 60 | 61 | An entry is either a [=file entry=] or a [=directory entry=]. 62 | 64 | 65 | Each [=/entry=] has an associated query access algorithm, which takes "`read`" 66 | or "`readwrite`" mode and returns a {{PermissionState}}. Unless specified 67 | otherwise it returns "{{PermissionState/denied}}". The algorithm is allowed to throw. 68 | 69 | Each [=/entry=] has an associated request access algorithm, which takes 70 | "`read`" or "`readwrite`" mode and returns a {{PermissionState}}. Unless specified 71 | otherwise it returns "{{PermissionState/denied}}". The algorithm is allowed to throw. 72 | 73 | Note: Implementations that only implement this specification and not dependent specifications do not 74 | need to bother implementing [=/entry=]'s [=entry/query access=] and [=entry/request access=]. 75 | 76 | Each [=/entry=] has an associated name (a [=string=]). 77 | 78 | A valid file name is a [=string=] that is not an empty string, is not equal to "." or "..", 79 | and does not contain '/' or any other character used as path separator on the underlying platform. 80 | 81 | Note: This means that '\' is not allowed in names on Windows, but might be allowed on 82 | other operating systems. Additionally underlying file systems might have further restrictions 83 | on what names are or aren't allowed, so a string merely being a [=valid file name=] is not 84 | a guarantee that creating a file or directory with that name will succeed. 85 | 86 | Issue: We should consider having further normative restrictions on file names that will 87 | never be allowed using this API, rather than leaving it entirely up to underlying file 88 | systems. 89 | 90 | A file entry additionally consists of 91 | binary data (a [=byte sequence=]) and a 92 | modification timestamp (a number representing the number of milliseconds since the Unix Epoch). 93 | 94 | A directory entry additionally consists of a [=/set=] of 95 | children, which are themselves [=/entries=]. Each member is either a [=/file=] or a [=directory=]. 96 | 97 | An [=/entry=] |entry| should be [=list/contained=] in the [=children=] of at most one 98 | [=directory entry=], and that directory entry is also known as |entry|'s parent. 99 | An [=/entry=]'s [=entry/parent=] is null if no such directory entry exists. 100 | 101 | Note: Two different [=/entries=] can represent the same file or directory on disk, in which 102 | case it is possible for both entries to have a different parent, or for one entry to have a 103 | parent while the other entry does not have a parent. 104 | 105 | [=/Entries=] can (but don't have to) be backed by files on the host operating system's local file system, 106 | so it is possible for the [=binary data=], [=modification timestamp=], 107 | and [=children=] of entries to be modified by applications outside of this specification. 108 | Exactly how external changes are reflected in the data structures defined by this specification, 109 | as well as how changes made to the data structures defined here are reflected externally 110 | is left up to individual user-agent implementations. 111 | 112 | An [=/entry=] |a| is the same as an [=/entry=] |b| if |a| is equal to |b|, or 113 | if |a| and |b| are backed by the same file or directory on the local file system. 114 | 115 | Issue: TODO: Explain better how entries map to files on disk (multiple entries can map to the same file or 116 | directory on disk but an entry doesn't have to map to any file on disk). 117 | 118 |
119 | To resolve an [=/entry=] |child| relative to a [=directory entry=] |root|, 120 | run the following steps: 121 | 122 | 1. Let |result| be [=a new promise=]. 123 | 1. Run the following steps [=in parallel=]: 124 | 1. If |child| is [=the same as=] |root|, 125 | [=/resolve=] |result| with an empty list, and abort. 126 | 1. Let |childPromises| be « ». 127 | 1. [=set/For each=] |entry| of |root|'s [=FileSystemHandle/entry=]'s [=children=]: 128 | 1. Let |p| be the result of [=entry/resolving=] |child| relative to |entry|. 129 | 1. [=list/Append=] |p| to |childPromises|. 130 | 1. [=Upon fulfillment=] of |p| with value |path|: 131 | 1. If |path| is not null: 132 | 1. [=list/Prepend=] |entry|'s [=entry/name=] to |path|. 133 | 1. [=/Resolve=] |result| with |path|. 134 | 1. [=Wait for all=] |childPromises|, with the following success steps: 135 | 1. If |result| hasn't been resolved yet, [=/resolve=] |result| with `null`. 136 | 1. Return |result|. 137 | 138 |
139 | 140 | ## The {{FileSystemHandle}} interface ## {#api-filesystemhandle} 141 | 142 | 143 | enum FileSystemHandleKind { 144 | "file", 145 | "directory", 146 | }; 147 | 148 | [Exposed=(Window,Worker), SecureContext, Serializable] 149 | interface FileSystemHandle { 150 | readonly attribute FileSystemHandleKind kind; 151 | readonly attribute USVString name; 152 | 153 | Promise<boolean> isSameEntry(FileSystemHandle other); 154 | }; 155 | 156 | 157 | A {{FileSystemHandle}} object represents an [=/entry=]. Each {{FileSystemHandle}} object is associated 158 | with an entry (an [=/entry=]). Multiple separate objects implementing 159 | the {{FileSystemHandle}} interface can all be associated with the same [=/entry=] simultaneously. 160 | 161 |
162 | {{FileSystemHandle}} objects are [=serializable objects=]. 163 | 164 | Their [=serialization steps=], given |value|, |serialized| and forStorage are: 165 | 166 | 1. Set |serialized|.\[[Origin]] to |value|'s [=relevant settings object=]'s [=environment settings object/origin=]. 167 | 1. Set |serialized|.\[[Entry]] to |value|'s [=FileSystemHandle/entry=]. 168 | 169 |
170 | 171 |
172 | Their [=deserialization steps=], given |serialized| and |value| are: 173 | 174 | 1. If |serialized|.\[[Origin]] is not [=same origin=] with 175 | |value|'s [=relevant settings object=]'s [=environment settings object/origin=], 176 | then throw a {{DataCloneError}}. 177 | 1. Set |value|'s [=FileSystemHandle/entry=] to |serialized|.\[[Entry]] 178 | 179 |
180 | 181 |
182 | : |handle| . {{FileSystemHandle/kind}} 183 | :: Returns {{FileSystemHandleKind/"file"}} if |handle| is a {{FileSystemFileHandle}}, 184 | or {{FileSystemHandleKind/"directory"}} if |handle| is a {{FileSystemDirectoryHandle}}. 185 | 186 | This can be used to distinguish files from directories when iterating over the contents 187 | of a directory. 188 | 189 | : |handle| . {{FileSystemHandle/name}} 190 | :: Returns the [=entry/name=] of the entry represented by |handle|. 191 |
192 | 193 | The kind attribute must 194 | return {{FileSystemHandleKind/"file"}} if the associated [=FileSystemHandle/entry=] is a [=file entry=], 195 | and return {{FileSystemHandleKind/"directory"}} otherwise. 196 | 197 | The name attribute must return the [=entry/name=] of the 198 | associated [=FileSystemHandle/entry=]. 199 | 200 | ### The {{FileSystemHandle/isSameEntry()}} method ### {#api-filesystemhandle-issameentry} 201 | 202 |
203 | : same = await |handle1| . {{FileSystemHandle/isSameEntry()|isSameEntry}}( |handle2| ) 204 | :: Returns true if |handle1| and |handle2| represent the same file or directory. 205 |
206 | 207 |
208 | The isSameEntry(|other|) method, when invoked, must run these steps: 209 | 210 | 1. Let |realm| be [=this=]'s [=relevant Realm=]. 211 | 1. Let |p| be [=a new promise=] in |realm|. 212 | 1. Run the following steps [=in parallel=]: 213 | 1. If [=this=]'s [=FileSystemHandle/entry=] is [=the same as=] |other|'s [=FileSystemHandle/entry=], 214 | [=/resolve=] |p| with `true`. 215 | 1. Else [=/resolve=] |p| with `false`. 216 | 1. Return |p|. 217 | 218 |
219 | 220 | ## The {{FileSystemFileHandle}} interface ## {#api-filesystemfilehandle} 221 | 222 | 223 | dictionary FileSystemCreateWritableOptions { 224 | boolean keepExistingData = false; 225 | }; 226 | 227 | [Exposed=(Window,Worker), SecureContext, Serializable] 228 | interface FileSystemFileHandle : FileSystemHandle { 229 | Promise<File> getFile(); 230 | Promise<FileSystemWritableFileStream> createWritable(optional FileSystemCreateWritableOptions options = {}); 231 | }; 232 | 233 | 234 | A {{FileSystemFileHandle}}'s associated [=FileSystemHandle/entry=] must be a [=file entry=]. 235 | 236 | {{FileSystemFileHandle}} objects are [=serializable objects=]. Their [=serialization steps=] and 237 | [=deserialization steps=] are the same as those for {{FileSystemHandle}}. 238 | 239 | ### The {{FileSystemFileHandle/getFile()}} method ### {#api-filesystemfilehandle-getfile} 240 | 241 |
242 | : file = await |fileHandle| . {{FileSystemFileHandle/getFile()}} 243 | :: Returns a {{File}} representing the state on disk of the entry represented by |handle|. 244 | If the file on disk changes or is removed after this method is called, the returned 245 | {{File}} object will likely be no longer readable. 246 |
247 | 248 |
249 | The getFile() method, when invoked, must run these steps: 250 | 251 | 1. Let |result| be [=a new promise=]. 252 | 1. Run the following steps [=in parallel=]: 253 | 1. Let |access| be the result of running [=this=]'s [=FileSystemHandle/entry=]'s 254 | [=entry/query access=] given "`read`". 255 | 1. If |access| is not "{{PermissionState/granted}}", 256 | reject |result| with a {{NotAllowedError}} and abort. 257 | 1. Let |entry| be [=this=]'s [=FileSystemHandle/entry=]. 258 | 1. Let |f| be a new {{File}}. 259 | 1. Set |f|'s snapshot state to the current state of |entry|. 260 | 1. Set |f|'s underlying byte sequence to a copy of |entry|'s [=binary data=]. 261 | 1. Initialize the value of |f|'s {{File/name}} attribute to |entry|'s [=entry/name=]. 262 | 1. Initialize the value of |f|'s {{File/lastModified}} attribute to |entry|'s [=file entry/modification timestamp=]. 263 | 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. 264 | 265 | Issue: The reading and snapshotting behavior needs to be better specified in the [[FILE-API]] spec, 266 | for now this is kind of hand-wavy. 267 | 1. [=/Resolve=] |result| with |f|. 268 | 1. Return |result|. 269 | 270 |
271 | 272 | ### The {{FileSystemFileHandle/createWritable()}} method ### {#api-filesystemfilehandle-createwritable} 273 | 274 |
275 | : |stream| = await |fileHandle| . {{FileSystemFileHandle/createWritable()}} 276 | : |stream| = await |fileHandle| . {{FileSystemFileHandle/createWritable()|createWritable}}({ {{FileSystemCreateWritableOptions/keepExistingData}}: true/false }) 277 | :: Returns a {{FileSystemWritableFileStream}} that can be used to write to the file. Any changes made through 278 | |stream| won't be reflected in the file represented by |fileHandle| until the stream has been closed. 279 | User agents try to ensure that no partial writes happen, i.e. the file represented by 280 | |fileHandle| will either contain its old contents or it will contain whatever data was written 281 | through |stream| up until the stream has been closed. 282 | 283 | This is typically implemented by writing data to a temporary file, and only replacing the file 284 | represented by |fileHandle| with the temporary file when the writable filestream is closed. 285 | 286 | If {{FileSystemCreateWritableOptions/keepExistingData}} is `false` or not specified, 287 | the temporary file starts out empty, 288 | otherwise the existing file is first copied to this temporary file. 289 |
290 | 291 | Issue(67): There has been some discussion around and desire for a "inPlace" mode for createWritable 292 | (where changes will be written to the actual underlying file as they are written to the writer, for 293 | example to support in-place modification of large files or things like databases). This is not 294 | currently implemented in Chrome. Implementing this is currently blocked on figuring out how to 295 | combine the desire to run malware checks with the desire to let websites make fast in-place 296 | modifications to existing large files. 297 | 298 |
299 | The createWritable(|options|) method, when invoked, must run these steps: 300 | 301 | 1. Let |result| be [=a new promise=]. 302 | 1. Run the following steps [=in parallel=]: 303 | 1. Let |access| be the result of running [=this=]'s [=FileSystemHandle/entry=]'s 304 | [=entry/request access=] given "`readwrite`". 305 | If that throws an exception, [=reject=] |result| with that exception and abort. 306 | 1. If |access| is not "{{PermissionState/granted}}", 307 | reject |result| with a {{NotAllowedError}} and abort. 308 | 1. Let |entry| be [=this=]'s [=FileSystemHandle/entry=]. 309 | 1. Let |stream| be the result of [=create a new FileSystemWritableFileStream|creating a new FileSystemWritableFileStream=] 310 | for |entry| in [=this=]'s [=relevant realm=]. 311 | 1. If |options|.{{FileSystemCreateWritableOptions/keepExistingData}} is `true`: 312 | 1. Set |stream|.[=[[buffer]]=] to a copy of |entry|'s [=file entry/binary data=]. 313 | 1. [=/Resolve=] |result| with |stream|. 314 | 1. Return |result|. 315 | 316 |
317 | 318 | ## The {{FileSystemDirectoryHandle}} interface ## {#api-filesystemdirectoryhandle} 319 | 320 | 321 | dictionary FileSystemGetFileOptions { 322 | boolean create = false; 323 | }; 324 | 325 | dictionary FileSystemGetDirectoryOptions { 326 | boolean create = false; 327 | }; 328 | 329 | dictionary FileSystemRemoveOptions { 330 | boolean recursive = false; 331 | }; 332 | 333 | [Exposed=(Window,Worker), SecureContext, Serializable] 334 | interface FileSystemDirectoryHandle : FileSystemHandle { 335 | async iterable<USVString, FileSystemHandle>; 336 | 337 | Promise<FileSystemFileHandle> getFileHandle(USVString name, optional FileSystemGetFileOptions options = {}); 338 | Promise<FileSystemDirectoryHandle> getDirectoryHandle(USVString name, optional FileSystemGetDirectoryOptions options = {}); 339 | 340 | Promise<undefined> removeEntry(USVString name, optional FileSystemRemoveOptions options = {}); 341 | 342 | Promise<sequence<USVString>?> resolve(FileSystemHandle possibleDescendant); 343 | }; 344 | 345 | 346 | A {{FileSystemDirectoryHandle}}'s associated [=FileSystemHandle/entry=] must be a [=directory entry=]. 347 | 348 | {{FileSystemDirectoryHandle}} objects are [=serializable objects=]. Their [=serialization steps=] and 349 | [=deserialization steps=] are the same as those for {{FileSystemHandle}}. 350 | 351 | ### Directory iteration ### {#api-filesystemdirectoryhandle-asynciterable} 352 | 353 |
354 | : for await (let [|name|, |handle|] of |directoryHandle|) {} 355 | : for await (let [|name|, |handle|] of |directoryHandle| . entries()) {} 356 | : for await (let |handle| of |directoryHandle| . values()) {} 357 | : for await (let |name| of |directoryHandle| . keys()) {} 358 | :: Iterates over all entries whose parent is the entry represented by |directoryHandle|. Entries 359 | that are created or deleted while the iteration is in progress might or might not be included. 360 | No guarantees are given either way. 361 |
362 | 363 | Issue(173): In the future we might want to add arguments to the async iterable declaration to 364 | support for example recursive iteration. 365 | 366 |
367 | The [=asynchronous iterator initialization steps=] for a {{FileSystemDirectoryHandle}} |handle| 368 | ant its async iterator |iterator| are: 369 | 370 | 1. Let |access| be the result of running |handle|'s [=FileSystemHandle/entry=]'s 371 | [=entry/query access=] given "`read`". 372 | 373 | 1. If |access| is not "{{PermissionState/granted}}", 374 | throw a {{NotAllowedError}}. 375 | 376 | 1. Set |iterator|'s past results to an empty [=/set=]. 377 | 378 |
379 | 380 |
381 | To [=get the next iteration result=] for a {{FileSystemDirectoryHandle}} |handle| 382 | and its async iterator |iterator|: 383 | 384 | 1. Let |promise| be [=a new promise=]. 385 | 386 | 1. Let |directory| be |handle|'s [=FileSystemHandle/entry=]. 387 | 388 | 1. Let |access| be the result of running |handle|'s [=FileSystemHandle/entry=]'s 389 | [=entry/query access=] given "`read`". 390 | 391 | 1. If |access| is not "{{PermissionState/granted}}", 392 | reject |promise| with a {{NotAllowedError}} and return |promise|. 393 | 394 | 1. Let |child| be an [=/entry=] in |directory|'s [=directory entry/children=], 395 | such that |child|'s [=entry/name=] is not contained in |iterator|'s [=past results=], 396 | or `null` if no such entry exists. 397 | 398 | Note: This is intentionally very vague about the iteration order. Different platforms 399 | and file systems provide different guarantees about iteration order, and we want it to 400 | be possible to efficiently implement this on all platforms. As such no guarantees are given 401 | about the exact order in which elements are returned. 402 | 403 | 1. If |child| is `null`, then: 404 | 1. [=/Resolve=] |promise| with `undefined`. 405 | 406 | 1. Otherwise: 407 | 1. [=set/Append=] |child|'s [=entry/name=] to |iterator|'s [=past results=]. 408 | 1. If |child| is a [=file entry=]: 409 | 1. Let |result| be a new {{FileSystemFileHandle}} associated with |child|. 410 | 1. Otherwise: 411 | 1. Let |result| be a new {{FileSystemDirectoryHandle}} associated with |child|. 412 | 1. [=/Resolve=] |promise| with (|child|'s [=entry/name=], |result|). 413 | 414 | 1. Return |promise|. 415 | 416 |
417 | 418 | ### The {{FileSystemDirectoryHandle/getFileHandle()}} method ### {#api-filesystemdirectoryhandle-getfilehandle} 419 | 420 |
421 | : |fileHandle| = await |directoryHandle| . {{FileSystemDirectoryHandle/getFileHandle()|getFileHandle}}(|name|) 422 | : |fileHandle| = await |directoryHandle| . {{FileSystemDirectoryHandle/getFileHandle()|getFileHandle}}(|name|, { {{FileSystemGetFileOptions/create}}: false }) 423 | :: Returns a handle for a file named |name| in the directory represented by |directoryHandle|. If 424 | no such file exists, this rejects. 425 | 426 | : |fileHandle| = await |directoryHandle| . {{FileSystemDirectoryHandle/getFileHandle()|getFileHandle}}(|name|, { {{FileSystemGetFileOptions/create}}: true }) 427 | :: Returns a handle for a file named |name| in the directory represented by |directoryHandle|. If 428 | no such file exists, this creates a new file. If no file with named |name| can be created this 429 | rejects. Creation can fail because there already is a directory with the same name, because the 430 | name uses characters that aren't supported in file names on the underlying file system, or 431 | because the user agent for security reasons decided not to allow creation of the file. 432 | 433 | This operation requires write permission, even if the file being returned already exists. If 434 | this handle doesn't already have write permission, this could result in a prompt being shown to 435 | the user. To get an existing file without needing write permission, call this method 436 | with { {{FileSystemGetFileOptions/create}}: false }. 437 |
438 | 439 |
440 | The getFileHandle(|name|, |options|) method, when invoked, 441 | must run these steps: 442 | 443 | 1. Let |result| be [=a new promise=]. 444 | 1. Run the following steps [=in parallel=]: 445 | 1. If |name| is not a [=valid file name=], [=/reject=] |result| with a {{TypeError}} and abort. 446 | 447 | 1. Let |entry| be [=this=]'s [=FileSystemHandle/entry=]. 448 | 1. If |options|.{{FileSystemGetFileOptions/create}} is `true`: 449 | 1. Let |access| be the result of running [=this=]'s [=FileSystemHandle/entry=]'s 450 | [=entry/request access=] given "`readwrite`". 451 | If that throws an exception, [=reject=] |result| with that exception and abort. 452 | 1. Otherwise: 453 | 1. Let |access| be the result of running [=this=]'s [=FileSystemHandle/entry=]'s 454 | [=entry/query access=] given "`read`". 455 | 1. If |access| is not "{{PermissionState/granted}}", 456 | reject |result| with a {{NotAllowedError}} and abort. 457 | 458 | 1. [=set/For each=] |child| of |entry|'s [=directory entry/children=]: 459 | 1. If |child|'s [=entry/name=] equals |name|: 460 | 1. If |child| is a [=directory entry=]: 461 | 1. [=/Reject=] |result| with a {{TypeMismatchError}} and abort. 462 | 1. [=/Resolve=] |result| with a new {{FileSystemFileHandle}} whose [=FileSystemHandle/entry=] is |child| and abort. 463 | 1. If |options|.{{FileSystemGetFileOptions/create}} is `false`: 464 | 1. [=/Reject=] |result| with a {{NotFoundError}} and abort. 465 | 1. Let |child| be a new [=file entry=] whose [=query access=] and [=request access=] algorithms 466 | are those of |entry|. 467 | 1. Set |child|'s [=entry/name=] to |name|. 468 | 1. Set |child|'s [=binary data=] to an empty [=byte sequence=]. 469 | 1. Set |child|'s [=modification timestamp=] to the current time. 470 | 1. [=set/Append=] |child| to |entry|'s [=directory entry/children=]. 471 | 1. If creating |child| in the underlying file system throws an exception, 472 | [=/reject=] |result| with that exception and abort. 473 | 474 | Issue(68): Better specify what possible exceptions this could throw. 475 | 1. [=/Resolve=] |result| with a new {{FileSystemFileHandle}} whose [=FileSystemHandle/entry=] is |child|. 476 | 1. Return |result|. 477 | 478 |
479 | 480 | ### The {{FileSystemDirectoryHandle/getDirectoryHandle()}} method ### {#api-filesystemdirectoryhandle-getdirectoryhandle} 481 | 482 |
483 | : |subdirHandle| = await |directoryHandle| . {{FileSystemDirectoryHandle/getDirectoryHandle()|getDirectoryHandle}}(|name|) 484 | : |subdirHandle| = await |directoryHandle| . {{FileSystemDirectoryHandle/getDirectoryHandle()|getDirectoryHandle}}(|name|, { {{FileSystemGetDirectoryOptions/create}}: false }) 485 | :: Returns a handle for a directory named |name| in the directory represented by 486 | |directoryHandle|. If no such directory exists, this rejects. 487 | 488 | : |subdirHandle| = await |directoryHandle| . {{FileSystemDirectoryHandle/getDirectoryHandle()|getDirectoryHandle}}(|name|, { {{FileSystemGetDirectoryOptions/create}}: true }) 489 | :: Returns a handle for a directory named |name| in the directory represented by 490 | |directoryHandle|. If no such directory exists, this creates a new directory. If creating the 491 | directory failed, this rejects. Creation can fail because there already is a file with the same 492 | name, or because the name uses characters that aren't supported in file names on the underlying 493 | file system. 494 | 495 | This operation requires write permission, even if the directory being returned already exists. 496 | If this handle doesn't already have write permission, this could result in a prompt being shown 497 | to the user. To get an existing directory without needing write permission, call this method 498 | with { {{FileSystemGetDirectoryOptions/create}}: false }. 499 |
500 | 501 |
502 | The getDirectoryHandle(|name|, |options|) method, when 503 | invoked, must run these steps: 504 | 505 | 1. Let |result| be [=a new promise=]. 506 | 1. Run the following steps [=in parallel=]: 507 | 1. If |name| is not a [=valid file name=], [=/reject=] |result| with a {{TypeError}} and abort. 508 | 509 | 1. Let |entry| be [=this=]'s [=FileSystemHandle/entry=]. 510 | 1. If |options|.{{FileSystemGetDirectoryOptions/create}} is `true`: 511 | 1. Let |access| be the result of running [=this=]'s [=FileSystemHandle/entry=]'s 512 | [=entry/request access=] given "`readwrite`". 513 | If that throws an exception, [=reject=] |result| with that exception and abort. 514 | 1. Otherwise: 515 | 1. Let |access| be the result of running [=this=]'s [=FileSystemHandle/entry=]'s 516 | [=entry/query access=] given "`read`". 517 | 1. If |access| is not "{{PermissionState/granted}}", 518 | reject |result| with a {{NotAllowedError}} and abort. 519 | 520 | 1. [=set/For each=] |child| of |entry|'s [=directory entry/children=]: 521 | 1. If |child|'s [=entry/name=] equals |name|: 522 | 1. If |child| is a [=file entry=]: 523 | 1. [=/Reject=] |result| with a {{TypeMismatchError}} and abort. 524 | 1. [=/Resolve=] |result| with a new {{FileSystemDirectoryHandle}} whose [=FileSystemHandle/entry=] is |child| and abort. 525 | 1. If |options|.{{FileSystemGetFileOptions/create}} is `false`: 526 | 1. [=/Reject=] |result| with a {{NotFoundError}} and abort. 527 | 1. Let |child| be a new [=directory entry=] whose [=query access=] and [=request access=] 528 | algorithms are those of |entry|. 529 | 1. Set |child|'s [=entry/name=] to |name|. 530 | 1. Set |child|'s [=directory entry/children=] to an empty [=/set=]. 531 | 1. [=set/Append=] |child| to |entry|'s [=directory entry/children=]. 532 | 1. If creating |child| in the underlying file system throws an exception, 533 | [=/reject=] |result| with that exception and abort. 534 | 535 | Issue(68): Better specify what possible exceptions this could throw. 536 | 1. [=/Resolve=] |result| with a new {{FileSystemDirectoryHandle}} whose [=FileSystemHandle/entry=] is |child|. 537 | 1. Return |result|. 538 | 539 |
540 | 541 | ### The {{FileSystemDirectoryHandle/removeEntry()}} method ### {#api-filesystemdirectoryhandle-removeentry} 542 | 543 |
544 | : await |directoryHandle| . {{FileSystemDirectoryHandle/removeEntry()|removeEntry}}(|name|) 545 | : await |directoryHandle| . {{FileSystemDirectoryHandle/removeEntry()|removeEntry}}(|name|, { {{FileSystemRemoveOptions/recursive}}: false }) 546 | :: If the directory represented by |directoryHandle| contains a file named |name|, or an empty 547 | directory named |name|, this will attempt to delete that file or directory. 548 | 549 | Attempting to delete a file or directory that does not exist is considered success, 550 | while attempting to delete a non-empty directory will result in a promise rejection. 551 | 552 | : await |directoryHandle| . {{FileSystemDirectoryHandle/removeEntry()|removeEntry}}(|name|, { {{FileSystemRemoveOptions/recursive}}: true }) 553 | :: Removes the entry named |name| in the directory represented by |directoryHandle|. 554 | If that entry is a directory, its contents will also be deleted recursively. 555 | recursively. 556 | 557 | Attempting to delete a file or directory that does not exist is considered success. 558 |
559 | 560 |
561 | The removeEntry(|name|, |options|) method, when invoked, must run 562 | these steps: 563 | 564 | 1. Let |result| be [=a new promise=]. 565 | 1. Run the following steps [=in parallel=]: 566 | 1. If |name| is not a [=valid file name=], [=/reject=] |result| with a {{TypeError}} and abort. 567 | 568 | 1. Let |entry| be [=this=]'s [=FileSystemHandle/entry=]. 569 | 1. Let |access| be the result of running [=this=]'s [=FileSystemHandle/entry=]'s 570 | [=entry/request access=] given "`readwrite`". 571 | If that throws an exception, [=reject=] |result| with that exception and abort. 572 | 1. If |access| is not "{{PermissionState/granted}}", 573 | reject |result| with a {{NotAllowedError}} and abort. 574 | 575 | 1. [=set/For each=] |child| of |entry|'s [=directory entry/children=]: 576 | 1. If |child|'s [=entry/name=] equals |name|: 577 | 1. If |child| is a [=directory entry=]: 578 | 1. If |child|'s [=directory entry/children=] is not [=set/is empty|empty=] and |options|.{{FileSystemRemoveOptions/recursive}} is `false`: 579 | 1. [=/Reject=] |result| with an {{InvalidModificationError}} and abort. 580 | 1. [=set/Remove=] |child| from |entry|'s [=directory entry/children=]. 581 | 1. If removing |child| in the underlying file system throws an exception, 582 | [=/reject=] |result| with that exception and abort. 583 | 584 | Note: If {{FileSystemRemoveOptions/recursive}} is `true`, the removal can fail 585 | non-atomically. Some files or directories might have been removed while other files 586 | or directories still exist. 587 | 588 | Issue(68): Better specify what possible exceptions this could throw. 589 | 1. [=/Resolve=] |result| with `undefined`. 590 | 1. [=/Reject=] |result| with a {{NotFoundError}}. 591 | 1. Return |result|. 592 | 593 | 594 |
595 | 596 | ### The {{FileSystemDirectoryHandle/resolve()}} method ### {#api-filesystemdirectoryhandle-resolve} 597 | 598 |
599 | : |path| = await |directory| . {{FileSystemDirectoryHandle/resolve()|resolve}}( |child| ) 600 | :: If |child| is equal to |directory|, |path| will be an empty array. 601 | :: If |child| is a direct child of |directory|, |path| will be an array containing |child|'s name. 602 | :: If |child| is a descendant of |directory|, |path| will be an array containing the names of 603 | all the intermediate directories and |child|'s name as last element. 604 | For example if |directory| represents `/home/user/project` 605 | and |child| represents `/home/user/project/foo/bar`, this will return 606 | `['foo', 'bar']`. 607 | :: Otherwise (|directory| and |child| are not related), |path| will be null. 608 | 609 | This functionality can be useful if a web application shows a directory 610 | listing to highlight a file opened through a file picker in that directory listing. 611 |
612 | 613 |
614 | 615 | // Assume we at some point got a valid directory handle. 616 | const dir_ref = current_project_dir; 617 | if (!dir_ref) return; 618 | 619 | // Now get a file reference by showing a file picker: 620 | const file_ref = await self.showOpenFilePicker(); 621 | if (!file_ref) { 622 | // User cancelled, or otherwise failed to open a file. 623 | return; 624 | } 625 | 626 | // Check if file_ref exists inside dir_ref: 627 | const relative_path = await dir_ref.resolve(file_ref); 628 | if (relative_path === null) { 629 | // Not inside dir_ref 630 | } else { 631 | // relative_path is an array of names, giving the relative path 632 | // from dir_ref to the file that is represented by file_ref: 633 | assert relative_path.pop() === file_ref.name; 634 | 635 | let entry = dir_ref; 636 | for (const name of relative_path) { 637 | entry = await entry.getDirectory(name); 638 | } 639 | entry = await entry.getFile(file_ref.name); 640 | 641 | // Now |entry| will represent the same file on disk as |file_ref|. 642 | assert await entry.isSameEntry(file_ref) === true; 643 | } 644 | 645 |
646 | 647 |
648 | The resolve(|possibleDescendant|) method, 649 | when invoked, must return the result of [=entry/resolving=] 650 | |possibleDescendant|'s [=FileSystemHandle/entry=] relative to [=this=]'s [=FileSystemHandle/entry=]. 651 | 652 |
653 | 654 | 655 | 656 | ## The {{FileSystemWritableFileStream}} interface ## {#api-filesystemwritablefilestream} 657 | 658 | 659 | enum WriteCommandType { 660 | "write", 661 | "seek", 662 | "truncate", 663 | }; 664 | 665 | dictionary WriteParams { 666 | required WriteCommandType type; 667 | unsigned long long? size; 668 | unsigned long long? position; 669 | (BufferSource or Blob or USVString)? data; 670 | }; 671 | 672 | typedef (BufferSource or Blob or USVString or WriteParams) FileSystemWriteChunkType; 673 | 674 | [Exposed=(Window,Worker), SecureContext] 675 | interface FileSystemWritableFileStream : WritableStream { 676 | Promise<undefined> write(FileSystemWriteChunkType data); 677 | Promise<undefined> seek(unsigned long long position); 678 | Promise<undefined> truncate(unsigned long long size); 679 | }; 680 | 681 | 682 | A {{FileSystemWritableFileStream}} has an associated \[[file]] (a [=file entry=]). 683 | 684 | A {{FileSystemWritableFileStream}} has an associated \[[buffer]] (a [=byte sequence=]). 685 | It is initially empty. 686 | 687 | Note: This buffer can get arbitrarily large, so it is expected that implementations will not keep this in memory, 688 | but instead use a temporary file for this. All access to \[[buffer]] is done in promise returning methods and 689 | algorithms, so even though operations on it seem sync, implementations can implement them async. 690 | 691 | A {{FileSystemWritableFileStream}} has an associated \[[seekOffset]] (a number). 692 | It is initially 0. 693 | 694 |
695 | A {{FileSystemWritableFileStream}} object is a {{WritableStream}} object with additional 696 | convenience methods, which operates on a single file on disk. 697 | 698 | Upon creation, an underlying sink will have been created and the stream will be usable. 699 | All operations executed on the stream are queuable and producers will be able to respond to backpressure. 700 | 701 | The underlying sink's write method, and therefore {{WritableStreamDefaultWriter/write()|WritableStreamDefaultWriter's write()}} 702 | method, will accept byte-like data or {{WriteParams}} as input. 703 | 704 | The {{FileSystemWritableFileStream}} has a file position cursor initialized at byte offset 0 from the top of the file. 705 | When using {{FileSystemWritableFileStream/write()|write()}} or by using WritableStream capabilities through the {{WritableStreamDefaultWriter/write()|WritableStreamDefaultWriter's write()}} method, this position will be advanced based on the number of bytes written through the stream object. 706 | 707 | Similarly, when piping a {{ReadableStream}} into a {{FileSystemWritableFileStream}} object, this position is updated with the number of bytes that passed through the stream. 708 | 709 | {{WritableStream/getWriter()|getWriter()}} returns an instance of {{WritableStreamDefaultWriter}}. 710 |
711 | 712 |
713 | To create a new FileSystemWritableFileStream given a [=file entry=] |file| 714 | in a [=/Realm=] |realm|, perform the following steps: 715 | 716 | 1. Let |stream| be a [=new=] {{FileSystemWritableFileStream}} in |realm|. 717 | 1. Set |stream|.[=FileSystemWritableFileStream/[[file]]=] to |file|. 718 | 1. Let |writeAlgorithm| be an algorithm which takes a |chunk| argument 719 | and returns the result of running the [=write a chunk=] algorithm with |stream| and |chunk|. 720 | 1. Let |closeAlgorithm| be the following steps: 721 | 1. Let |closeResult| be [=a new promise=]. 722 | 1. Run the following steps [=in parallel=]: 723 | 1. Let |access| be the result of running |file|'s [=entry/query access=] given "`readwrite`". 724 | 1. If |access| is not "{{PermissionState/granted}}", 725 | reject |closeResult| with a {{NotAllowedError}} and abort. 726 | 1. Perform [=implementation-defined=] malware scans and safe browsing checks. 727 | If these checks fail, [=/reject=] |closeResult| with an {{AbortError}} and abort. 728 | 1. Set |stream|.[=[[file]]=]'s [=file entry/binary data=] to |stream|.[=[[buffer]]=]. 729 | If that throws an exception, [=/reject=] |closeResult| with that exception and abort. 730 | 731 | Note: It is expected that this atomically updates the contents of the file on disk 732 | being written to. 733 | 1. [=/Resolve=] |closeResult| with `undefined`. 734 | 1. Return |closeResult|. 735 | 1. Let |highWaterMark| be 1. 736 | 1. Let |sizeAlgorithm| be an algorithm that returns `1`. 737 | 1. [=WritableStream/Set up=] |stream| with writeAlgorithm set to |writeAlgorithm|, closeAlgorithm set to |closeAlgorithm|, highWaterMark set to |highWaterMark|, and sizeAlgorithm set to |sizeAlgorithm|. 742 | 1. Return |stream|. 743 | 744 |
745 | 746 |
747 | The write a chunk algorithm, 748 | given a {{FileSystemWritableFileStream}} |stream| and |chunk|, 749 | runs these steps: 750 | 751 | 1. Let |input| be the result of [=converting=] |chunk| to a {{FileSystemWriteChunkType}}. 752 | If this throws an exception, then return [=a promise rejected with=] that exception. 753 | 1. Let |p| be [=a new promise=]. 754 | 1. Run the following steps [=in parallel=]: 755 | 1. Let |access| be the result of running |stream|'s [=FileSystemWritableFileStream/[[file]]=]'s 756 | [=entry/query access=] given "`readwrite`". 757 | 1. If |access| is not "{{PermissionState/granted}}", 758 | reject |p| with a {{NotAllowedError}} and abort. 759 | 1. Let |command| be |input|.{{WriteParams/type}} if |input| is a {{WriteParams}}, 760 | and {{WriteCommandType/"write"}} otherwise. 761 | 1. If |command| is {{WriteCommandType/"write"}}: 762 | 1. Let |data| be |input|.{{WriteParams/data}} if |input| is a {{WriteParams}}, 763 | and |input| otherwise. 764 | 1. If |data| is `undefined`, 765 | reject |p| with a {{TypeError}} and abort. 766 | 1. Let |writePosition| be |stream|.[=[[seekOffset]]=]. 767 | 1. If |input| is a {{WriteParams}} and |input|.{{WriteParams/position}} is not `undefined`, 768 | set |writePosition| to |input|.{{WriteParams/position}}. 769 | 1. Let |oldSize| be |stream|.[=[[buffer]]=]'s [=byte sequence/length=]. 770 | 1. If |data| is a {{BufferSource}}, 771 | let |dataBytes| be [=get a copy of the buffer source|a copy of=] |data|. 772 | 1. Else if |data| is a {{Blob}}: 773 | 1. Let |dataBytes| be the result of performing the 774 | read operation on |data|. 775 | If this throws an exception, [=/reject=] |p| with that exception and abort. 776 | 1. Else: 777 | 1. [=Assert=]: |data| is a {{USVString}}. 778 | 1. Let |dataBytes| be the result of [=UTF-8 encoding=] |data|. 779 | 1. If |writePosition| is larger than |oldSize|, 780 | append |writePosition| - |oldSize| `0x00` (NUL) bytes to the end of |stream|.[=[[buffer]]=]. 781 | 782 | Note: Implementations are expected to behave as if the skipped over file contents 783 | are indeed filled with NUL bytes. That doesn't mean these bytes have to actually be 784 | written to disk and take up disk space. Instead most file systems support so called 785 | sparse files, where these NUL bytes don't take up actual disk space. 786 | 787 | 1. Let |head| be a [=byte sequence=] containing the first |writePosition| bytes of |stream|.[=[[buffer]]=]. 788 | 1. Let |tail| be an empty [=byte sequence=]. 789 | 1. If |writePosition| + |data|.[=byte sequence/length=] is smaller than |oldSize|: 790 | 1. Let |tail| be a [=byte sequence=] containing the last 791 | |oldSize| - (|writePosition| + |data|.[=byte sequence/length=]) bytes of |stream|.[=[[buffer]]=]. 792 | 1. Set |stream|.[=[[buffer]]=] to the concatenation of |head|, |data| and |tail|. 793 | 1. If the operations modifying |stream|.[=[[buffer]]=] in the previous steps failed 794 | due to exceeding the [=storage quota=], [=/reject=] |p| with a {{QuotaExceededError}} and abort, 795 | leaving |stream|.[=[[buffer]]=] unmodified. 796 | 797 | Note: [=Storage quota=] only applies to files stored in the [=origin private file system=]. 798 | However this operation could still fail for other files, for example if the disk being written 799 | to runs out of disk space. 800 | 1. Set |stream|.[=[[seekOffset]]=] to |writePosition| + |data|.[=byte sequence/length=]. 801 | 1. [=/Resolve=] |p|. 802 | 1. Else if |command| is {{WriteCommandType/"seek"}}: 803 | 1. If |chunk|.{{WriteParams/position}} is `undefined`, 804 | [=/reject=] |p| with a {{TypeError}} and abort. 805 | 1. Set |stream|.[=[[seekOffset]]=] to |chunk|.{{WriteParams/position}}. 806 | 1. [=/Resolve=] |p|. 807 | 1. Else if |command| is {{WriteCommandType/"truncate"}}: 808 | 1. If |chunk|.{{WriteParams/size}} is `undefined`, 809 | [=/reject=] |p| with a {{TypeError}} and abort. 810 | 1. Let |newSize| be |chunk|.{{WriteParams/size}}. 811 | 1. Let |oldSize| be |stream|.[=[[buffer]]=]'s [=byte sequence/length=]. 812 | 1. If |newSize| is larger than |oldSize|: 813 | 1. Set |stream|.[=[[buffer]]=] to a [=byte sequence=] formed by concating 814 | |stream|.[=[[buffer]]=] with a [=byte sequence=] containing |newSize|-|oldSize| `0x00` bytes. 815 | 1. If the operation in the previous step failed due to exceeding the [=storage quota=], 816 | [=/reject=] |p| with a {{QuotaExceededError}} and abort, 817 | leaving |stream|.[=[[buffer]]=] unmodified. 818 | 819 | Note: [=Storage quota=] only applies to files stored in the [=origin private file system=]. 820 | However this operation could still fail for other files, for example if the disk being written 821 | to runs out of disk space. 822 | 1. Else if |newSize| is smaller than |oldSize|: 823 | 1. Set |stream|.[=[[buffer]]=] to a [=byte sequence=] containing the first |newSize| bytes 824 | in |stream|.[=[[buffer]]=]. 825 | 1. If |stream|.[=[[seekOffset]]=] is bigger than |newSize|, 826 | set |stream|.[=[[seekOffset]]=] to |newSize|. 827 | 1. [=/Resolve=] |p|. 828 | 1. Return |p|. 829 | 830 |
831 | 832 | ### The {{FileSystemWritableFileStream/write()}} method ### {#api-filesystemwritablefilestream-write} 833 | 834 |
835 | : await |stream| . {{FileSystemWritableFileStream/write()|write}}(|data|) 836 | : await |stream| . {{FileSystemWritableFileStream/write()|write}}({ 837 | {{WriteParams/type}}: {{WriteCommandType/"write"}}, 838 | {{WriteParams/data}}: |data| }) 839 | :: Writes the content of |data| into the file associated with |stream| at the current file 840 | cursor offset. 841 | 842 | No changes are written to the actual file on disk until the stream has been closed. 843 | Changes are typically written to a temporary file instead. 844 | 845 | : await |stream| . {{FileSystemWritableFileStream/write()|write}}({ 846 | {{WriteParams/type}}: {{WriteCommandType/"write"}}, 847 | {{WriteParams/position}}: |position|, 848 | {{WriteParams/data}}: |data| }) 849 | :: Writes the content of |data| into the file associated with |stream| at |position| 850 | bytes from the top of the file. Also updates the current file cursor offset to the 851 | end of the written data. 852 | 853 | No changes are written to the actual file on disk until the stream has been closed. 854 | Changes are typically written to a temporary file instead. 855 | 856 | : await |stream| . {{FileSystemWritableFileStream/write()|write}}({ 857 | {{WriteParams/type}}: {{WriteCommandType/"seek"}}, 858 | {{WriteParams/position}}: |position| }) 859 | :: Updates the current file cursor offset the |position| bytes from the top of the file. 860 | 861 | : await |stream| . {{FileSystemWritableFileStream/write()|write}}({ 862 | {{WriteParams/type}}: {{WriteCommandType/"truncate"}}, 863 | {{WriteParams/size}}: |size| }) 864 | :: Resizes the file associated with |stream| to be |size| bytes long. If |size| is larger than 865 | the current file size this pads the file with null bytes, otherwise it truncates the file. 866 | 867 | The file cursor is updated when {{truncate}} is called. If the offset is smaller than offset, 868 | it remains unchanged. If the offset is larger than |size|, the offset is set to |size| to 869 | ensure that subsequent writes do not error. 870 | 871 | No changes are written to the actual file until on disk until the stream has been closed. 872 | Changes are typically written to a temporary file instead. 873 |
874 | 875 |
876 | The write(|data|) method, when invoked, must run 877 | these steps: 878 | 879 | 1. Let |writer| be the result of [=WritableStream/getting a writer=] for [=this=]. 880 | 1. Let |result| be the result of [=WritableStreamDefaultWriter/writing a chunk=] to |writer| given 881 | |data|. 882 | 1. [=WritableStreamDefaultWriter/Release=] |writer|. 883 | 1. Return |result|. 884 | 885 |
886 | 887 | ### The {{FileSystemWritableFileStream/seek()}} method ### {#api-filesystemwritablefilestream-seek} 888 | 889 |
890 | : await |stream| . {{FileSystemWritableFileStream/seek()|seek}}(|position|) 891 | :: Updates the current file cursor offset the |position| bytes from the top of the file. 892 |
893 | 894 |
895 | The seek(|position|) method, when invoked, must run these 896 | steps: 897 | 898 | 1. Let |writer| be the result of [=WritableStream/getting a writer=] for [=this=]. 899 | 1. Let |result| be the result of [=WritableStreamDefaultWriter/writing a chunk=] to |writer| given 900 | «[ "{{WriteParams/type}}" → {{WriteCommandType/"seek"}}, "{{WriteParams/position}}" → 901 | |position| ]». 902 | 1. [=WritableStreamDefaultWriter/Release=] |writer|. 903 | 1. Return |result|. 904 | 905 |
906 | 907 | ### The {{FileSystemWritableFileStream/truncate()}} method ### {#api-filesystemwritablefilestream-truncate} 908 | 909 |
910 | : await |stream| . {{FileSystemWritableFileStream/truncate()|truncate}}(|size|) 911 | :: Resizes the file associated with |stream| to be |size| bytes long. If |size| is larger than 912 | the current file size this pads the file with null bytes, otherwise it truncates the file. 913 | 914 | The file cursor is updated when {{truncate}} is called. If the offset is smaller than offset, 915 | it remains unchanged. If the offset is larger than |size|, the offset is set to |size| to 916 | ensure that subsequent writes do not error. 917 | 918 | No changes are written to the actual file until on disk until the stream has been closed. 919 | Changes are typically written to a temporary file instead. 920 |
921 | 922 |
923 | The truncate(|size|) method, when invoked, must run these 924 | steps: 925 | 926 | 1. Let |writer| be the result of [=WritableStream/getting a writer=] for [=this=]. 927 | 1. Let |result| be the result of [=WritableStreamDefaultWriter/writing a chunk=] to |writer| given 928 | «[ "{{WriteParams/type}}" → {{WriteCommandType/"truncate"}}, "{{WriteParams/size}}" → 929 | |size| ]». 930 | 1. [=WritableStreamDefaultWriter/Release=] |writer|. 931 | 1. Return |result|. 932 | 933 |
934 | 935 | 936 | # Accessing the Origin Private File System # {#sandboxed-filesystem} 937 | 938 | The origin private file system is a [=storage endpoint=] whose 939 | identifier is `"fileSystem"`, 940 | types are `« "local" »`, 941 | and quota is null. 942 | 943 | Issue: Storage endpoints should be defined in [[storage]] itself, rather 944 | than being defined here. So merge this into the table there. 945 | 946 | Note: While user agents will typically implement this by persisting the contents of this 947 | [=origin private file system=] to disk, it is not intended that the contents are easily 948 | user accessible. Similarly there is no expectation that files or directories with names 949 | matching the names of children of the [=origin private file system=] exist. 950 | 951 | 952 | [SecureContext] 953 | partial interface StorageManager { 954 | Promise<FileSystemDirectoryHandle> getDirectory(); 955 | }; 956 | 957 | 958 |
959 | : |directoryHandle| = await navigator . storage . {{StorageManager/getDirectory()}} 960 | :: Returns the root directory of the origin private file system. 961 |
962 | 963 |
964 | The getDirectory() method, when 965 | invoked, must run these steps: 966 | 967 | 1. Let |environment| be the [=current settings object=]. 968 | 969 | 1. Let |map| be the result of running [=obtain a local storage bottle map=] 970 | with |environment| and `"fileSystem"`. If this returns failure, 971 | return [=a promise rejected with=] a {{SecurityError}}. 972 | 973 | 1. If |map|["root"] does not [=map/exist=]: 974 | 1. Let |dir| be a new [=directory entry=] whose [=query access=] and [=request access=] algorithms 975 | always return "{{PermissionState/granted}}". 976 | 1. Set |dir|'s [=entry/name=] to the empty string. 977 | 1. Set |dir|'s [=directory entry/children=] to an empty [=/set=]. 978 | 1. Set |map|["root"] to |dir|. 979 | 980 | 1. Return [=a promise resolved with=] a new {{FileSystemDirectoryHandle}}, 981 | whose associated [=FileSystemHandle/entry=] is |map|["root"]. 982 | 983 |
984 | 985 | 986 |

Acknowledgments

987 | 988 |

This standard is written by Marijn Kruisselbrink 989 | (Google, mek@chromium.org). 990 | 991 | 992 |

This Living Standard includes material copied from W3C WICG's 993 | File System Access, which is 994 | available under the 995 | W3C Software and Document License. 996 | -------------------------------------------------------------------------------- /review-drafts/2022-09.bs: -------------------------------------------------------------------------------- 1 |

13 | 14 | 17 | 18 |
 19 | urlPrefix: https://tc39.es/ecma262/; spec: ECMA-262
 20 |   type: dfn; text: realm; url: realm
 21 | urlPrefix: https://storage.spec.whatwg.org/; spec: storage
 22 |   type: dfn; text: storage; url: site-storage
 23 | 
24 | 25 | 44 | 45 | 46 | # Introduction # {#introduction} 47 | 48 | *This section is non-normative.* 49 | 50 | This document defines fundamental infrastructure for file system APIs. In addition, it defines an 51 | API that makes it possible for websites to get access to a file system directory without having to 52 | first prompt the user for access. This enables use cases where a website wants to save data to disk 53 | before a user has picked a location to save to, without forcing the website to use a completely 54 | different storage mechanism with a different API for such files. The entry point for this is the 55 | {{StorageManager/getDirectory()|navigator.storage.getDirectory()}} method. 56 | 57 | 58 | # Files and Directories # {#files-and-directories} 59 | 60 | ## Concepts ## {#concepts} 61 | 62 | An entry is either a [=file entry=] or a [=directory entry=]. 63 | 65 | 66 | Each [=/entry=] has an associated query access algorithm, which takes "`read`" 67 | or "`readwrite`" mode and returns a {{PermissionState}}. Unless specified 68 | otherwise it returns "{{PermissionState/denied}}". The algorithm is allowed to throw. 69 | 70 | Each [=/entry=] has an associated request access algorithm, which takes 71 | "`read`" or "`readwrite`" mode and returns a {{PermissionState}}. Unless specified 72 | otherwise it returns "{{PermissionState/denied}}". The algorithm is allowed to throw. 73 | 74 | Note: Implementations that only implement this specification and not dependent specifications do not 75 | need to bother implementing [=/entry=]'s [=entry/query access=] and [=entry/request access=]. 76 | 77 | Each [=/entry=] has an associated name (a [=string=]). 78 | 79 | A valid file name is a [=string=] that is not an empty string, is not equal to "." or "..", 80 | and does not contain '/' or any other character used as path separator on the underlying platform. 81 | 82 | Note: This means that '\' is not allowed in names on Windows, but might be allowed on 83 | other operating systems. Additionally underlying file systems might have further restrictions 84 | on what names are or aren't allowed, so a string merely being a [=valid file name=] is not 85 | a guarantee that creating a file or directory with that name will succeed. 86 | 87 | Issue: We should consider having further normative restrictions on file names that will 88 | never be allowed using this API, rather than leaving it entirely up to underlying file 89 | systems. 90 | 91 | A file entry additionally consists of 92 | binary data (a [=byte sequence=]) and a 93 | modification timestamp (a number representing the number of milliseconds since the Unix Epoch). 94 | 95 | A directory entry additionally consists of a [=/set=] of 96 | children, which are themselves [=/entries=]. Each member is either a [=/file=] or a [=directory=]. 97 | 98 | An [=/entry=] |entry| should be [=list/contained=] in the [=children=] of at most one 99 | [=directory entry=], and that directory entry is also known as |entry|'s parent. 100 | An [=/entry=]'s [=entry/parent=] is null if no such directory entry exists. 101 | 102 | Note: Two different [=/entries=] can represent the same file or directory on disk, in which 103 | case it is possible for both entries to have a different parent, or for one entry to have a 104 | parent while the other entry does not have a parent. 105 | 106 | [=/Entries=] can (but don't have to) be backed by files on the host operating system's local file system, 107 | so it is possible for the [=binary data=], [=modification timestamp=], 108 | and [=children=] of entries to be modified by applications outside of this specification. 109 | Exactly how external changes are reflected in the data structures defined by this specification, 110 | as well as how changes made to the data structures defined here are reflected externally 111 | is left up to individual user-agent implementations. 112 | 113 | An [=/entry=] |a| is the same as an [=/entry=] |b| if |a| is equal to |b|, or 114 | if |a| and |b| are backed by the same file or directory on the local file system. 115 | 116 | Issue: TODO: Explain better how entries map to files on disk (multiple entries can map to the same file or 117 | directory on disk but an entry doesn't have to map to any file on disk). 118 | 119 |
120 | To resolve an [=/entry=] |child| relative to a [=directory entry=] |root|, 121 | run the following steps: 122 | 123 | 1. Let |result| be [=a new promise=]. 124 | 1. Run the following steps [=in parallel=]: 125 | 1. If |child| is [=the same as=] |root|, 126 | [=/resolve=] |result| with an empty list, and abort. 127 | 1. Let |childPromises| be « ». 128 | 1. [=set/For each=] |entry| of |root|'s [=FileSystemHandle/entry=]'s [=children=]: 129 | 1. Let |p| be the result of [=entry/resolving=] |child| relative to |entry|. 130 | 1. [=list/Append=] |p| to |childPromises|. 131 | 1. [=Upon fulfillment=] of |p| with value |path|: 132 | 1. If |path| is not null: 133 | 1. [=list/Prepend=] |entry|'s [=entry/name=] to |path|. 134 | 1. [=/Resolve=] |result| with |path|. 135 | 1. [=Wait for all=] |childPromises|, with the following success steps: 136 | 1. If |result| hasn't been resolved yet, [=/resolve=] |result| with `null`. 137 | 1. Return |result|. 138 | 139 |
140 | 141 | ## The {{FileSystemHandle}} interface ## {#api-filesystemhandle} 142 | 143 | 144 | enum FileSystemHandleKind { 145 | "file", 146 | "directory", 147 | }; 148 | 149 | [Exposed=(Window,Worker), SecureContext, Serializable] 150 | interface FileSystemHandle { 151 | readonly attribute FileSystemHandleKind kind; 152 | readonly attribute USVString name; 153 | 154 | Promise<boolean> isSameEntry(FileSystemHandle other); 155 | }; 156 | 157 | 158 | A {{FileSystemHandle}} object represents an [=/entry=]. Each {{FileSystemHandle}} object is associated 159 | with an entry (an [=/entry=]). Multiple separate objects implementing 160 | the {{FileSystemHandle}} interface can all be associated with the same [=/entry=] simultaneously. 161 | 162 |
163 | {{FileSystemHandle}} objects are [=serializable objects=]. 164 | 165 | Their [=serialization steps=], given |value|, |serialized| and forStorage are: 166 | 167 | 1. Set |serialized|.\[[Origin]] to |value|'s [=relevant settings object=]'s [=environment settings object/origin=]. 168 | 1. Set |serialized|.\[[Entry]] to |value|'s [=FileSystemHandle/entry=]. 169 | 170 |
171 | 172 |
173 | Their [=deserialization steps=], given |serialized| and |value| are: 174 | 175 | 1. If |serialized|.\[[Origin]] is not [=same origin=] with 176 | |value|'s [=relevant settings object=]'s [=environment settings object/origin=], 177 | then throw a {{DataCloneError}}. 178 | 1. Set |value|'s [=FileSystemHandle/entry=] to |serialized|.\[[Entry]] 179 | 180 |
181 | 182 |
183 | : |handle| . {{FileSystemHandle/kind}} 184 | :: Returns {{FileSystemHandleKind/"file"}} if |handle| is a {{FileSystemFileHandle}}, 185 | or {{FileSystemHandleKind/"directory"}} if |handle| is a {{FileSystemDirectoryHandle}}. 186 | 187 | This can be used to distinguish files from directories when iterating over the contents 188 | of a directory. 189 | 190 | : |handle| . {{FileSystemHandle/name}} 191 | :: Returns the [=entry/name=] of the entry represented by |handle|. 192 |
193 | 194 | The kind attribute must 195 | return {{FileSystemHandleKind/"file"}} if the associated [=FileSystemHandle/entry=] is a [=file entry=], 196 | and return {{FileSystemHandleKind/"directory"}} otherwise. 197 | 198 | The name attribute must return the [=entry/name=] of the 199 | associated [=FileSystemHandle/entry=]. 200 | 201 | ### The {{FileSystemHandle/isSameEntry()}} method ### {#api-filesystemhandle-issameentry} 202 | 203 |
204 | : same = await |handle1| . {{FileSystemHandle/isSameEntry()|isSameEntry}}( |handle2| ) 205 | :: Returns true if |handle1| and |handle2| represent the same file or directory. 206 |
207 | 208 |
209 | The isSameEntry(|other|) method, when invoked, must run these steps: 210 | 211 | 1. Let |realm| be [=this=]'s [=relevant Realm=]. 212 | 1. Let |p| be [=a new promise=] in |realm|. 213 | 1. Run the following steps [=in parallel=]: 214 | 1. If [=this=]'s [=FileSystemHandle/entry=] is [=the same as=] |other|'s [=FileSystemHandle/entry=], 215 | [=/resolve=] |p| with `true`. 216 | 1. Else [=/resolve=] |p| with `false`. 217 | 1. Return |p|. 218 | 219 |
220 | 221 | ## The {{FileSystemFileHandle}} interface ## {#api-filesystemfilehandle} 222 | 223 | 224 | dictionary FileSystemCreateWritableOptions { 225 | boolean keepExistingData = false; 226 | }; 227 | 228 | [Exposed=(Window,Worker), SecureContext, Serializable] 229 | interface FileSystemFileHandle : FileSystemHandle { 230 | Promise<File> getFile(); 231 | Promise<FileSystemWritableFileStream> createWritable(optional FileSystemCreateWritableOptions options = {}); 232 | }; 233 | 234 | 235 | A {{FileSystemFileHandle}}'s associated [=FileSystemHandle/entry=] must be a [=file entry=]. 236 | 237 | {{FileSystemFileHandle}} objects are [=serializable objects=]. Their [=serialization steps=] and 238 | [=deserialization steps=] are the same as those for {{FileSystemHandle}}. 239 | 240 | ### The {{FileSystemFileHandle/getFile()}} method ### {#api-filesystemfilehandle-getfile} 241 | 242 |
243 | : file = await |fileHandle| . {{FileSystemFileHandle/getFile()}} 244 | :: Returns a {{File}} representing the state on disk of the entry represented by |handle|. 245 | If the file on disk changes or is removed after this method is called, the returned 246 | {{File}} object will likely be no longer readable. 247 |
248 | 249 |
250 | The getFile() method, when invoked, must run these steps: 251 | 252 | 1. Let |result| be [=a new promise=]. 253 | 1. Run the following steps [=in parallel=]: 254 | 1. Let |access| be the result of running [=this=]'s [=FileSystemHandle/entry=]'s 255 | [=entry/query access=] given "`read`". 256 | 1. If |access| is not "{{PermissionState/granted}}", 257 | reject |result| with a {{NotAllowedError}} and abort. 258 | 1. Let |entry| be [=this=]'s [=FileSystemHandle/entry=]. 259 | 1. Let |f| be a new {{File}}. 260 | 1. Set |f|'s snapshot state to the current state of |entry|. 261 | 1. Set |f|'s underlying byte sequence to a copy of |entry|'s [=binary data=]. 262 | 1. Initialize the value of |f|'s {{File/name}} attribute to |entry|'s [=entry/name=]. 263 | 1. Initialize the value of |f|'s {{File/lastModified}} attribute to |entry|'s [=file entry/modification timestamp=]. 264 | 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. 265 | 266 | Issue: The reading and snapshotting behavior needs to be better specified in the [[FILE-API]] spec, 267 | for now this is kind of hand-wavy. 268 | 1. [=/Resolve=] |result| with |f|. 269 | 1. Return |result|. 270 | 271 |
272 | 273 | ### The {{FileSystemFileHandle/createWritable()}} method ### {#api-filesystemfilehandle-createwritable} 274 | 275 |
276 | : |stream| = await |fileHandle| . {{FileSystemFileHandle/createWritable()}} 277 | : |stream| = await |fileHandle| . {{FileSystemFileHandle/createWritable()|createWritable}}({ {{FileSystemCreateWritableOptions/keepExistingData}}: true/false }) 278 | :: Returns a {{FileSystemWritableFileStream}} that can be used to write to the file. Any changes made through 279 | |stream| won't be reflected in the file represented by |fileHandle| until the stream has been closed. 280 | User agents try to ensure that no partial writes happen, i.e. the file represented by 281 | |fileHandle| will either contain its old contents or it will contain whatever data was written 282 | through |stream| up until the stream has been closed. 283 | 284 | This is typically implemented by writing data to a temporary file, and only replacing the file 285 | represented by |fileHandle| with the temporary file when the writable filestream is closed. 286 | 287 | If {{FileSystemCreateWritableOptions/keepExistingData}} is `false` or not specified, 288 | the temporary file starts out empty, 289 | otherwise the existing file is first copied to this temporary file. 290 |
291 | 292 | Issue(67): There has been some discussion around and desire for a "inPlace" mode for createWritable 293 | (where changes will be written to the actual underlying file as they are written to the writer, for 294 | example to support in-place modification of large files or things like databases). This is not 295 | currently implemented in Chrome. Implementing this is currently blocked on figuring out how to 296 | combine the desire to run malware checks with the desire to let websites make fast in-place 297 | modifications to existing large files. 298 | 299 |
300 | The createWritable(|options|) method, when invoked, must run these steps: 301 | 302 | 1. Let |result| be [=a new promise=]. 303 | 1. Run the following steps [=in parallel=]: 304 | 1. Let |access| be the result of running [=this=]'s [=FileSystemHandle/entry=]'s 305 | [=entry/request access=] given "`readwrite`". 306 | If that throws an exception, [=reject=] |result| with that exception and abort. 307 | 1. If |access| is not "{{PermissionState/granted}}", 308 | reject |result| with a {{NotAllowedError}} and abort. 309 | 1. Let |entry| be [=this=]'s [=FileSystemHandle/entry=]. 310 | 1. Let |stream| be the result of [=create a new FileSystemWritableFileStream|creating a new FileSystemWritableFileStream=] 311 | for |entry| in [=this=]'s [=relevant realm=]. 312 | 1. If |options|.{{FileSystemCreateWritableOptions/keepExistingData}} is `true`: 313 | 1. Set |stream|.[=[[buffer]]=] to a copy of |entry|'s [=file entry/binary data=]. 314 | 1. [=/Resolve=] |result| with |stream|. 315 | 1. Return |result|. 316 | 317 |
318 | 319 | ## The {{FileSystemDirectoryHandle}} interface ## {#api-filesystemdirectoryhandle} 320 | 321 | 322 | dictionary FileSystemGetFileOptions { 323 | boolean create = false; 324 | }; 325 | 326 | dictionary FileSystemGetDirectoryOptions { 327 | boolean create = false; 328 | }; 329 | 330 | dictionary FileSystemRemoveOptions { 331 | boolean recursive = false; 332 | }; 333 | 334 | [Exposed=(Window,Worker), SecureContext, Serializable] 335 | interface FileSystemDirectoryHandle : FileSystemHandle { 336 | async iterable<USVString, FileSystemHandle>; 337 | 338 | Promise<FileSystemFileHandle> getFileHandle(USVString name, optional FileSystemGetFileOptions options = {}); 339 | Promise<FileSystemDirectoryHandle> getDirectoryHandle(USVString name, optional FileSystemGetDirectoryOptions options = {}); 340 | 341 | Promise<undefined> removeEntry(USVString name, optional FileSystemRemoveOptions options = {}); 342 | 343 | Promise<sequence<USVString>?> resolve(FileSystemHandle possibleDescendant); 344 | }; 345 | 346 | 347 | A {{FileSystemDirectoryHandle}}'s associated [=FileSystemHandle/entry=] must be a [=directory entry=]. 348 | 349 | {{FileSystemDirectoryHandle}} objects are [=serializable objects=]. Their [=serialization steps=] and 350 | [=deserialization steps=] are the same as those for {{FileSystemHandle}}. 351 | 352 | ### Directory iteration ### {#api-filesystemdirectoryhandle-asynciterable} 353 | 354 |
355 | : for await (let [|name|, |handle|] of |directoryHandle|) {} 356 | : for await (let [|name|, |handle|] of |directoryHandle| . entries()) {} 357 | : for await (let |handle| of |directoryHandle| . values()) {} 358 | : for await (let |name| of |directoryHandle| . keys()) {} 359 | :: Iterates over all entries whose parent is the entry represented by |directoryHandle|. Entries 360 | that are created or deleted while the iteration is in progress might or might not be included. 361 | No guarantees are given either way. 362 |
363 | 364 | Issue(173): In the future we might want to add arguments to the async iterable declaration to 365 | support for example recursive iteration. 366 | 367 |
368 | The [=asynchronous iterator initialization steps=] for a {{FileSystemDirectoryHandle}} |handle| 369 | ant its async iterator |iterator| are: 370 | 371 | 1. Let |access| be the result of running |handle|'s [=FileSystemHandle/entry=]'s 372 | [=entry/query access=] given "`read`". 373 | 374 | 1. If |access| is not "{{PermissionState/granted}}", 375 | throw a {{NotAllowedError}}. 376 | 377 | 1. Set |iterator|'s past results to an empty [=/set=]. 378 | 379 |
380 | 381 |
382 | To [=get the next iteration result=] for a {{FileSystemDirectoryHandle}} |handle| 383 | and its async iterator |iterator|: 384 | 385 | 1. Let |promise| be [=a new promise=]. 386 | 387 | 1. Let |directory| be |handle|'s [=FileSystemHandle/entry=]. 388 | 389 | 1. Let |access| be the result of running |handle|'s [=FileSystemHandle/entry=]'s 390 | [=entry/query access=] given "`read`". 391 | 392 | 1. If |access| is not "{{PermissionState/granted}}", 393 | reject |promise| with a {{NotAllowedError}} and return |promise|. 394 | 395 | 1. Let |child| be an [=/entry=] in |directory|'s [=directory entry/children=], 396 | such that |child|'s [=entry/name=] is not contained in |iterator|'s [=past results=], 397 | or `null` if no such entry exists. 398 | 399 | Note: This is intentionally very vague about the iteration order. Different platforms 400 | and file systems provide different guarantees about iteration order, and we want it to 401 | be possible to efficiently implement this on all platforms. As such no guarantees are given 402 | about the exact order in which elements are returned. 403 | 404 | 1. If |child| is `null`, then: 405 | 1. [=/Resolve=] |promise| with `undefined`. 406 | 407 | 1. Otherwise: 408 | 1. [=set/Append=] |child|'s [=entry/name=] to |iterator|'s [=past results=]. 409 | 1. If |child| is a [=file entry=]: 410 | 1. Let |result| be a new {{FileSystemFileHandle}} associated with |child|. 411 | 1. Otherwise: 412 | 1. Let |result| be a new {{FileSystemDirectoryHandle}} associated with |child|. 413 | 1. [=/Resolve=] |promise| with (|child|'s [=entry/name=], |result|). 414 | 415 | 1. Return |promise|. 416 | 417 |
418 | 419 | ### The {{FileSystemDirectoryHandle/getFileHandle()}} method ### {#api-filesystemdirectoryhandle-getfilehandle} 420 | 421 |
422 | : |fileHandle| = await |directoryHandle| . {{FileSystemDirectoryHandle/getFileHandle()|getFileHandle}}(|name|) 423 | : |fileHandle| = await |directoryHandle| . {{FileSystemDirectoryHandle/getFileHandle()|getFileHandle}}(|name|, { {{FileSystemGetFileOptions/create}}: false }) 424 | :: Returns a handle for a file named |name| in the directory represented by |directoryHandle|. If 425 | no such file exists, this rejects. 426 | 427 | : |fileHandle| = await |directoryHandle| . {{FileSystemDirectoryHandle/getFileHandle()|getFileHandle}}(|name|, { {{FileSystemGetFileOptions/create}}: true }) 428 | :: Returns a handle for a file named |name| in the directory represented by |directoryHandle|. If 429 | no such file exists, this creates a new file. If no file with named |name| can be created this 430 | rejects. Creation can fail because there already is a directory with the same name, because the 431 | name uses characters that aren't supported in file names on the underlying file system, or 432 | because the user agent for security reasons decided not to allow creation of the file. 433 | 434 | This operation requires write permission, even if the file being returned already exists. If 435 | this handle doesn't already have write permission, this could result in a prompt being shown to 436 | the user. To get an existing file without needing write permission, call this method 437 | with { {{FileSystemGetFileOptions/create}}: false }. 438 |
439 | 440 |
441 | The getFileHandle(|name|, |options|) method, when invoked, 442 | must run these steps: 443 | 444 | 1. Let |result| be [=a new promise=]. 445 | 1. Run the following steps [=in parallel=]: 446 | 1. If |name| is not a [=valid file name=], [=/reject=] |result| with a {{TypeError}} and abort. 447 | 448 | 1. Let |entry| be [=this=]'s [=FileSystemHandle/entry=]. 449 | 1. If |options|.{{FileSystemGetFileOptions/create}} is `true`: 450 | 1. Let |access| be the result of running [=this=]'s [=FileSystemHandle/entry=]'s 451 | [=entry/request access=] given "`readwrite`". 452 | If that throws an exception, [=reject=] |result| with that exception and abort. 453 | 1. Otherwise: 454 | 1. Let |access| be the result of running [=this=]'s [=FileSystemHandle/entry=]'s 455 | [=entry/query access=] given "`read`". 456 | 1. If |access| is not "{{PermissionState/granted}}", 457 | reject |result| with a {{NotAllowedError}} and abort. 458 | 459 | 1. [=set/For each=] |child| of |entry|'s [=directory entry/children=]: 460 | 1. If |child|'s [=entry/name=] equals |name|: 461 | 1. If |child| is a [=directory entry=]: 462 | 1. [=/Reject=] |result| with a {{TypeMismatchError}} and abort. 463 | 1. [=/Resolve=] |result| with a new {{FileSystemFileHandle}} whose [=FileSystemHandle/entry=] is |child| and abort. 464 | 1. If |options|.{{FileSystemGetFileOptions/create}} is `false`: 465 | 1. [=/Reject=] |result| with a {{NotFoundError}} and abort. 466 | 1. Let |child| be a new [=file entry=] whose [=query access=] and [=request access=] algorithms 467 | are those of |entry|. 468 | 1. Set |child|'s [=entry/name=] to |name|. 469 | 1. Set |child|'s [=binary data=] to an empty [=byte sequence=]. 470 | 1. Set |child|'s [=modification timestamp=] to the current time. 471 | 1. [=set/Append=] |child| to |entry|'s [=directory entry/children=]. 472 | 1. If creating |child| in the underlying file system throws an exception, 473 | [=/reject=] |result| with that exception and abort. 474 | 475 | Issue(68): Better specify what possible exceptions this could throw. 476 | 1. [=/Resolve=] |result| with a new {{FileSystemFileHandle}} whose [=FileSystemHandle/entry=] is |child|. 477 | 1. Return |result|. 478 | 479 |
480 | 481 | ### The {{FileSystemDirectoryHandle/getDirectoryHandle()}} method ### {#api-filesystemdirectoryhandle-getdirectoryhandle} 482 | 483 |
484 | : |subdirHandle| = await |directoryHandle| . {{FileSystemDirectoryHandle/getDirectoryHandle()|getDirectoryHandle}}(|name|) 485 | : |subdirHandle| = await |directoryHandle| . {{FileSystemDirectoryHandle/getDirectoryHandle()|getDirectoryHandle}}(|name|, { {{FileSystemGetDirectoryOptions/create}}: false }) 486 | :: Returns a handle for a directory named |name| in the directory represented by 487 | |directoryHandle|. If no such directory exists, this rejects. 488 | 489 | : |subdirHandle| = await |directoryHandle| . {{FileSystemDirectoryHandle/getDirectoryHandle()|getDirectoryHandle}}(|name|, { {{FileSystemGetDirectoryOptions/create}}: true }) 490 | :: Returns a handle for a directory named |name| in the directory represented by 491 | |directoryHandle|. If no such directory exists, this creates a new directory. If creating the 492 | directory failed, this rejects. Creation can fail because there already is a file with the same 493 | name, or because the name uses characters that aren't supported in file names on the underlying 494 | file system. 495 | 496 | This operation requires write permission, even if the directory being returned already exists. 497 | If this handle doesn't already have write permission, this could result in a prompt being shown 498 | to the user. To get an existing directory without needing write permission, call this method 499 | with { {{FileSystemGetDirectoryOptions/create}}: false }. 500 |
501 | 502 |
503 | The getDirectoryHandle(|name|, |options|) method, when 504 | invoked, must run these steps: 505 | 506 | 1. Let |result| be [=a new promise=]. 507 | 1. Run the following steps [=in parallel=]: 508 | 1. If |name| is not a [=valid file name=], [=/reject=] |result| with a {{TypeError}} and abort. 509 | 510 | 1. Let |entry| be [=this=]'s [=FileSystemHandle/entry=]. 511 | 1. If |options|.{{FileSystemGetDirectoryOptions/create}} is `true`: 512 | 1. Let |access| be the result of running [=this=]'s [=FileSystemHandle/entry=]'s 513 | [=entry/request access=] given "`readwrite`". 514 | If that throws an exception, [=reject=] |result| with that exception and abort. 515 | 1. Otherwise: 516 | 1. Let |access| be the result of running [=this=]'s [=FileSystemHandle/entry=]'s 517 | [=entry/query access=] given "`read`". 518 | 1. If |access| is not "{{PermissionState/granted}}", 519 | reject |result| with a {{NotAllowedError}} and abort. 520 | 521 | 1. [=set/For each=] |child| of |entry|'s [=directory entry/children=]: 522 | 1. If |child|'s [=entry/name=] equals |name|: 523 | 1. If |child| is a [=file entry=]: 524 | 1. [=/Reject=] |result| with a {{TypeMismatchError}} and abort. 525 | 1. [=/Resolve=] |result| with a new {{FileSystemDirectoryHandle}} whose [=FileSystemHandle/entry=] is |child| and abort. 526 | 1. If |options|.{{FileSystemGetFileOptions/create}} is `false`: 527 | 1. [=/Reject=] |result| with a {{NotFoundError}} and abort. 528 | 1. Let |child| be a new [=directory entry=] whose [=query access=] and [=request access=] 529 | algorithms are those of |entry|. 530 | 1. Set |child|'s [=entry/name=] to |name|. 531 | 1. Set |child|'s [=directory entry/children=] to an empty [=/set=]. 532 | 1. [=set/Append=] |child| to |entry|'s [=directory entry/children=]. 533 | 1. If creating |child| in the underlying file system throws an exception, 534 | [=/reject=] |result| with that exception and abort. 535 | 536 | Issue(68): Better specify what possible exceptions this could throw. 537 | 1. [=/Resolve=] |result| with a new {{FileSystemDirectoryHandle}} whose [=FileSystemHandle/entry=] is |child|. 538 | 1. Return |result|. 539 | 540 |
541 | 542 | ### The {{FileSystemDirectoryHandle/removeEntry()}} method ### {#api-filesystemdirectoryhandle-removeentry} 543 | 544 |
545 | : await |directoryHandle| . {{FileSystemDirectoryHandle/removeEntry()|removeEntry}}(|name|) 546 | : await |directoryHandle| . {{FileSystemDirectoryHandle/removeEntry()|removeEntry}}(|name|, { {{FileSystemRemoveOptions/recursive}}: false }) 547 | :: If the directory represented by |directoryHandle| contains a file named |name|, or an empty 548 | directory named |name|, this will attempt to delete that file or directory. 549 | 550 | Attempting to delete a file or directory that does not exist is considered success, 551 | while attempting to delete a non-empty directory will result in a promise rejection. 552 | 553 | : await |directoryHandle| . {{FileSystemDirectoryHandle/removeEntry()|removeEntry}}(|name|, { {{FileSystemRemoveOptions/recursive}}: true }) 554 | :: Removes the entry named |name| in the directory represented by |directoryHandle|. 555 | If that entry is a directory, its contents will also be deleted recursively. 556 | recursively. 557 | 558 | Attempting to delete a file or directory that does not exist is considered success. 559 |
560 | 561 |
562 | The removeEntry(|name|, |options|) method, when invoked, must run 563 | these steps: 564 | 565 | 1. Let |result| be [=a new promise=]. 566 | 1. Run the following steps [=in parallel=]: 567 | 1. If |name| is not a [=valid file name=], [=/reject=] |result| with a {{TypeError}} and abort. 568 | 569 | 1. Let |entry| be [=this=]'s [=FileSystemHandle/entry=]. 570 | 1. Let |access| be the result of running [=this=]'s [=FileSystemHandle/entry=]'s 571 | [=entry/request access=] given "`readwrite`". 572 | If that throws an exception, [=reject=] |result| with that exception and abort. 573 | 1. If |access| is not "{{PermissionState/granted}}", 574 | reject |result| with a {{NotAllowedError}} and abort. 575 | 576 | 1. [=set/For each=] |child| of |entry|'s [=directory entry/children=]: 577 | 1. If |child|'s [=entry/name=] equals |name|: 578 | 1. If |child| is a [=directory entry=]: 579 | 1. If |child|'s [=directory entry/children=] is not [=set/is empty|empty=] and |options|.{{FileSystemRemoveOptions/recursive}} is `false`: 580 | 1. [=/Reject=] |result| with an {{InvalidModificationError}} and abort. 581 | 1. [=set/Remove=] |child| from |entry|'s [=directory entry/children=]. 582 | 1. If removing |child| in the underlying file system throws an exception, 583 | [=/reject=] |result| with that exception and abort. 584 | 585 | Note: If {{FileSystemRemoveOptions/recursive}} is `true`, the removal can fail 586 | non-atomically. Some files or directories might have been removed while other files 587 | or directories still exist. 588 | 589 | Issue(68): Better specify what possible exceptions this could throw. 590 | 1. [=/Resolve=] |result| with `undefined`. 591 | 1. [=/Reject=] |result| with a {{NotFoundError}}. 592 | 1. Return |result|. 593 | 594 | 595 |
596 | 597 | ### The {{FileSystemDirectoryHandle/resolve()}} method ### {#api-filesystemdirectoryhandle-resolve} 598 | 599 |
600 | : |path| = await |directory| . {{FileSystemDirectoryHandle/resolve()|resolve}}( |child| ) 601 | :: If |child| is equal to |directory|, |path| will be an empty array. 602 | :: If |child| is a direct child of |directory|, |path| will be an array containing |child|'s name. 603 | :: If |child| is a descendant of |directory|, |path| will be an array containing the names of 604 | all the intermediate directories and |child|'s name as last element. 605 | For example if |directory| represents `/home/user/project` 606 | and |child| represents `/home/user/project/foo/bar`, this will return 607 | `['foo', 'bar']`. 608 | :: Otherwise (|directory| and |child| are not related), |path| will be null. 609 |
610 | 611 |
612 | 613 | // Assume we at some point got a valid directory handle. 614 | const dir_ref = current_project_dir; 615 | if (!dir_ref) return; 616 | 617 | // Now get a file reference: 618 | const file_ref = await dir_ref.getFileHandle(filename, { create: true }); 619 | 620 | // Check if file_ref exists inside dir_ref: 621 | const relative_path = await dir_ref.resolve(file_ref); 622 | if (relative_path === null) { 623 | // Not inside dir_ref. 624 | } else { 625 | // relative_path is an array of names, giving the relative path 626 | // from dir_ref to the file that is represented by file_ref: 627 | assert relative_path.pop() === file_ref.name; 628 | 629 | let entry = dir_ref; 630 | for (const name of relative_path) { 631 | entry = await entry.getDirectory(name); 632 | } 633 | entry = await entry.getFile(file_ref.name); 634 | 635 | // Now |entry| will represent the same file on disk as |file_ref|. 636 | assert await entry.isSameEntry(file_ref) === true; 637 | } 638 | 639 |
640 | 641 |
642 | The resolve(|possibleDescendant|) method, 643 | when invoked, must return the result of [=entry/resolving=] 644 | |possibleDescendant|'s [=FileSystemHandle/entry=] relative to [=this=]'s [=FileSystemHandle/entry=]. 645 | 646 |
647 | 648 | 649 | 650 | ## The {{FileSystemWritableFileStream}} interface ## {#api-filesystemwritablefilestream} 651 | 652 | 653 | enum WriteCommandType { 654 | "write", 655 | "seek", 656 | "truncate", 657 | }; 658 | 659 | dictionary WriteParams { 660 | required WriteCommandType type; 661 | unsigned long long? size; 662 | unsigned long long? position; 663 | (BufferSource or Blob or USVString)? data; 664 | }; 665 | 666 | typedef (BufferSource or Blob or USVString or WriteParams) FileSystemWriteChunkType; 667 | 668 | [Exposed=(Window,Worker), SecureContext] 669 | interface FileSystemWritableFileStream : WritableStream { 670 | Promise<undefined> write(FileSystemWriteChunkType data); 671 | Promise<undefined> seek(unsigned long long position); 672 | Promise<undefined> truncate(unsigned long long size); 673 | }; 674 | 675 | 676 | A {{FileSystemWritableFileStream}} has an associated \[[file]] (a [=file entry=]). 677 | 678 | A {{FileSystemWritableFileStream}} has an associated \[[buffer]] (a [=byte sequence=]). 679 | It is initially empty. 680 | 681 | Note: This buffer can get arbitrarily large, so it is expected that implementations will not keep this in memory, 682 | but instead use a temporary file for this. All access to \[[buffer]] is done in promise returning methods and 683 | algorithms, so even though operations on it seem sync, implementations can implement them async. 684 | 685 | A {{FileSystemWritableFileStream}} has an associated \[[seekOffset]] (a number). 686 | It is initially 0. 687 | 688 |
689 | A {{FileSystemWritableFileStream}} object is a {{WritableStream}} object with additional 690 | convenience methods, which operates on a single file on disk. 691 | 692 | Upon creation, an underlying sink will have been created and the stream will be usable. 693 | All operations executed on the stream are queuable and producers will be able to respond to backpressure. 694 | 695 | The underlying sink's write method, and therefore {{WritableStreamDefaultWriter/write()|WritableStreamDefaultWriter's write()}} 696 | method, will accept byte-like data or {{WriteParams}} as input. 697 | 698 | The {{FileSystemWritableFileStream}} has a file position cursor initialized at byte offset 0 from the top of the file. 699 | When using {{FileSystemWritableFileStream/write()|write()}} or by using WritableStream capabilities through the {{WritableStreamDefaultWriter/write()|WritableStreamDefaultWriter's write()}} method, this position will be advanced based on the number of bytes written through the stream object. 700 | 701 | Similarly, when piping a {{ReadableStream}} into a {{FileSystemWritableFileStream}} object, this position is updated with the number of bytes that passed through the stream. 702 | 703 | {{WritableStream/getWriter()|getWriter()}} returns an instance of {{WritableStreamDefaultWriter}}. 704 |
705 | 706 |
707 | To create a new FileSystemWritableFileStream given a [=file entry=] |file| 708 | in a [=/Realm=] |realm|, perform the following steps: 709 | 710 | 1. Let |stream| be a [=new=] {{FileSystemWritableFileStream}} in |realm|. 711 | 1. Set |stream|.[=FileSystemWritableFileStream/[[file]]=] to |file|. 712 | 1. Let |writeAlgorithm| be an algorithm which takes a |chunk| argument 713 | and returns the result of running the [=write a chunk=] algorithm with |stream| and |chunk|. 714 | 1. Let |closeAlgorithm| be the following steps: 715 | 1. Let |closeResult| be [=a new promise=]. 716 | 1. Run the following steps [=in parallel=]: 717 | 1. Let |access| be the result of running |file|'s [=entry/query access=] given "`readwrite`". 718 | 1. If |access| is not "{{PermissionState/granted}}", 719 | reject |closeResult| with a {{NotAllowedError}} and abort. 720 | 1. Perform [=implementation-defined=] malware scans and safe browsing checks. 721 | If these checks fail, [=/reject=] |closeResult| with an {{AbortError}} and abort. 722 | 1. Set |stream|.[=[[file]]=]'s [=file entry/binary data=] to |stream|.[=[[buffer]]=]. 723 | If that throws an exception, [=/reject=] |closeResult| with that exception and abort. 724 | 725 | Note: It is expected that this atomically updates the contents of the file on disk 726 | being written to. 727 | 1. [=/Resolve=] |closeResult| with `undefined`. 728 | 1. Return |closeResult|. 729 | 1. Let |highWaterMark| be 1. 730 | 1. Let |sizeAlgorithm| be an algorithm that returns `1`. 731 | 1. [=WritableStream/Set up=] |stream| with writeAlgorithm set to |writeAlgorithm|, closeAlgorithm set to |closeAlgorithm|, highWaterMark set to |highWaterMark|, and sizeAlgorithm set to |sizeAlgorithm|. 736 | 1. Return |stream|. 737 | 738 |
739 | 740 |
741 | The write a chunk algorithm, 742 | given a {{FileSystemWritableFileStream}} |stream| and |chunk|, 743 | runs these steps: 744 | 745 | 1. Let |input| be the result of [=converting=] |chunk| to a {{FileSystemWriteChunkType}}. 746 | If this throws an exception, then return [=a promise rejected with=] that exception. 747 | 1. Let |p| be [=a new promise=]. 748 | 1. Run the following steps [=in parallel=]: 749 | 1. Let |access| be the result of running |stream|'s [=FileSystemWritableFileStream/[[file]]=]'s 750 | [=entry/query access=] given "`readwrite`". 751 | 1. If |access| is not "{{PermissionState/granted}}", 752 | reject |p| with a {{NotAllowedError}} and abort. 753 | 1. Let |command| be |input|.{{WriteParams/type}} if |input| is a {{WriteParams}}, 754 | and {{WriteCommandType/"write"}} otherwise. 755 | 1. If |command| is {{WriteCommandType/"write"}}: 756 | 1. Let |data| be |input|.{{WriteParams/data}} if |input| is a {{WriteParams}}, 757 | and |input| otherwise. 758 | 1. If |data| is `undefined`, 759 | reject |p| with a {{TypeError}} and abort. 760 | 1. Let |writePosition| be |stream|.[=[[seekOffset]]=]. 761 | 1. If |input| is a {{WriteParams}} and |input|.{{WriteParams/position}} is not `undefined`, 762 | set |writePosition| to |input|.{{WriteParams/position}}. 763 | 1. Let |oldSize| be |stream|.[=[[buffer]]=]'s [=byte sequence/length=]. 764 | 1. If |data| is a {{BufferSource}}, 765 | let |dataBytes| be [=get a copy of the buffer source|a copy of=] |data|. 766 | 1. Else if |data| is a {{Blob}}: 767 | 1. Let |dataBytes| be the result of performing the 768 | read operation on |data|. 769 | If this throws an exception, [=/reject=] |p| with that exception and abort. 770 | 1. Else: 771 | 1. [=Assert=]: |data| is a {{USVString}}. 772 | 1. Let |dataBytes| be the result of [=UTF-8 encoding=] |data|. 773 | 1. If |writePosition| is larger than |oldSize|, 774 | append |writePosition| - |oldSize| `0x00` (NUL) bytes to the end of |stream|.[=[[buffer]]=]. 775 | 776 | Note: Implementations are expected to behave as if the skipped over file contents 777 | are indeed filled with NUL bytes. That doesn't mean these bytes have to actually be 778 | written to disk and take up disk space. Instead most file systems support so called 779 | sparse files, where these NUL bytes don't take up actual disk space. 780 | 781 | 1. Let |head| be a [=byte sequence=] containing the first |writePosition| bytes of |stream|.[=[[buffer]]=]. 782 | 1. Let |tail| be an empty [=byte sequence=]. 783 | 1. If |writePosition| + |data|.[=byte sequence/length=] is smaller than |oldSize|: 784 | 1. Let |tail| be a [=byte sequence=] containing the last 785 | |oldSize| - (|writePosition| + |data|.[=byte sequence/length=]) bytes of |stream|.[=[[buffer]]=]. 786 | 1. Set |stream|.[=[[buffer]]=] to the concatenation of |head|, |data| and |tail|. 787 | 1. If the operations modifying |stream|.[=[[buffer]]=] in the previous steps failed 788 | due to exceeding the [=storage quota=], [=/reject=] |p| with a {{QuotaExceededError}} and abort, 789 | leaving |stream|.[=[[buffer]]=] unmodified. 790 | 791 | Note: [=Storage quota=] only applies to files stored in the [=origin private file system=]. 792 | However this operation could still fail for other files, for example if the disk being written 793 | to runs out of disk space. 794 | 1. Set |stream|.[=[[seekOffset]]=] to |writePosition| + |data|.[=byte sequence/length=]. 795 | 1. [=/Resolve=] |p|. 796 | 1. Else if |command| is {{WriteCommandType/"seek"}}: 797 | 1. If |chunk|.{{WriteParams/position}} is `undefined`, 798 | [=/reject=] |p| with a {{TypeError}} and abort. 799 | 1. Set |stream|.[=[[seekOffset]]=] to |chunk|.{{WriteParams/position}}. 800 | 1. [=/Resolve=] |p|. 801 | 1. Else if |command| is {{WriteCommandType/"truncate"}}: 802 | 1. If |chunk|.{{WriteParams/size}} is `undefined`, 803 | [=/reject=] |p| with a {{TypeError}} and abort. 804 | 1. Let |newSize| be |chunk|.{{WriteParams/size}}. 805 | 1. Let |oldSize| be |stream|.[=[[buffer]]=]'s [=byte sequence/length=]. 806 | 1. If |newSize| is larger than |oldSize|: 807 | 1. Set |stream|.[=[[buffer]]=] to a [=byte sequence=] formed by concating 808 | |stream|.[=[[buffer]]=] with a [=byte sequence=] containing |newSize|-|oldSize| `0x00` bytes. 809 | 1. If the operation in the previous step failed due to exceeding the [=storage quota=], 810 | [=/reject=] |p| with a {{QuotaExceededError}} and abort, 811 | leaving |stream|.[=[[buffer]]=] unmodified. 812 | 813 | Note: [=Storage quota=] only applies to files stored in the [=origin private file system=]. 814 | However this operation could still fail for other files, for example if the disk being written 815 | to runs out of disk space. 816 | 1. Else if |newSize| is smaller than |oldSize|: 817 | 1. Set |stream|.[=[[buffer]]=] to a [=byte sequence=] containing the first |newSize| bytes 818 | in |stream|.[=[[buffer]]=]. 819 | 1. If |stream|.[=[[seekOffset]]=] is bigger than |newSize|, 820 | set |stream|.[=[[seekOffset]]=] to |newSize|. 821 | 1. [=/Resolve=] |p|. 822 | 1. Return |p|. 823 | 824 |
825 | 826 | ### The {{FileSystemWritableFileStream/write()}} method ### {#api-filesystemwritablefilestream-write} 827 | 828 |
829 | : await |stream| . {{FileSystemWritableFileStream/write()|write}}(|data|) 830 | : await |stream| . {{FileSystemWritableFileStream/write()|write}}({ 831 | {{WriteParams/type}}: {{WriteCommandType/"write"}}, 832 | {{WriteParams/data}}: |data| }) 833 | :: Writes the content of |data| into the file associated with |stream| at the current file 834 | cursor offset. 835 | 836 | No changes are written to the actual file on disk until the stream has been closed. 837 | Changes are typically written to a temporary file instead. 838 | 839 | : await |stream| . {{FileSystemWritableFileStream/write()|write}}({ 840 | {{WriteParams/type}}: {{WriteCommandType/"write"}}, 841 | {{WriteParams/position}}: |position|, 842 | {{WriteParams/data}}: |data| }) 843 | :: Writes the content of |data| into the file associated with |stream| at |position| 844 | bytes from the top of the file. Also updates the current file cursor offset to the 845 | end of the written data. 846 | 847 | No changes are written to the actual file on disk until the stream has been closed. 848 | Changes are typically written to a temporary file instead. 849 | 850 | : await |stream| . {{FileSystemWritableFileStream/write()|write}}({ 851 | {{WriteParams/type}}: {{WriteCommandType/"seek"}}, 852 | {{WriteParams/position}}: |position| }) 853 | :: Updates the current file cursor offset the |position| bytes from the top of the file. 854 | 855 | : await |stream| . {{FileSystemWritableFileStream/write()|write}}({ 856 | {{WriteParams/type}}: {{WriteCommandType/"truncate"}}, 857 | {{WriteParams/size}}: |size| }) 858 | :: Resizes the file associated with |stream| to be |size| bytes long. If |size| is larger than 859 | the current file size this pads the file with null bytes, otherwise it truncates the file. 860 | 861 | The file cursor is updated when {{truncate}} is called. If the offset is smaller than offset, 862 | it remains unchanged. If the offset is larger than |size|, the offset is set to |size| to 863 | ensure that subsequent writes do not error. 864 | 865 | No changes are written to the actual file until on disk until the stream has been closed. 866 | Changes are typically written to a temporary file instead. 867 |
868 | 869 |
870 | The write(|data|) method, when invoked, must run 871 | these steps: 872 | 873 | 1. Let |writer| be the result of [=WritableStream/getting a writer=] for [=this=]. 874 | 1. Let |result| be the result of [=WritableStreamDefaultWriter/writing a chunk=] to |writer| given 875 | |data|. 876 | 1. [=WritableStreamDefaultWriter/Release=] |writer|. 877 | 1. Return |result|. 878 | 879 |
880 | 881 | ### The {{FileSystemWritableFileStream/seek()}} method ### {#api-filesystemwritablefilestream-seek} 882 | 883 |
884 | : await |stream| . {{FileSystemWritableFileStream/seek()|seek}}(|position|) 885 | :: Updates the current file cursor offset the |position| bytes from the top of the file. 886 |
887 | 888 |
889 | The seek(|position|) method, when invoked, must run these 890 | steps: 891 | 892 | 1. Let |writer| be the result of [=WritableStream/getting a writer=] for [=this=]. 893 | 1. Let |result| be the result of [=WritableStreamDefaultWriter/writing a chunk=] to |writer| given 894 | «[ "{{WriteParams/type}}" → {{WriteCommandType/"seek"}}, "{{WriteParams/position}}" → 895 | |position| ]». 896 | 1. [=WritableStreamDefaultWriter/Release=] |writer|. 897 | 1. Return |result|. 898 | 899 |
900 | 901 | ### The {{FileSystemWritableFileStream/truncate()}} method ### {#api-filesystemwritablefilestream-truncate} 902 | 903 |
904 | : await |stream| . {{FileSystemWritableFileStream/truncate()|truncate}}(|size|) 905 | :: Resizes the file associated with |stream| to be |size| bytes long. If |size| is larger than 906 | the current file size this pads the file with null bytes, otherwise it truncates the file. 907 | 908 | The file cursor is updated when {{truncate}} is called. If the offset is smaller than offset, 909 | it remains unchanged. If the offset is larger than |size|, the offset is set to |size| to 910 | ensure that subsequent writes do not error. 911 | 912 | No changes are written to the actual file until on disk until the stream has been closed. 913 | Changes are typically written to a temporary file instead. 914 |
915 | 916 |
917 | The truncate(|size|) method, when invoked, must run these 918 | steps: 919 | 920 | 1. Let |writer| be the result of [=WritableStream/getting a writer=] for [=this=]. 921 | 1. Let |result| be the result of [=WritableStreamDefaultWriter/writing a chunk=] to |writer| given 922 | «[ "{{WriteParams/type}}" → {{WriteCommandType/"truncate"}}, "{{WriteParams/size}}" → 923 | |size| ]». 924 | 1. [=WritableStreamDefaultWriter/Release=] |writer|. 925 | 1. Return |result|. 926 | 927 |
928 | 929 | 930 | # Accessing the Origin Private File System # {#sandboxed-filesystem} 931 | 932 | The origin private file system is a [=storage endpoint=] whose 933 | identifier is `"fileSystem"`, 934 | types are `« "local" »`, 935 | and quota is null. 936 | 937 | Issue: Storage endpoints should be defined in [[storage]] itself, rather 938 | than being defined here. So merge this into the table there. 939 | 940 | Note: While user agents will typically implement this by persisting the contents of this 941 | [=origin private file system=] to disk, it is not intended that the contents are easily 942 | user accessible. Similarly there is no expectation that files or directories with names 943 | matching the names of children of the [=origin private file system=] exist. 944 | 945 | 946 | [SecureContext] 947 | partial interface StorageManager { 948 | Promise<FileSystemDirectoryHandle> getDirectory(); 949 | }; 950 | 951 | 952 |
953 | : |directoryHandle| = await navigator . storage . {{StorageManager/getDirectory()}} 954 | :: Returns the root directory of the origin private file system. 955 |
956 | 957 |
958 | The getDirectory() method, when 959 | invoked, must run these steps: 960 | 961 | 1. Let |environment| be the [=current settings object=]. 962 | 963 | 1. Let |map| be the result of running [=obtain a local storage bottle map=] 964 | with |environment| and `"fileSystem"`. If this returns failure, 965 | return [=a promise rejected with=] a {{SecurityError}}. 966 | 967 | 1. If |map|["root"] does not [=map/exist=]: 968 | 1. Let |dir| be a new [=directory entry=] whose [=query access=] and [=request access=] algorithms 969 | always return "{{PermissionState/granted}}". 970 | 1. Set |dir|'s [=entry/name=] to the empty string. 971 | 1. Set |dir|'s [=directory entry/children=] to an empty [=/set=]. 972 | 1. Set |map|["root"] to |dir|. 973 | 974 | 1. Return [=a promise resolved with=] a new {{FileSystemDirectoryHandle}}, 975 | whose associated [=FileSystemHandle/entry=] is |map|["root"]. 976 | 977 |
978 | 979 | 980 |

Acknowledgments

981 | 982 |

This standard is written by Marijn Kruisselbrink 983 | (Google, mek@chromium.org). 984 | 985 | 986 |

This Living Standard includes material copied from W3C WICG's 987 | File System Access, which is 988 | available under the 989 | W3C Software and Document License. 990 | --------------------------------------------------------------------------------