Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

refactor: clean up retrieval from data transfer item #114

Merged
merged 1 commit into from
Dec 16, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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);
rolandjitsu marked this conversation as resolved.
Show resolved Hide resolved
if (fileWithPath) {
return fileWithPath;
}

const file = item.getAsFile();
if (!file) {
throw new Error("Transferred item is not a file.");
rolandjitsu marked this conversation as resolved.
Show resolved Hide resolved
}
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;
rolandjitsu marked this conversation as resolved.
Show resolved Hide resolved
}
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)) {
rolandjitsu marked this conversation as resolved.
Show resolved Hide resolved
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
Loading