Skip to content

Commit

Permalink
refactor: clean up retrieval from data transfer item
Browse files Browse the repository at this point in the history
Signed-off-by: Jon Koops <[email protected]>
  • Loading branch information
jonkoops authored and rolandjitsu committed Dec 16, 2024
1 parent df93aa0 commit 91f497a
Show file tree
Hide file tree
Showing 2 changed files with 70 additions and 33 deletions.
34 changes: 22 additions & 12 deletions src/file-selector.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 () => {
Expand Down Expand Up @@ -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 () => {
Expand Down Expand Up @@ -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",
Expand Down Expand Up @@ -589,21 +602,18 @@ function createFileSystemFileHandle<T>(
return [
file,
{
kind: "file",
getFile() {
return Promise.resolve(file);
},
},
} as FileSystemFileHandle,
];
}

function sortFiles<T extends File>(files: T[]) {
return files.slice(0).sort((a, b) => a.name.localeCompare(b.name));
}

interface FileSystemFileHandle {
getFile(): Promise<File | null>;
}

type FileOrDirEntry = FileEntry | DirEntry;

interface FileEntry extends Entry {
Expand Down
69 changes: 48 additions & 21 deletions src/file-selector.ts
Original file line number Diff line number Diff line change
Expand Up @@ -108,34 +108,49 @@ function flatten<T>(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<FileWithPath> {
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<File | 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).
// 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(
Expand Down Expand Up @@ -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;
Expand All @@ -194,6 +213,14 @@ const readEntries = (
): Promise<FileSystemEntry[]> =>
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<FileSystemHandle | null | undefined>;
}

// Infinite type recursion
// https://github.com/Microsoft/TypeScript/issues/3496#issuecomment-128553540
interface FileArray extends Array<FileValue> {}
Expand Down

0 comments on commit 91f497a

Please sign in to comment.