├── .prettierignore
├── .gitignore
├── demo
├── floppy.png
├── style.css
├── image-to-blob.mjs
├── index.html
└── script.mjs
├── CODE_OF_CONDUCT.md
├── .github
└── workflows
│ └── main.yml
├── .prettierrc
├── eslint.config.mjs
├── src
├── file-open.mjs
├── file-save.mjs
├── directory-open.mjs
├── index.js
├── supported.mjs
├── fs-access
│ ├── file-open.mjs
│ ├── directory-open.mjs
│ └── file-save.mjs
└── legacy
│ ├── file-open.mjs
│ ├── directory-open.mjs
│ └── file-save.mjs
├── CONTRIBUTING.md
├── package.json
├── index.d.ts
├── README.md
└── LICENSE
/.prettierignore:
--------------------------------------------------------------------------------
1 | dist/
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 |
2 | .DS_Store
3 | node_modules/
4 | dist/
5 |
6 |
--------------------------------------------------------------------------------
/demo/floppy.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/GoogleChromeLabs/browser-fs-access/main/demo/floppy.png
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/.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 |
--------------------------------------------------------------------------------
/.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 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/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/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 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/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/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 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "browser-fs-access",
3 | "version": "0.38.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.29.0",
54 | "eslint-config-google": "^0.14.0",
55 | "eslint-config-prettier": "^10.1.5",
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/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/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/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 |
--------------------------------------------------------------------------------
/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 |
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 |
--------------------------------------------------------------------------------
/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-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/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 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/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 | 
230 |
231 | It also powers the [SVGcode](https://svgco.de/) app that converts raster images to SVGs.
232 |
233 | 
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 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------