├── .github └── workflows │ └── main.yml ├── .gitignore ├── .prettierignore ├── .prettierrc ├── CODE_OF_CONDUCT.md ├── CONTRIBUTING.md ├── LICENSE ├── README.md ├── demo ├── floppy.png ├── image-to-blob.mjs ├── index.html ├── script.mjs └── style.css ├── eslint.config.mjs ├── index.d.ts ├── package-lock.json ├── package.json └── src ├── directory-open.mjs ├── file-open.mjs ├── file-save.mjs ├── fs-access ├── directory-open.mjs ├── file-open.mjs └── file-save.mjs ├── index.js ├── legacy ├── directory-open.mjs ├── file-open.mjs └── file-save.mjs └── supported.mjs /.github/workflows/main.yml: -------------------------------------------------------------------------------- 1 | name: Compressed Size 2 | 3 | on: [pull_request] 4 | 5 | jobs: 6 | build: 7 | runs-on: ubuntu-latest 8 | 9 | steps: 10 | - uses: actions/checkout@v2-beta 11 | with: 12 | fetch-depth: 1 13 | - uses: preactjs/compressed-size-action@v1 14 | with: 15 | repo-token: '${{ secrets.GITHUB_TOKEN }}' 16 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | 2 | .DS_Store 3 | node_modules/ 4 | dist/ 5 | 6 | -------------------------------------------------------------------------------- /.prettierignore: -------------------------------------------------------------------------------- 1 | dist/ -------------------------------------------------------------------------------- /.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "arrowParens": "always", 3 | "bracketSpacing": true, 4 | "htmlWhitespaceSensitivity": "css", 5 | "insertPragma": false, 6 | "jsxBracketSameLine": false, 7 | "jsxSingleQuote": false, 8 | "printWidth": 80, 9 | "proseWrap": "preserve", 10 | "quoteProps": "as-needed", 11 | "requirePragma": false, 12 | "semi": true, 13 | "singleQuote": true, 14 | "tabWidth": 2, 15 | "trailingComma": "es5", 16 | "useTabs": false, 17 | "vueIndentScriptAndStyle": false 18 | } 19 | -------------------------------------------------------------------------------- /CODE_OF_CONDUCT.md: -------------------------------------------------------------------------------- 1 | # Code of Conduct 2 | 3 | All Google open source projects are covered by our [community guidelines](https://opensource.google/conduct/) which define the kind of respectful behavior we expect of all participants. 4 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # How to Contribute 2 | 3 | We'd love to accept your patches and contributions to this project. There are 4 | just a few small guidelines you need to follow. 5 | 6 | ## Contributor License Agreement 7 | 8 | Contributions to this project must be accompanied by a Contributor License 9 | Agreement. You (or your employer) retain the copyright to your contribution; 10 | this simply gives us permission to use and redistribute your contributions as 11 | part of the project. Head over to to see 12 | your current agreements on file or to sign a new one. 13 | 14 | You generally only need to submit a CLA once, so if you've already submitted one 15 | (even if it was for a different project), you probably don't need to do it 16 | again. 17 | 18 | ## Code reviews 19 | 20 | All submissions, including submissions by project members, require review. We 21 | use GitHub pull requests for this purpose. Consult 22 | [GitHub Help](https://help.github.com/articles/about-pull-requests/) for more 23 | information on using pull requests. 24 | 25 | ## Community Guidelines 26 | 27 | This project follows 28 | [Google's Open Source Community Guidelines](https://opensource.google/conduct/). 29 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Apache License 2 | Version 2.0, January 2004 3 | http://www.apache.org/licenses/ 4 | 5 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 6 | 7 | 1. Definitions. 8 | 9 | "License" shall mean the terms and conditions for use, reproduction, 10 | and distribution as defined by Sections 1 through 9 of this document. 11 | 12 | "Licensor" shall mean the copyright owner or entity authorized by 13 | the copyright owner that is granting the License. 14 | 15 | "Legal Entity" shall mean the union of the acting entity and all 16 | other entities that control, are controlled by, or are under common 17 | control with that entity. For the purposes of this definition, 18 | "control" means (i) the power, direct or indirect, to cause the 19 | direction or management of such entity, whether by contract or 20 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 21 | outstanding shares, or (iii) beneficial ownership of such entity. 22 | 23 | "You" (or "Your") shall mean an individual or Legal Entity 24 | exercising permissions granted by this License. 25 | 26 | "Source" form shall mean the preferred form for making modifications, 27 | including but not limited to software source code, documentation 28 | source, and configuration files. 29 | 30 | "Object" form shall mean any form resulting from mechanical 31 | transformation or translation of a Source form, including but 32 | not limited to compiled object code, generated documentation, 33 | and conversions to other media types. 34 | 35 | "Work" shall mean the work of authorship, whether in Source or 36 | Object form, made available under the License, as indicated by a 37 | copyright notice that is included in or attached to the work 38 | (an example is provided in the Appendix below). 39 | 40 | "Derivative Works" shall mean any work, whether in Source or Object 41 | form, that is based on (or derived from) the Work and for which the 42 | editorial revisions, annotations, elaborations, or other modifications 43 | represent, as a whole, an original work of authorship. For the purposes 44 | of this License, Derivative Works shall not include works that remain 45 | separable from, or merely link (or bind by name) to the interfaces of, 46 | the Work and Derivative Works thereof. 47 | 48 | "Contribution" shall mean any work of authorship, including 49 | the original version of the Work and any modifications or additions 50 | to that Work or Derivative Works thereof, that is intentionally 51 | submitted to Licensor for inclusion in the Work by the copyright owner 52 | or by an individual or Legal Entity authorized to submit on behalf of 53 | the copyright owner. For the purposes of this definition, "submitted" 54 | means any form of electronic, verbal, or written communication sent 55 | to the Licensor or its representatives, including but not limited to 56 | communication on electronic mailing lists, source code control systems, 57 | and issue tracking systems that are managed by, or on behalf of, the 58 | Licensor for the purpose of discussing and improving the Work, but 59 | excluding communication that is conspicuously marked or otherwise 60 | designated in writing by the copyright owner as "Not a Contribution." 61 | 62 | "Contributor" shall mean Licensor and any individual or Legal Entity 63 | on behalf of whom a Contribution has been received by Licensor and 64 | subsequently incorporated within the Work. 65 | 66 | 2. Grant of Copyright License. Subject to the terms and conditions of 67 | this License, each Contributor hereby grants to You a perpetual, 68 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 69 | copyright license to reproduce, prepare Derivative Works of, 70 | publicly display, publicly perform, sublicense, and distribute the 71 | Work and such Derivative Works in Source or Object form. 72 | 73 | 3. Grant of Patent License. Subject to the terms and conditions of 74 | this License, each Contributor hereby grants to You a perpetual, 75 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 76 | (except as stated in this section) patent license to make, have made, 77 | use, offer to sell, sell, import, and otherwise transfer the Work, 78 | where such license applies only to those patent claims licensable 79 | by such Contributor that are necessarily infringed by their 80 | Contribution(s) alone or by combination of their Contribution(s) 81 | with the Work to which such Contribution(s) was submitted. If You 82 | institute patent litigation against any entity (including a 83 | cross-claim or counterclaim in a lawsuit) alleging that the Work 84 | or a Contribution incorporated within the Work constitutes direct 85 | or contributory patent infringement, then any patent licenses 86 | granted to You under this License for that Work shall terminate 87 | as of the date such litigation is filed. 88 | 89 | 4. Redistribution. You may reproduce and distribute copies of the 90 | Work or Derivative Works thereof in any medium, with or without 91 | modifications, and in Source or Object form, provided that You 92 | meet the following conditions: 93 | 94 | (a) You must give any other recipients of the Work or 95 | Derivative Works a copy of this License; and 96 | 97 | (b) You must cause any modified files to carry prominent notices 98 | stating that You changed the files; and 99 | 100 | (c) You must retain, in the Source form of any Derivative Works 101 | that You distribute, all copyright, patent, trademark, and 102 | attribution notices from the Source form of the Work, 103 | excluding those notices that do not pertain to any part of 104 | the Derivative Works; and 105 | 106 | (d) If the Work includes a "NOTICE" text file as part of its 107 | distribution, then any Derivative Works that You distribute must 108 | include a readable copy of the attribution notices contained 109 | within such NOTICE file, excluding those notices that do not 110 | pertain to any part of the Derivative Works, in at least one 111 | of the following places: within a NOTICE text file distributed 112 | as part of the Derivative Works; within the Source form or 113 | documentation, if provided along with the Derivative Works; or, 114 | within a display generated by the Derivative Works, if and 115 | wherever such third-party notices normally appear. The contents 116 | of the NOTICE file are for informational purposes only and 117 | do not modify the License. You may add Your own attribution 118 | notices within Derivative Works that You distribute, alongside 119 | or as an addendum to the NOTICE text from the Work, provided 120 | that such additional attribution notices cannot be construed 121 | as modifying the License. 122 | 123 | You may add Your own copyright statement to Your modifications and 124 | may provide additional or different license terms and conditions 125 | for use, reproduction, or distribution of Your modifications, or 126 | for any such Derivative Works as a whole, provided Your use, 127 | reproduction, and distribution of the Work otherwise complies with 128 | the conditions stated in this License. 129 | 130 | 5. Submission of Contributions. Unless You explicitly state otherwise, 131 | any Contribution intentionally submitted for inclusion in the Work 132 | by You to the Licensor shall be under the terms and conditions of 133 | this License, without any additional terms or conditions. 134 | Notwithstanding the above, nothing herein shall supersede or modify 135 | the terms of any separate license agreement you may have executed 136 | with Licensor regarding such Contributions. 137 | 138 | 6. Trademarks. This License does not grant permission to use the trade 139 | names, trademarks, service marks, or product names of the Licensor, 140 | except as required for reasonable and customary use in describing the 141 | origin of the Work and reproducing the content of the NOTICE file. 142 | 143 | 7. Disclaimer of Warranty. Unless required by applicable law or 144 | agreed to in writing, Licensor provides the Work (and each 145 | Contributor provides its Contributions) on an "AS IS" BASIS, 146 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 147 | implied, including, without limitation, any warranties or conditions 148 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 149 | PARTICULAR PURPOSE. You are solely responsible for determining the 150 | appropriateness of using or redistributing the Work and assume any 151 | risks associated with Your exercise of permissions under this License. 152 | 153 | 8. Limitation of Liability. In no event and under no legal theory, 154 | whether in tort (including negligence), contract, or otherwise, 155 | unless required by applicable law (such as deliberate and grossly 156 | negligent acts) or agreed to in writing, shall any Contributor be 157 | liable to You for damages, including any direct, indirect, special, 158 | incidental, or consequential damages of any character arising as a 159 | result of this License or out of the use or inability to use the 160 | Work (including but not limited to damages for loss of goodwill, 161 | work stoppage, computer failure or malfunction, or any and all 162 | other commercial damages or losses), even if such Contributor 163 | has been advised of the possibility of such damages. 164 | 165 | 9. Accepting Warranty or Additional Liability. While redistributing 166 | the Work or Derivative Works thereof, You may choose to offer, 167 | and charge a fee for, acceptance of support, warranty, indemnity, 168 | or other liability obligations and/or rights consistent with this 169 | License. However, in accepting such obligations, You may act only 170 | on Your own behalf and on Your sole responsibility, not on behalf 171 | of any other Contributor, and only if You agree to indemnify, 172 | defend, and hold each Contributor harmless for any liability 173 | incurred by, or claims asserted against, such Contributor by reason 174 | of your accepting any such warranty or additional liability. 175 | 176 | END OF TERMS AND CONDITIONS 177 | 178 | APPENDIX: How to apply the Apache License to your work. 179 | 180 | To apply the Apache License to your work, attach the following 181 | boilerplate notice, with the fields enclosed by brackets "[]" 182 | replaced with your own identifying information. (Don't include 183 | the brackets!) The text should be enclosed in the appropriate 184 | comment syntax for the file format. We also recommend that a 185 | file or class name and description of purpose be included on the 186 | same "printed page" as the copyright notice for easier 187 | identification within third-party archives. 188 | 189 | Copyright [yyyy] [name of copyright owner] 190 | 191 | Licensed under the Apache License, Version 2.0 (the "License"); 192 | you may not use this file except in compliance with the License. 193 | You may obtain a copy of the License at 194 | 195 | http://www.apache.org/licenses/LICENSE-2.0 196 | 197 | Unless required by applicable law or agreed to in writing, software 198 | distributed under the License is distributed on an "AS IS" BASIS, 199 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 200 | See the License for the specific language governing permissions and 201 | limitations under the License. 202 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Browser-FS-Access 2 | 3 | This module allows you to easily use the 4 | [File System Access API](https://wicg.github.io/file-system-access/) on supporting browsers, 5 | with a transparent fallback to the `` and `` legacy methods. 6 | This library is a [ponyfill](https://ponyfill.com/). 7 | 8 | Read more on the background of this module in my post 9 | [Progressive Enhancement In the Age of Fugu APIs](https://blog.tomayac.com/2020/01/23/progressive-enhancement-in-the-age-of-fugu-apis/). 10 | 11 | ## Live Demo 12 | 13 | Try the library in your browser: https://googlechromelabs.github.io/browser-fs-access/demo/. 14 | 15 | ## Installation 16 | 17 | You can install the module with npm. 18 | 19 | ```bash 20 | npm install --save browser-fs-access 21 | ``` 22 | 23 | ## Usage Examples 24 | 25 | The module feature-detects support for the File System Access API and 26 | only loads the actually relevant code. 27 | 28 | ### Importing what you need 29 | 30 | Import only the features that you need. In the code sample below, all 31 | features are loaded. The imported methods will use the File System 32 | Access API or a fallback implementation. 33 | 34 | ```js 35 | import { 36 | fileOpen, 37 | directoryOpen, 38 | fileSave, 39 | supported, 40 | } from 'https://unpkg.com/browser-fs-access'; 41 | ``` 42 | 43 | ### Feature detection 44 | 45 | You can check `supported` to see if the File System Access API is 46 | supported. 47 | 48 | ```js 49 | if (supported) { 50 | console.log('Using the File System Access API.'); 51 | } else { 52 | console.log('Using the fallback implementation.'); 53 | } 54 | ``` 55 | 56 | ### Opening a file 57 | 58 | ```js 59 | const blob = await fileOpen({ 60 | mimeTypes: ['image/*'], 61 | }); 62 | ``` 63 | 64 | ### Opening multiple files 65 | 66 | ```js 67 | const blobs = await fileOpen({ 68 | mimeTypes: ['image/*'], 69 | multiple: true, 70 | }); 71 | ``` 72 | 73 | ### Opening files of different MIME types 74 | 75 | ```js 76 | const blobs = await fileOpen([ 77 | { 78 | description: 'Image files', 79 | mimeTypes: ['image/jpg', 'image/png', 'image/gif', 'image/webp'], 80 | extensions: ['.jpg', '.jpeg', '.png', '.gif', '.webp'], 81 | multiple: true, 82 | }, 83 | { 84 | description: 'Text files', 85 | mimeTypes: ['text/*'], 86 | extensions: ['.txt'], 87 | }, 88 | ]); 89 | ``` 90 | 91 | ### Opening all files in a directory 92 | 93 | Optionally, you can recursively include subdirectories. 94 | 95 | ```js 96 | const blobsInDirectory = await directoryOpen({ 97 | recursive: true, 98 | }); 99 | ``` 100 | 101 | ### Saving a file 102 | 103 | ```js 104 | await fileSave(blob, { 105 | fileName: 'Untitled.png', 106 | extensions: ['.png'], 107 | }); 108 | ``` 109 | 110 | ### Saving a `Response` that will be streamed 111 | 112 | ```js 113 | const response = await fetch('foo.png'); 114 | await fileSave(response, { 115 | fileName: 'foo.png', 116 | extensions: ['.png'], 117 | }); 118 | ``` 119 | 120 | ### Saving a `Promise` that will be streamed. 121 | 122 | No need to `await` the `Blob` to be created. 123 | 124 | ```js 125 | const blob = createBlobAsyncWhichMightTakeLonger(someData); 126 | await fileSave(blob, { 127 | fileName: 'Untitled.png', 128 | extensions: ['.png'], 129 | }); 130 | ``` 131 | 132 | ## API Documentation 133 | 134 | ### Opening files: 135 | 136 | ```js 137 | // Options are optional. You can pass an array of options, too. 138 | const options = { 139 | // List of allowed MIME types, defaults to `*/*`. 140 | mimeTypes: ['image/*'], 141 | // List of allowed file extensions (with leading '.'), defaults to `''`. 142 | extensions: ['.png', '.jpg', '.jpeg', '.webp'], 143 | // Set to `true` for allowing multiple files, defaults to `false`. 144 | multiple: true, 145 | // Textual description for file dialog , defaults to `''`. 146 | description: 'Image files', 147 | // Suggested directory in which the file picker opens. A well-known directory, or a file or directory handle. 148 | startIn: 'downloads', 149 | // By specifying an ID, the user agent can remember different directories for different IDs. 150 | id: 'projects', 151 | // Include an option to not apply any filter in the file picker, defaults to `false`. 152 | excludeAcceptAllOption: true, 153 | }; 154 | 155 | const blobs = await fileOpen(options); 156 | ``` 157 | 158 | ### Opening directories: 159 | 160 | ```js 161 | // Options are optional. 162 | const options = { 163 | // Set to `true` to recursively open files in all subdirectories, defaults to `false`. 164 | recursive: true, 165 | // Open the directory with `"read"` or `"readwrite"` permission, defaults to `"read"`. 166 | mode: 167 | // Suggested directory in which the file picker opens. A well-known directory, or a file or directory handle. 168 | startIn: 'downloads', 169 | // By specifying an ID, the user agent can remember different directories for different IDs. 170 | id: 'projects', 171 | // Callback to determine whether a directory should be entered, return `true` to skip. 172 | skipDirectory: (entry) => entry.name[0] === '.', 173 | }; 174 | 175 | const blobs = await directoryOpen(options); 176 | ``` 177 | 178 | The module also polyfills a [`webkitRelativePath`](https://developer.mozilla.org/en-US/docs/Web/API/File/webkitRelativePath) property on returned files in a consistent way, regardless of the underlying implementation. 179 | 180 | ### Saving files: 181 | 182 | ```js 183 | // Options are optional. You can pass an array of options, too. 184 | const options = { 185 | // Suggested file name to use, defaults to `''`. 186 | fileName: 'Untitled.txt', 187 | // Suggested file extensions (with leading '.'), defaults to `''`. 188 | extensions: ['.txt'], 189 | // Suggested directory in which the file picker opens. A well-known directory, or a file or directory handle. 190 | startIn: 'downloads', 191 | // By specifying an ID, the user agent can remember different directories for different IDs. 192 | id: 'projects', 193 | // Include an option to not apply any filter in the file picker, defaults to `false`. 194 | excludeAcceptAllOption: true, 195 | }; 196 | 197 | // Optional file handle to save back to an existing file. 198 | // This will only work with the File System Access API. 199 | // Get a `FileHandle` from the `handle` property of the `Blob` 200 | // you receive from `fileOpen()` (this is non-standard). 201 | const existingHandle = previouslyOpenedBlob.handle; 202 | 203 | // Optional flag to determine whether to throw (rather than open a new file 204 | // save dialog) when `existingHandle` is no longer good, for example, because 205 | // the underlying file was deleted. Defaults to `false`. 206 | const throwIfExistingHandleNotGood = true; 207 | 208 | // `blobOrPromiseBlobOrResponse` is a `Blob`, a `Promise`, or a `Response`. 209 | await fileSave( 210 | blobOrResponseOrPromiseBlob, 211 | options, 212 | existingHandle, 213 | throwIfExistingHandleNotGood 214 | ); 215 | ``` 216 | 217 | ### File operations and exceptions 218 | 219 | The File System Access API supports exceptions, so apps can throw when problems occur (permissions 220 | not granted, out of disk space,…), or when the user cancels the dialog. The legacy save method, 221 | unfortunately, doesn't support exceptions. If your app depends on exceptions, see the file 222 | [`index.d.ts`](https://github.com/GoogleChromeLabs/browser-fs-access/blob/main/index.d.ts) for the 223 | documentation of the `legacySetup` parameter. 224 | 225 | ## Browser-FS-Access in Action 226 | 227 | You can see the module in action in the [Excalidraw](https://excalidraw.com/) drawing app. 228 | 229 | ![excalidraw](https://user-images.githubusercontent.com/145676/73060246-b4a64200-3e97-11ea-8f70-fa5edd63f78e.png) 230 | 231 | It also powers the [SVGcode](https://svgco.de/) app that converts raster images to SVGs. 232 | 233 | ![svgcode](https://github.com/tomayac/SVGcode/raw/main/public/screenshots/desktop.png) 234 | 235 | ## Alternatives 236 | 237 | A similar, but more extensive library called 238 | [native-file-system-adapter](https://github.com/jimmywarting/native-file-system-adapter/) 239 | is provided by [@jimmywarting](https://github.com/jimmywarting). 240 | 241 | ## Ecosystem 242 | 243 | If you are looking for a similar solution for dragging and dropping of files, 244 | check out [@placemarkio/flat-drop-files](https://github.com/placemark/flat-drop-files). 245 | 246 | ## Acknowledgements 247 | 248 | Thanks to [@developit](https://github.com/developit) 249 | for improving the dynamic module loading 250 | and [@dwelle](https://github.com/dwelle) for the helpful feedback, 251 | issue reports, and the Windows build fix. 252 | Directory operations were made consistent regarding `webkitRelativePath` 253 | and parallelized and sped up significantly by 254 | [@RReverser](https://github.com/RReverser). 255 | The TypeScript type annotations were initially provided by 256 | [@nanaian](https://github.com/nanaian). 257 | Dealing correctly with cross-origin iframes was contributed by 258 | [@nikhilbghodke](https://github.com/nikhilbghodke) and 259 | [@kbariotis](https://github.com/kbariotis). 260 | The exception handling of the legacy methods was contributed by 261 | [@jmrog](https://github.com/jmrog). 262 | The streams and blob saving was improved by [@tmcw](https://github.com/tmcw). 263 | 264 | ## License and Note 265 | 266 | Apache 2.0. 267 | 268 | This is not an official Google product. 269 | -------------------------------------------------------------------------------- /demo/floppy.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/GoogleChromeLabs/browser-fs-access/669637fdf59c4a39bb0721a547b6de739e8943f5/demo/floppy.png -------------------------------------------------------------------------------- /demo/image-to-blob.mjs: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright 2020 Google LLC 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | // @license © 2020 Google LLC. Licensed under the Apache License, Version 2.0. 17 | 18 | /** 19 | * Converts an image to a Blob. 20 | * @param {HTMLImageElement} img - Image element. 21 | * @return {Blob} Resulting Blob. 22 | */ 23 | const imageToBlob = async (img) => { 24 | return new Promise((resolve) => { 25 | const canvas = document.createElement('canvas'); 26 | canvas.width = img.naturalWidth; 27 | canvas.height = img.naturalHeight; 28 | const ctx = canvas.getContext('2d'); 29 | ctx.drawImage(img, 0, 0); 30 | canvas.toBlob((blob) => { 31 | resolve(blob); 32 | }); 33 | }); 34 | }; 35 | 36 | export { imageToBlob }; 37 | -------------------------------------------------------------------------------- /demo/index.html: -------------------------------------------------------------------------------- 1 | 16 | 17 | 18 | 19 | Browser-FS-Access.js Demo 20 | 21 | 22 | 23 | 24 | 25 | 26 |

