diff --git a/.gitignore b/.gitignore index 3c94717..12721f2 100644 --- a/.gitignore +++ b/.gitignore @@ -2,4 +2,4 @@ .DS_Store node_modules/ dist/ - +.idea diff --git a/README.md b/README.md index d13b61f..78b0dc4 100644 --- a/README.md +++ b/README.md @@ -167,6 +167,8 @@ const options = { startIn: 'downloads', // By specifying an ID, the user agent can remember different directories for different IDs. id: 'projects', + // Set to 'read' or 'readwrite'; the browser will prompt the user accordingly. + mode: 'read', // Callback to determine whether a directory should be entered, return `true` to skip. skipDirectory: (entry) => entry.name[0] === '.', }; @@ -176,6 +178,32 @@ const blobs = await directoryOpen(options); 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. +### Opening A Directory Hierarchy + +A variant of directoryOpen which is not backwards/legacy compatible, but includes +a handle referencing the directory being opened, as well as its contents. + +```js +// Options are optional. +const options = { + // Set to `true` to recursively open files in all subdirectories, + // defaults to `false`. + recursive: true, + // Suggested directory in which the file picker opens. A well-known directory, or a file or directory handle. + startIn: 'downloads', + // By specifying an ID, the user agent can remember different directories for different IDs. + id: 'projects', + // Set to 'read' or 'readwrite'; the browser will prompt the user accordingly. + mode: 'read', + // Callback to determine whether a directory should be entered, return `true` to skip. + skipDirectory: (entry) => entry.name[0] === '.', +}; + +const dirWithContents = fileHierarchy(options); +const dir = dirWithContents.currentDir; +const contents = await dirWithContents.contents; +``` + ### Saving files: ```js diff --git a/index.d.ts b/index.d.ts index 47fdba8..3f7894d 100644 --- a/index.d.ts +++ b/index.d.ts @@ -209,6 +209,64 @@ export function directoryOpen(options?: { ) => (reject?: (reason?: any) => void) => void; }): Promise; +/** + * Opens a directory from disk using the File System Access API. Includes a reference to the directory being listed + * (similar to the "." entry in UNIX file systems.) Not supported in legacy fallback mode. + * @returns Contained files. + */ +export function fileHierarchy(options?: { + /** Whether to recursively get subdirectories. */ + recursive: boolean; + /** Suggested directory in which the file picker opens. */ + startIn?: WellKnownDirectory | FileSystemHandle; + /** By specifying an ID, the user agent can remember different directories for different IDs. */ + id?: string; + /** By specifying a mode of `'readwrite'`, you can open a directory with write access. */ + mode?: FileSystemPermissionMode; + /** Callback to determine whether a directory should be entered, return `true` to skip. */ + skipDirectory?: ( + entry: FileSystemDirectoryEntry | FileSystemDirectoryHandle + ) => boolean; + /** + * Configurable setup, cleanup and `Promise` rejector usable with legacy API + * for determining when (and reacting if) a user cancels the operation. The + * method will be passed a reference to the internal `rejectionHandler` that + * can, e.g., be attached to/removed from the window or called after a + * timeout. The method should return a function that will be called when + * either the user chooses to open a file or the `rejectionHandler` is + * called. In the latter case, the returned function will also be passed a + * reference to the `reject` callback for the `Promise` returned by + * `fileOpen`, so that developers may reject the `Promise` when desired at + * that time. + * Example rejector: + * + * const file = await fileHierarchy({ + * legacySetup: (rejectionHandler) => { + * const timeoutId = setTimeout(rejectionHandler, 10_000); + * return (reject) => { + * clearTimeout(timeoutId); + * if (reject) { + * reject('My error message here.'); + * } + * }; + * }, + * }); + * + * ToDo: Remove this workaround once + * https://github.com/whatwg/html/issues/6376 is specified and supported. + */ + legacySetup?: ( + resolve: (value: FileWithDirectoryAndFileHandle) => void, + rejectionHandler: () => void, + input: HTMLInputElement + ) => (reject?: (reason?: any) => void) => void; +}): DirectoryHandleWithContents; + +export interface DirectoryHandleWithContents { + currentDir: FileSystemDirectoryHandle; + contents: Promise; +} + /** * Whether the File System Access API is supported. */ diff --git a/src/file-hierarchy.mjs b/src/file-hierarchy.mjs new file mode 100644 index 0000000..e366c97 --- /dev/null +++ b/src/file-hierarchy.mjs @@ -0,0 +1,30 @@ +/** + * Copyright 2020 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +// @license © 2020 Google LLC. Licensed under the Apache License, Version 2.0. + +// there is no legacy fallback possible for this since it changes the return type +const implementation = import('./fs-access/file-hierarchy.mjs'); + +/** + * Variant of (and largely delegates to) directory-open. Opens a directory from disk using the File System Access API. + * Includes a reference to the directory which was opened, and therefore is not backward compatible with + * ``. + * + * @type { typeof import("../index").fileHierarchy } + */ +export async function fileHierarchy(...args) { + return (await implementation).default(...args); +} diff --git a/src/fs-access/file-hierarchy.mjs b/src/fs-access/file-hierarchy.mjs new file mode 100644 index 0000000..97ab74c --- /dev/null +++ b/src/fs-access/file-hierarchy.mjs @@ -0,0 +1,40 @@ +/** + * Copyright 2020 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +// @license © 2020 Google LLC. Licensed under the Apache License, Version 2.0. + +import getFiles from './directory-open.mjs'; + +/** + * Variant of (and largely delegates to) directory-open. Opens a directory from disk using the File System Access API. + * Includes a reference to the directory which was opened, and therefore is not backward compatible with + * ``. + * + * @type { typeof import("../index").fileHierarchy } + */ +export default async (options = {}) => { + options.recursive = options.recursive || false; + options.mode = options.mode || 'read'; + const handle = await window.showDirectoryPicker({ + id: options.id, + startIn: options.startIn, + mode: options.mode, + }); + const dirResults = getFiles(options); + return { + currentDir: handle, + contents: dirResults, + }; +}; diff --git a/src/index.js b/src/index.js index ee1b7e2..ea2ba8d 100644 --- a/src/index.js +++ b/src/index.js @@ -21,6 +21,7 @@ export { fileOpen } from './file-open.mjs'; export { directoryOpen } from './directory-open.mjs'; export { fileSave } from './file-save.mjs'; +export { fileHierarchy } from './file-hierarchy.mjs'; export { default as fileOpenModern } from './fs-access/file-open.mjs'; export { default as directoryOpenModern } from './fs-access/directory-open.mjs';