diff --git a/src/file-selector.spec.ts b/src/file-selector.spec.ts index f8e4e0e..3a8b1b5 100644 --- a/src/file-selector.spec.ts +++ b/src/file-selector.spec.ts @@ -312,13 +312,13 @@ it("should throw if reading file entry fails", (done) => { .catch(() => done()); }); -it("should throw if DataTransferItem is not a File", (done) => { +it("should throw if DataTransferItem is not a File", async () => { const item = dataTransferItem(null, "file"); - const evt = dragEvtFromFilesAndItems([], [item]); + const event = dragEvtFromFilesAndItems([], [item]); - fromEvent(evt) - .then(() => done.fail("Getting the files should have failed")) - .catch(() => done()); + await expect(fromEvent(event)).rejects.toThrow( + "Transferred item is not a file.", + ); }); it("should use getAsFileSystemHandle when available", async () => { @@ -380,8 +380,11 @@ it("should not use getAsFileSystemHandle when not in a secure context", async () }); it("should reject when getAsFileSystemHandle resolves to null", async () => { - const evt = dragEvtFromItems([dataTransferItemWithFsHandle(null, null)]); - expect(fromEvent(evt)).rejects.toThrow("[object Object] is not a File"); + const event = dragEvtFromItems([dataTransferItemWithFsHandle(null, null)]); + + await expect(fromEvent(event)).rejects.toThrow( + "Transferred item is not a file.", + ); }); it("should fallback to getAsFile when getAsFileSystemHandle resolves to undefined", async () => { @@ -410,6 +413,16 @@ it("should fallback to getAsFile when getAsFileSystemHandle resolves to undefine expect(file.path).toBe(`./${name}`); }); +it("should throw if getAsFileSystemHandle() does not return a file", async () => { + const file = createFile("test.json", {}); + const handle = { kind: "unknown" } as unknown as FileSystemFileHandle; + const event = dragEvtFromItems([dataTransferItemWithFsHandle(file, handle)]); + + await expect(fromEvent(event)).rejects.toThrow( + "Transferred item is not a file.", + ); +}); + function dragEvtFromItems( items: DataTransferItem | DataTransferItem[], type: string = "drop", @@ -589,10 +602,11 @@ function createFileSystemFileHandle( return [ file, { + kind: "file", getFile() { return Promise.resolve(file); }, - }, + } as FileSystemFileHandle, ]; } @@ -600,10 +614,6 @@ function sortFiles(files: T[]) { return files.slice(0).sort((a, b) => a.name.localeCompare(b.name)); } -interface FileSystemFileHandle { - getFile(): Promise; -} - type FileOrDirEntry = FileEntry | DirEntry; interface FileEntry extends Entry { diff --git a/src/file-selector.ts b/src/file-selector.ts index c108a2a..cdea4ca 100644 --- a/src/file-selector.ts +++ b/src/file-selector.ts @@ -108,34 +108,49 @@ function flatten(items: any[]): T[] { async function fromDataTransferItem( item: DataTransferItem, entry?: FileSystemEntry | null, -) { - // Check if we're in a secure context; due to a bug in Chrome (as far as we know) the browser crashes when calling this API (yet to be confirmed as a consistent behavior). - // +): Promise { + const fileWithPath = await fromFileSystemHandle(item, entry); + if (fileWithPath) { + return fileWithPath; + } + + const file = item.getAsFile(); + if (!file) { + throw new Error("Transferred item is not a file."); + } + return toFileWithPath(file, entry?.fullPath); +} + +async function fromFileSystemHandle( + item: PartialDataTransferItem, + entry?: FileSystemEntry | null, +): Promise { + // Check if we're in a secure context; due to a bug in Chrome (as far as we know) + // the browser crashes when calling this API (yet to be confirmed as a consistent behavior). // See: // - https://issues.chromium.org/issues/40186242 // - https://github.com/react-dropzone/react-dropzone/issues/1397 + // If the browser does not support the API act as if the file could not be retrieved. if ( - globalThis.isSecureContext && - typeof (item as any).getAsFileSystemHandle === "function" + !globalThis.isSecureContext || + !(typeof item.getAsFileSystemHandle === "function") ) { - const h = await (item as any).getAsFileSystemHandle(); - if (h === null) { - throw new Error(`${item} is not a File`); - } - // It seems that the handle can be `undefined` (see https://github.com/react-dropzone/file-selector/issues/120), - // so we check if it isn't; if it is, the code path continues to the next API (`getAsFile`). - if (h !== undefined) { - const file = await h.getFile(); - file.handle = h; - return toFileWithPath(file); - } + return null; } - const file = item.getAsFile(); - if (!file) { - throw new Error(`${item} is not a File`); + + const handle = await item.getAsFileSystemHandle(); + + // The handle can be undefined due to a browser bug, in this case we act as if the file could not be retrieved. + // See: https://github.com/react-dropzone/file-selector/issues/120 + if (handle === undefined) { + return null; + } + + if (!handle || !isFileHandle(handle)) { + throw new Error("Transferred item is not a file."); } - const fwp = toFileWithPath(file, entry?.fullPath ?? undefined); - return fwp; + + return toFileWithPath(await handle.getFile(), entry?.fullPath, handle); } async function fromEntry( @@ -179,6 +194,10 @@ async function fromFileEntry( return fileWithPath; } +const isFileHandle = ( + handle: FileSystemHandle, +): handle is FileSystemFileHandle => handle.kind === "file"; + const isDirectoryEntry = ( entry: FileSystemEntry, ): entry is FileSystemDirectoryEntry => entry.isDirectory; @@ -194,6 +213,14 @@ const readEntries = ( ): Promise => new Promise((resolve, reject) => reader.readEntries(resolve, reject)); +interface PartialDataTransferItem extends DataTransferItem { + // This method is not yet widely supported in all browsers, and is thus marked as optional. + // See: https://developer.mozilla.org/en-US/docs/Web/API/DataTransferItem/getAsFileSystemHandle + // Additionally, this method is known to return `undefined` in some cases due to browser bugs. + // See: https://github.com/react-dropzone/file-selector/issues/120 + getAsFileSystemHandle?(): Promise; +} + // Infinite type recursion // https://github.com/Microsoft/TypeScript/issues/3496#issuecomment-128553540 interface FileArray extends Array {}