Browser-FS-Access.js Demo

27 |

28 | Floppy disk 29 | 30 | 31 | 34 | 35 | 36 | 37 | 40 |

41 | Powered by 42 | browser-fs-access. 45 |

46 |

47 |     
48 |

In same-origin iframe

49 |

50 | If above it says "Using the File System Access API", then it should say 51 | so in the iframe. 52 |

53 | 54 |

In cross-origin iframe

55 |

56 | Cross-origin iframes cannot use the File System Access API, so it uses 57 | the fallback. 58 |

59 | 62 |
63 | 64 | 65 | -------------------------------------------------------------------------------- /demo/script.mjs: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright 2020 Google LLC 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | import { fileOpen, directoryOpen, fileSave, supported } from '../src/index.js'; 18 | 19 | import { imageToBlob } from './image-to-blob.mjs'; 20 | 21 | (async () => { 22 | const openButton = document.querySelector('#open'); 23 | const openMultipleButton = document.querySelector('#open-multiple'); 24 | const openImageOrTextButton = document.querySelector('#open-image-or-text'); 25 | const openDirectoryButton = document.querySelector('#open-directory'); 26 | const saveButton = document.querySelector('#save'); 27 | const saveBlobButton = document.querySelector('#save-blob'); 28 | const saveResponseButton = document.querySelector('#save-response'); 29 | const supportedParagraph = document.querySelector('.supported'); 30 | const pre = document.querySelector('pre'); 31 | 32 | const ABORT_MESSAGE = 'The user aborted a request.'; 33 | 34 | if (supported) { 35 | supportedParagraph.textContent = 'Using the File System Access API.'; 36 | } else { 37 | supportedParagraph.textContent = 'Using the fallback implementation.'; 38 | } 39 | 40 | const appendImage = (blob) => { 41 | const img = document.createElement('img'); 42 | img.src = URL.createObjectURL(blob); 43 | document.body.append(img); 44 | img.onload = img.onerror = () => URL.revokeObjectURL(img.src); 45 | }; 46 | 47 | const listDirectory = (blobs) => { 48 | let fileStructure = ''; 49 | if (blobs.length && !(blobs[0] instanceof File)) { 50 | return (pre.textContent += 'No files in directory.\n'); 51 | } 52 | blobs 53 | .sort((a, b) => a.webkitRelativePath.localeCompare(b)) 54 | .forEach((blob) => { 55 | // The File System Access API currently reports the `webkitRelativePath` 56 | // as empty string `''`. 57 | fileStructure += `${blob.webkitRelativePath}\n`; 58 | }); 59 | pre.textContent += fileStructure; 60 | 61 | blobs 62 | .filter((blob) => { 63 | return blob.type.startsWith('image/'); 64 | }) 65 | .forEach((blob) => { 66 | appendImage(blob); 67 | }); 68 | }; 69 | 70 | openButton.addEventListener('click', async () => { 71 | try { 72 | const blob = await fileOpen({ 73 | description: 'Image files', 74 | mimeTypes: ['image/jpg', 'image/png', 'image/gif', 'image/webp'], 75 | extensions: ['.jpg', '.jpeg', '.png', '.gif', '.webp'], 76 | }); 77 | appendImage(blob); 78 | } catch (err) { 79 | if (err.name !== 'AbortError') { 80 | return console.error(err); 81 | } 82 | console.log(ABORT_MESSAGE); 83 | } 84 | }); 85 | 86 | openMultipleButton.addEventListener('click', async () => { 87 | try { 88 | const blobs = await fileOpen({ 89 | description: 'Image files', 90 | mimeTypes: ['image/jpg', 'image/png', 'image/gif', 'image/webp'], 91 | extensions: ['.jpg', '.jpeg', '.png', '.gif', '.webp'], 92 | multiple: true, 93 | }); 94 | for (const blob of blobs) { 95 | appendImage(blob); 96 | } 97 | } catch (err) { 98 | if (err.name !== 'AbortError') { 99 | return console.error(err); 100 | } 101 | console.log(ABORT_MESSAGE); 102 | } 103 | }); 104 | 105 | openImageOrTextButton.addEventListener('click', async () => { 106 | try { 107 | const blobs = await fileOpen([ 108 | { 109 | description: 'Image files', 110 | mimeTypes: ['image/jpg', 'image/png', 'image/gif', 'image/webp'], 111 | extensions: ['.jpg', '.jpeg', '.png', '.gif', '.webp'], 112 | multiple: true, 113 | }, 114 | { 115 | description: 'Text files', 116 | mimeTypes: ['text/*'], 117 | extensions: ['.txt'], 118 | }, 119 | ]); 120 | for (const blob of blobs) { 121 | if (blob.type.startsWith('image/')) { 122 | appendImage(blob); 123 | } else { 124 | document.body.append(await blob.text()); 125 | } 126 | } 127 | } catch (err) { 128 | if (err.name !== 'AbortError') { 129 | return console.error(err); 130 | } 131 | console.log(ABORT_MESSAGE); 132 | } 133 | }); 134 | 135 | openDirectoryButton.addEventListener('click', async () => { 136 | try { 137 | const blobs = await directoryOpen({ 138 | recursive: true, 139 | }); 140 | listDirectory(blobs); 141 | } catch (err) { 142 | if (err.name !== 'AbortError') { 143 | return console.error(err); 144 | } 145 | console.log(ABORT_MESSAGE); 146 | } 147 | }); 148 | 149 | saveButton.addEventListener('click', async () => { 150 | const blob = await imageToBlob(document.querySelector('img')); 151 | try { 152 | await fileSave(blob, { 153 | fileName: 'floppy.png', 154 | extensions: ['.png'], 155 | }); 156 | } catch (err) { 157 | if (err.name !== 'AbortError') { 158 | return console.error(err); 159 | } 160 | console.log(ABORT_MESSAGE); 161 | } 162 | }); 163 | 164 | saveBlobButton.addEventListener('click', async () => { 165 | const blob = imageToBlob(document.querySelector('img')); 166 | try { 167 | await fileSave(blob, { 168 | fileName: 'floppy-blob.png', 169 | extensions: ['.png'], 170 | }); 171 | } catch (err) { 172 | if (err.name !== 'AbortError') { 173 | return console.error(err); 174 | } 175 | console.log(ABORT_MESSAGE); 176 | } 177 | }); 178 | 179 | saveResponseButton.addEventListener('click', async () => { 180 | const response = await fetch('./floppy.png'); 181 | try { 182 | await fileSave(response, { 183 | fileName: 'floppy-response.png', 184 | extensions: ['.png'], 185 | }); 186 | } catch (err) { 187 | if (err.name !== 'AbortError') { 188 | return console.error(err); 189 | } 190 | console.log(ABORT_MESSAGE); 191 | } 192 | }); 193 | 194 | openButton.disabled = false; 195 | openMultipleButton.disabled = false; 196 | openImageOrTextButton.disabled = false; 197 | openDirectoryButton.disabled = false; 198 | saveButton.disabled = false; 199 | saveBlobButton.disabled = false; 200 | saveResponseButton.disabled = false; 201 | })(); 202 | 203 | if (window.self !== window.top) { 204 | document.querySelector('.iframes').remove(); 205 | } 206 | -------------------------------------------------------------------------------- /demo/style.css: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright 2020 Google LLC 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | body { 18 | font-family: helvetica, arial, sans-serif; 19 | margin: 2em; 20 | } 21 | 22 | h1 { 23 | font-style: italic; 24 | color: #373fff; 25 | } 26 | 27 | img { 28 | display: block; 29 | max-width: 100%; 30 | height: auto; 31 | margin-block: 1rem; 32 | } 33 | 34 | code { 35 | font-family: ui-monospace, monospace; 36 | } 37 | 38 | .supported { 39 | color: green; 40 | } 41 | 42 | iframe { 43 | width: 100%; 44 | height: 400px; 45 | border: solid 1px #000; 46 | } 47 | -------------------------------------------------------------------------------- /eslint.config.mjs: -------------------------------------------------------------------------------- 1 | // eslint.config.js 2 | import prettierConfig from 'eslint-config-prettier'; 3 | 4 | export default [ 5 | { 6 | files: ['**/*.js', '**/*.mjs'], 7 | languageOptions: { 8 | ecmaVersion: 2020, 9 | sourceType: 'module', 10 | }, 11 | rules: { 12 | quotes: ['error', 'single'], 13 | semi: ['error', 'always'], 14 | indent: ['error', 2], 15 | 'no-var': 'error', 16 | 'prefer-const': 'error', 17 | 'comma-dangle': ['error', 'never'], 18 | 'require-jsdoc': 'off', 19 | 'valid-jsdoc': 'off', 20 | ...prettierConfig.rules, 21 | }, 22 | }, 23 | ]; 24 | -------------------------------------------------------------------------------- /index.d.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Properties shared by all `options` provided to file save and open operations 3 | */ 4 | export interface CoreFileOptions { 5 | /** Acceptable file extensions. Defaults to `[""]`. */ 6 | extensions?: string[]; 7 | /** Suggested file description. Defaults to `""`. */ 8 | description?: string; 9 | /** Acceptable MIME types. Defaults to `[]`. */ 10 | mimeTypes?: string[]; 11 | } 12 | 13 | /** 14 | * Properties shared by the _first_ `options` object provided to file save and 15 | * open operations (any additional options objects provided to those operations 16 | * cannot have these properties) 17 | */ 18 | export interface FirstCoreFileOptions extends CoreFileOptions { 19 | startIn?: WellKnownDirectory | FileSystemHandle; 20 | /** By specifying an ID, the user agent can remember different directories for different IDs. */ 21 | id?: string; 22 | excludeAcceptAllOption?: boolean | false; 23 | } 24 | 25 | /** 26 | * The first `options` object passed to file save operations can also specify 27 | * a filename 28 | */ 29 | export interface FirstFileSaveOptions extends FirstCoreFileOptions { 30 | /** Suggested file name. Defaults to `"Untitled"`. */ 31 | fileName?: string; 32 | /** 33 | * Configurable cleanup and `Promise` rejector usable with legacy API for 34 | * determining when (and reacting if) a user cancels the operation. The 35 | * method will be passed a reference to the internal `rejectionHandler` that 36 | * can, e.g., be attached to/removed from the window or called after a 37 | * timeout. The method should return a function that will be called when 38 | * either the user chooses to open a file or the `rejectionHandler` is 39 | * called. In the latter case, the returned function will also be passed a 40 | * reference to the `reject` callback for the `Promise` returned by 41 | * `fileOpen`, so that developers may reject the `Promise` when desired at 42 | * that time. 43 | * Example rejector: 44 | * 45 | * const file = await fileOpen({ 46 | * legacySetup: (rejectionHandler) => { 47 | * const timeoutId = setTimeout(rejectionHandler, 10_000); 48 | * return (reject) => { 49 | * clearTimeout(timeoutId); 50 | * if (reject) { 51 | * reject('My error message here.'); 52 | * } 53 | * }; 54 | * }, 55 | * }); 56 | */ 57 | legacySetup?: ( 58 | resolve: () => void, 59 | rejectionHandler: () => void, 60 | anchor: HTMLAnchorElement 61 | ) => () => void; 62 | } 63 | 64 | /** 65 | * The first `options` object passed to file open operations can specify 66 | * whether multiple files can be selected (the return type of the operation 67 | * will be updated appropriately). 68 | */ 69 | export interface FirstFileOpenOptions 70 | extends FirstCoreFileOptions { 71 | /** Allow multiple files to be selected. Defaults to `false`. */ 72 | multiple?: M; 73 | } 74 | 75 | /** 76 | * Opens file(s) from disk. 77 | */ 78 | export function fileOpen( 79 | options?: 80 | | [FirstFileOpenOptions, ...CoreFileOptions[]] 81 | | FirstFileOpenOptions 82 | ): M extends false | undefined 83 | ? Promise 84 | : Promise; 85 | 86 | export type WellKnownDirectory = 87 | | 'desktop' 88 | | 'documents' 89 | | 'downloads' 90 | | 'music' 91 | | 'pictures' 92 | | 'videos'; 93 | 94 | export type FileSystemPermissionMode = 'read' | 'readwrite'; 95 | 96 | /** 97 | * Saves a file to disk. 98 | * @returns Optional file handle to save in place. 99 | */ 100 | export function fileSave( 101 | /** To-be-saved `Blob` or `Response` */ 102 | blobOrPromiseBlobOrResponse: Blob | Promise | Response, 103 | options?: [FirstFileSaveOptions, ...CoreFileOptions[]] | FirstFileSaveOptions, 104 | /** 105 | * A potentially existing file handle for a file to save to. Defaults to 106 | * `null`. 107 | */ 108 | existingHandle?: FileSystemFileHandle | null, 109 | /** 110 | * Determines whether to throw (rather than open a new file save dialog) 111 | * when `existingHandle` is no longer good. Defaults to `false`. 112 | */ 113 | throwIfExistingHandleNotGood?: boolean | false, 114 | /** 115 | * A callback to be called when the file picker was shown (which only happens 116 | * when no `existingHandle` is provided). Defaults to `null`. 117 | */ 118 | filePickerShown?: (handle: FileSystemFileHandle | null) => void | null 119 | ): Promise; 120 | 121 | /** 122 | * Opens a directory from disk using the File System Access API. 123 | * @returns Contained files. 124 | */ 125 | export function directoryOpen(options?: { 126 | /** Whether to recursively get subdirectories. */ 127 | recursive: boolean; 128 | /** Suggested directory in which the file picker opens. */ 129 | startIn?: WellKnownDirectory | FileSystemHandle; 130 | /** By specifying an ID, the user agent can remember different directories for different IDs. */ 131 | id?: string; 132 | /** By specifying a mode of `'readwrite'`, you can open a directory with write access. */ 133 | mode?: FileSystemPermissionMode; 134 | /** Callback to determine whether a directory should be entered, return `true` to skip. */ 135 | skipDirectory?: ( 136 | entry: FileSystemDirectoryEntry | FileSystemDirectoryHandle 137 | ) => boolean; 138 | }): Promise; 139 | 140 | /** 141 | * Whether the File System Access API is supported. 142 | */ 143 | export const supported: boolean; 144 | 145 | export function imageToBlob(img: HTMLImageElement): Promise; 146 | 147 | export interface FileWithHandle extends File { 148 | handle?: FileSystemFileHandle; 149 | } 150 | 151 | export interface FileWithDirectoryAndFileHandle extends File { 152 | directoryHandle?: FileSystemDirectoryHandle; 153 | handle?: FileSystemFileHandle; 154 | } 155 | 156 | // The following typings implement the relevant parts of the File System Access 157 | // API. This can be removed once the specification reaches the Candidate phase 158 | // and is implemented as part of microsoft/TSJS-lib-generator. 159 | 160 | export interface FileSystemHandlePermissionDescriptor { 161 | mode?: 'read' | 'readwrite'; 162 | } 163 | 164 | export interface FileSystemHandle { 165 | readonly kind: 'file' | 'directory'; 166 | readonly name: string; 167 | 168 | isSameEntry: (other: FileSystemHandle) => Promise; 169 | 170 | queryPermission: ( 171 | descriptor?: FileSystemHandlePermissionDescriptor 172 | ) => Promise; 173 | requestPermission: ( 174 | descriptor?: FileSystemHandlePermissionDescriptor 175 | ) => Promise; 176 | } 177 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "browser-fs-access", 3 | "version": "0.37.0", 4 | "description": "File System Access API with legacy fallback in the browser.", 5 | "type": "module", 6 | "source": "./src/index.js", 7 | "exports": { 8 | ".": { 9 | "types": "./index.d.ts", 10 | "module": "./dist/index.modern.js", 11 | "import": "./dist/index.modern.js", 12 | "require": "./dist/index.cjs", 13 | "browser": "./dist/index.modern.js" 14 | }, 15 | "./package.json": "./package.json" 16 | }, 17 | "main": "./dist/index.cjs", 18 | "module": "./dist/index.modern.js", 19 | "types": "./index.d.ts", 20 | "files": [ 21 | "dist/", 22 | "index.d.ts" 23 | ], 24 | "scripts": { 25 | "start": "npx http-server -o /demo/", 26 | "clean": "shx rm -rf ./dist", 27 | "build": "npm run clean && microbundle -f modern,cjs --no-sourcemap --no-generateTypes", 28 | "dev": "microbundle watch", 29 | "prepare": "npm run lint && npm run fix && npm run build", 30 | "lint": "npx eslint . --ext .js,.mjs --fix --ignore-pattern dist/", 31 | "fix": "npx prettier --write ." 32 | }, 33 | "publishConfig": { 34 | "access": "public" 35 | }, 36 | "repository": { 37 | "type": "git", 38 | "url": "git+https://github.com/GoogleChromeLabs/browser-fs-access.git" 39 | }, 40 | "keywords": [ 41 | "file system access", 42 | "file system access api", 43 | "file system", 44 | "ponyfill" 45 | ], 46 | "author": "Thomas Steiner (https://blog.tomayac.com/)", 47 | "license": "Apache-2.0", 48 | "bugs": { 49 | "url": "https://github.com/GoogleChromeLabs/browser-fs-access/issues" 50 | }, 51 | "homepage": "https://github.com/GoogleChromeLabs/browser-fs-access#readme", 52 | "devDependencies": { 53 | "eslint": "^9.25.1", 54 | "eslint-config-google": "^0.14.0", 55 | "eslint-config-prettier": "^10.1.2", 56 | "http-server": "^14.1.1", 57 | "microbundle": "^0.15.1", 58 | "prettier": "^3.5.3", 59 | "shx": "^0.4.0" 60 | } 61 | } 62 | -------------------------------------------------------------------------------- /src/directory-open.mjs: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright 2020 Google LLC 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | // @license © 2020 Google LLC. Licensed under the Apache License, Version 2.0. 17 | 18 | import supported from './supported.mjs'; 19 | 20 | const implementation = supported 21 | ? import('./fs-access/directory-open.mjs') 22 | : import('./legacy/directory-open.mjs'); 23 | 24 | /** 25 | * For opening directories, dynamically either loads the File System Access API 26 | * module or the legacy method. 27 | */ 28 | export async function directoryOpen(...args) { 29 | return (await implementation).default(...args); 30 | } 31 | -------------------------------------------------------------------------------- /src/file-open.mjs: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright 2020 Google LLC 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | // @license © 2020 Google LLC. Licensed under the Apache License, Version 2.0. 17 | 18 | import supported from './supported.mjs'; 19 | 20 | const implementation = supported 21 | ? import('./fs-access/file-open.mjs') 22 | : import('./legacy/file-open.mjs'); 23 | 24 | /** 25 | * For opening files, dynamically either loads the File System Access API module 26 | * or the legacy method. 27 | */ 28 | export async function fileOpen(...args) { 29 | return (await implementation).default(...args); 30 | } 31 | -------------------------------------------------------------------------------- /src/file-save.mjs: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright 2020 Google LLC 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | // @license © 2020 Google LLC. Licensed under the Apache License, Version 2.0. 17 | 18 | import supported from './supported.mjs'; 19 | 20 | const implementation = supported 21 | ? import('./fs-access/file-save.mjs') 22 | : import('./legacy/file-save.mjs'); 23 | 24 | /** 25 | * For saving files, dynamically either loads the File System Access API module 26 | * or the legacy method. 27 | */ 28 | export async function fileSave(...args) { 29 | return (await implementation).default(...args); 30 | } 31 | -------------------------------------------------------------------------------- /src/fs-access/directory-open.mjs: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright 2020 Google LLC 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | // @license © 2020 Google LLC. Licensed under the Apache License, Version 2.0. 17 | 18 | const getFiles = async ( 19 | dirHandle, 20 | recursive, 21 | path = dirHandle.name, 22 | skipDirectory 23 | ) => { 24 | const dirs = []; 25 | const files = []; 26 | for await (const entry of dirHandle.values()) { 27 | const nestedPath = `${path}/${entry.name}`; 28 | if (entry.kind === 'file') { 29 | files.push( 30 | entry.getFile().then((file) => { 31 | file.directoryHandle = dirHandle; 32 | file.handle = entry; 33 | return Object.defineProperty(file, 'webkitRelativePath', { 34 | configurable: true, 35 | enumerable: true, 36 | get: () => nestedPath, 37 | }); 38 | }) 39 | ); 40 | } else if ( 41 | entry.kind === 'directory' && 42 | recursive && 43 | (!skipDirectory || !skipDirectory(entry)) 44 | ) { 45 | dirs.push(getFiles(entry, recursive, nestedPath, skipDirectory)); 46 | } 47 | } 48 | return [...(await Promise.all(dirs)).flat(), ...(await Promise.all(files))]; 49 | }; 50 | 51 | /** 52 | * Opens a directory from disk using the File System Access API. 53 | * @type { typeof import("../index").directoryOpen } 54 | */ 55 | export default async (options = {}) => { 56 | options.recursive = options.recursive || false; 57 | options.mode = options.mode || 'read'; 58 | const handle = await window.showDirectoryPicker({ 59 | id: options.id, 60 | startIn: options.startIn, 61 | mode: options.mode, 62 | }); 63 | // If the directory is empty, return an array with the handle. 64 | if ((await (await handle.values()).next()).done) { 65 | return [handle]; 66 | } 67 | // Else, return an array of File objects. 68 | return getFiles(handle, options.recursive, undefined, options.skipDirectory); 69 | }; 70 | -------------------------------------------------------------------------------- /src/fs-access/file-open.mjs: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright 2020 Google LLC 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | // @license © 2020 Google LLC. Licensed under the Apache License, Version 2.0. 17 | 18 | const getFileWithHandle = async (handle) => { 19 | const file = await handle.getFile(); 20 | file.handle = handle; 21 | return file; 22 | }; 23 | 24 | /** 25 | * Opens a file from disk using the File System Access API. 26 | * @type { typeof import("../../index").fileOpen } 27 | */ 28 | export default async (options = [{}]) => { 29 | if (!Array.isArray(options)) { 30 | options = [options]; 31 | } 32 | const types = []; 33 | options.forEach((option, i) => { 34 | types[i] = { 35 | description: option.description || 'Files', 36 | accept: {}, 37 | }; 38 | if (option.mimeTypes) { 39 | option.mimeTypes.map((mimeType) => { 40 | types[i].accept[mimeType] = option.extensions || []; 41 | }); 42 | } else { 43 | types[i].accept['*/*'] = option.extensions || []; 44 | } 45 | }); 46 | const handleOrHandles = await window.showOpenFilePicker({ 47 | id: options[0].id, 48 | startIn: options[0].startIn, 49 | types, 50 | multiple: options[0].multiple || false, 51 | excludeAcceptAllOption: options[0].excludeAcceptAllOption || false, 52 | }); 53 | const files = await Promise.all(handleOrHandles.map(getFileWithHandle)); 54 | if (options[0].multiple) { 55 | return files; 56 | } 57 | return files[0]; 58 | }; 59 | -------------------------------------------------------------------------------- /src/fs-access/file-save.mjs: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright 2020 Google LLC 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | // @license © 2020 Google LLC. Licensed under the Apache License, Version 2.0. 17 | 18 | /** 19 | * Saves a file to disk using the File System Access API. 20 | * @type { typeof import("../../index").fileSave } 21 | */ 22 | export default async ( 23 | blobOrPromiseBlobOrResponse, 24 | options = [{}], 25 | existingHandle = null, 26 | throwIfExistingHandleNotGood = false, 27 | filePickerShown = null 28 | ) => { 29 | if (!Array.isArray(options)) { 30 | options = [options]; 31 | } 32 | options[0].fileName = options[0].fileName || 'Untitled'; 33 | const types = []; 34 | let type = null; 35 | if ( 36 | blobOrPromiseBlobOrResponse instanceof Blob && 37 | blobOrPromiseBlobOrResponse.type 38 | ) { 39 | type = blobOrPromiseBlobOrResponse.type; 40 | } else if ( 41 | blobOrPromiseBlobOrResponse.headers && 42 | blobOrPromiseBlobOrResponse.headers.get('content-type') 43 | ) { 44 | type = blobOrPromiseBlobOrResponse.headers.get('content-type'); 45 | } 46 | options.forEach((option, i) => { 47 | types[i] = { 48 | description: option.description || 'Files', 49 | accept: {}, 50 | }; 51 | if (option.mimeTypes) { 52 | if (i === 0 && type) { 53 | option.mimeTypes.push(type); 54 | } 55 | option.mimeTypes.map((mimeType) => { 56 | types[i].accept[mimeType] = option.extensions || []; 57 | }); 58 | } else if (type) { 59 | types[i].accept[type] = option.extensions || []; 60 | } else { 61 | types[i].accept['*/*'] = option.extensions || []; 62 | } 63 | }); 64 | if (existingHandle) { 65 | try { 66 | // Check if the file still exists. 67 | await existingHandle.getFile(); 68 | } catch (err) { 69 | existingHandle = null; 70 | if (throwIfExistingHandleNotGood) { 71 | throw err; 72 | } 73 | } 74 | } 75 | const handle = 76 | existingHandle || 77 | (await window.showSaveFilePicker({ 78 | suggestedName: options[0].fileName, 79 | id: options[0].id, 80 | startIn: options[0].startIn, 81 | types, 82 | excludeAcceptAllOption: options[0].excludeAcceptAllOption || false, 83 | })); 84 | if (!existingHandle && filePickerShown) { 85 | filePickerShown(handle); 86 | } 87 | const writable = await handle.createWritable(); 88 | // Use streaming on the `Blob` if the browser supports it. 89 | if ('stream' in blobOrPromiseBlobOrResponse) { 90 | const stream = blobOrPromiseBlobOrResponse.stream(); 91 | await stream.pipeTo(writable); 92 | return handle; 93 | // Handle passed `ReadableStream`. 94 | } else if ('body' in blobOrPromiseBlobOrResponse) { 95 | await blobOrPromiseBlobOrResponse.body.pipeTo(writable); 96 | return handle; 97 | } 98 | // Default case of `Blob` passed and `Blob.stream()` not supported. 99 | await writable.write(await blobOrPromiseBlobOrResponse); 100 | await writable.close(); 101 | return handle; 102 | }; 103 | -------------------------------------------------------------------------------- /src/index.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright 2020 Google LLC 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | // @license © 2020 Google LLC. Licensed under the Apache License, Version 2.0. 17 | 18 | /** 19 | * @module browser-fs-access 20 | */ 21 | export { fileOpen } from './file-open.mjs'; 22 | export { directoryOpen } from './directory-open.mjs'; 23 | export { fileSave } from './file-save.mjs'; 24 | 25 | export { default as fileOpenModern } from './fs-access/file-open.mjs'; 26 | export { default as directoryOpenModern } from './fs-access/directory-open.mjs'; 27 | export { default as fileSaveModern } from './fs-access/file-save.mjs'; 28 | 29 | export { default as fileOpenLegacy } from './legacy/file-open.mjs'; 30 | export { default as directoryOpenLegacy } from './legacy/directory-open.mjs'; 31 | export { default as fileSaveLegacy } from './legacy/file-save.mjs'; 32 | 33 | export { default as supported } from './supported.mjs'; 34 | -------------------------------------------------------------------------------- /src/legacy/directory-open.mjs: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright 2020 Google LLC 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | // @license © 2020 Google LLC. Licensed under the Apache License, Version 2.0. 17 | 18 | /** 19 | * Opens a directory from disk using the legacy 20 | * `` method. 21 | * @type { typeof import("../index").directoryOpen } 22 | */ 23 | export default async (options = [{}]) => { 24 | if (!Array.isArray(options)) { 25 | options = [options]; 26 | } 27 | options[0].recursive = options[0].recursive || false; 28 | return new Promise((resolve, reject) => { 29 | const input = document.createElement('input'); 30 | input.type = 'file'; 31 | input.webkitdirectory = true; 32 | // Append to the DOM, else Safari on iOS won't fire the `change` event 33 | // reliably. 34 | input.style.display = 'none'; 35 | document.body.append(input); 36 | 37 | input.addEventListener('cancel', () => { 38 | input.remove(); 39 | reject(new DOMException('The user aborted a request.', 'AbortError')); 40 | }); 41 | 42 | input.addEventListener('change', () => { 43 | input.remove(); 44 | let files = Array.from(input.files); 45 | if (!options[0].recursive) { 46 | files = files.filter((file) => { 47 | return file.webkitRelativePath.split('/').length === 2; 48 | }); 49 | } else if (options[0].recursive && options[0].skipDirectory) { 50 | files = files.filter((file) => { 51 | const directoriesName = file.webkitRelativePath.split('/'); 52 | return directoriesName.every( 53 | (directoryName) => 54 | !options[0].skipDirectory({ 55 | name: directoryName, 56 | kind: 'directory', 57 | }) 58 | ); 59 | }); 60 | } 61 | 62 | resolve(files); 63 | }); 64 | if ('showPicker' in HTMLInputElement.prototype) { 65 | input.showPicker(); 66 | } else { 67 | input.click(); 68 | } 69 | }); 70 | }; 71 | -------------------------------------------------------------------------------- /src/legacy/file-open.mjs: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright 2020 Google LLC 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | // @license © 2020 Google LLC. Licensed under the Apache License, Version 2.0. 17 | 18 | /** 19 | * Opens a file from disk using the legacy `` method. 20 | * @type { typeof import("../index").fileOpen } 21 | */ 22 | export default async (options = [{}]) => { 23 | if (!Array.isArray(options)) { 24 | options = [options]; 25 | } 26 | return new Promise((resolve, reject) => { 27 | const input = document.createElement('input'); 28 | input.type = 'file'; 29 | const accept = [ 30 | ...options.map((option) => option.mimeTypes || []), 31 | ...options.map((option) => option.extensions || []), 32 | ].join(); 33 | input.multiple = options[0].multiple || false; 34 | // Empty string allows everything. 35 | input.accept = accept || ''; 36 | // Append to the DOM, else Safari on iOS won't fire the `change` event 37 | // reliably. 38 | input.style.display = 'none'; 39 | document.body.append(input); 40 | 41 | input.addEventListener('cancel', () => { 42 | input.remove(); 43 | reject(new DOMException('The user aborted a request.', 'AbortError')); 44 | }); 45 | 46 | input.addEventListener('change', () => { 47 | input.remove(); 48 | resolve(input.multiple ? Array.from(input.files) : input.files[0]); 49 | }); 50 | 51 | if ('showPicker' in HTMLInputElement.prototype) { 52 | input.showPicker(); 53 | } else { 54 | input.click(); 55 | } 56 | }); 57 | }; 58 | -------------------------------------------------------------------------------- /src/legacy/file-save.mjs: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright 2020 Google LLC 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | // @license © 2020 Google LLC. Licensed under the Apache License, Version 2.0. 17 | 18 | /** 19 | * Saves a file to disk using the legacy `` method. 20 | * @type { typeof import("../../index").fileSave } 21 | */ 22 | export default async (blobOrPromiseBlobOrResponse, options = {}) => { 23 | if (Array.isArray(options)) { 24 | options = options[0]; 25 | } 26 | const a = document.createElement('a'); 27 | let data = blobOrPromiseBlobOrResponse; 28 | // Handle the case where input is a `ReadableStream`. 29 | if ('body' in blobOrPromiseBlobOrResponse) { 30 | data = await streamToBlob( 31 | blobOrPromiseBlobOrResponse.body, 32 | blobOrPromiseBlobOrResponse.headers.get('content-type') 33 | ); 34 | } 35 | a.download = options.fileName || 'Untitled'; 36 | a.href = URL.createObjectURL(await data); 37 | 38 | const _reject = () => cleanupListenersAndMaybeReject(); 39 | const _resolve = () => { 40 | if (typeof cleanupListenersAndMaybeReject === 'function') { 41 | cleanupListenersAndMaybeReject(); 42 | } 43 | }; 44 | 45 | const cleanupListenersAndMaybeReject = 46 | options.legacySetup && options.legacySetup(_resolve, _reject, a); 47 | 48 | a.addEventListener('click', () => { 49 | // `setTimeout()` due to 50 | // https://github.com/LLK/scratch-gui/issues/1783#issuecomment-426286393 51 | setTimeout(() => URL.revokeObjectURL(a.href), 30 * 1000); 52 | _resolve(null); 53 | }); 54 | a.click(); 55 | return null; 56 | }; 57 | 58 | /** 59 | * Converts a passed `ReadableStream` to a `Blob`. 60 | * @param {ReadableStream} stream 61 | * @param {string} type 62 | * @returns {Promise} 63 | */ 64 | async function streamToBlob(stream, type) { 65 | const reader = stream.getReader(); 66 | const pumpedStream = new ReadableStream({ 67 | start(controller) { 68 | return pump(); 69 | /** 70 | * Recursively pumps data chunks out of the `ReadableStream`. 71 | * @type { () => Promise } 72 | */ 73 | async function pump() { 74 | return reader.read().then(({ done, value }) => { 75 | if (done) { 76 | controller.close(); 77 | return; 78 | } 79 | controller.enqueue(value); 80 | return pump(); 81 | }); 82 | } 83 | }, 84 | }); 85 | 86 | const res = new Response(pumpedStream); 87 | const blob = await res.blob(); 88 | reader.releaseLock(); 89 | return new Blob([blob], { type }); 90 | } 91 | -------------------------------------------------------------------------------- /src/supported.mjs: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright 2020 Google LLC 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | // @license © 2020 Google LLC. Licensed under the Apache License, Version 2.0. 17 | 18 | /** 19 | * Returns whether the File System Access API is supported and usable in the 20 | * current context (for example cross-origin iframes). 21 | * @returns {boolean} Returns `true` if the File System Access API is supported and usable, else returns `false`. 22 | */ 23 | const supported = (() => { 24 | // When running in an SSR environment return `false`. 25 | if (typeof self === 'undefined') { 26 | return false; 27 | } 28 | // ToDo: Remove this check once Permissions Policy integration 29 | // has happened, tracked in 30 | // https://github.com/WICG/file-system-access/issues/245. 31 | if ('top' in self && self !== top) { 32 | try { 33 | // This will succeed on same-origin iframes, 34 | // but fail on cross-origin iframes. 35 | // This is longer than necessary, as else the minifier removes it. 36 | top.window.document._ = 0; 37 | } catch { 38 | return false; 39 | } 40 | } 41 | if ('showOpenFilePicker' in self) { 42 | return true; 43 | } 44 | return false; 45 | })(); 46 | 47 | export default supported; 48 | --------------------------------------------------------------------------------