From 548e8530a4c837d845da1da53da4fbc6bfb49c76 Mon Sep 17 00:00:00 2001 From: saul-prepared Date: Thu, 15 Feb 2024 16:26:25 +1100 Subject: [PATCH 1/3] feat: Always populate relativePath for access across browser and electron --- src/file-selector.ts | 2 +- src/file.spec.ts | 45 ++++++++++++++++++++++++++++++++++++++++++++ src/file.ts | 18 +++++++++++++++++- 3 files changed, 63 insertions(+), 2 deletions(-) diff --git a/src/file-selector.ts b/src/file-selector.ts index 31b6b36..86de49d 100644 --- a/src/file-selector.ts +++ b/src/file-selector.ts @@ -110,7 +110,7 @@ function toFilePromises(item: DataTransferItem) { return fromDirEntry(entry) as any; } - return fromDataTransferItem(item); + return fromFileEntry(entry); } function flatten(items: any[]): T[] { diff --git a/src/file.spec.ts b/src/file.spec.ts index 09dd674..69006c0 100644 --- a/src/file.spec.ts +++ b/src/file.spec.ts @@ -66,6 +66,51 @@ describe('toFile()', () => { expect(fileWithPath.path).toBe(path); }); + it('sets the {relativePath} if provided without overwriting {path}', () => { + const fullPath = '/Users/test/Desktop/test/test.json'; + const path = '/test/test.json'; + const file = new File([], 'test.json'); + + //@ts-expect-error + file.path = fullPath; + const fileWithPath = toFileWithPath(file, path); + expect(fileWithPath.path).toBe(fullPath); + expect(fileWithPath.relativePath).toBe(path); + }); + + test('{relativePath} is enumerable', () => { + const path = '/test/test.json'; + const file = new File([], 'test.json'); + const fileWithPath = toFileWithPath(file, path); + + expect(Object.keys(fileWithPath)).toContain('relativePath'); + + const keys = []; + for (const key in fileWithPath) { + keys.push(key); + } + + expect(keys).toContain('relativePath'); + }); + + it('uses the File {webkitRelativePath} as {relativePath} if it exists', () => { + const name = 'test.json'; + const path = 'test/test.json'; + const file = new File([], name); + Object.defineProperty(file, 'webkitRelativePath', { + value: path + }); + const fileWithPath = toFileWithPath(file); + expect(fileWithPath.relativePath).toBe(path); + }); + + it('uses the File {name} as {relativePath} if not provided and prefix with forward slash (/)', () => { + const name = 'test.json'; + const file = new File([], name); + const fileWithPath = toFileWithPath(file); + expect(fileWithPath.relativePath).toBe("/"+name); + }); + it('sets the {type} from extension', () => { const types = Array.from(COMMON_MIME_TYPES.values()); const files = Array.from(COMMON_MIME_TYPES.keys()) diff --git a/src/file.ts b/src/file.ts index aa99ec1..50a7e39 100644 --- a/src/file.ts +++ b/src/file.ts @@ -87,8 +87,8 @@ export const COMMON_MIME_TYPES = new Map([ export function toFileWithPath(file: FileWithPath, path?: string): FileWithPath { const f = withMimeType(file); + const {webkitRelativePath} = file; if (typeof f.path !== 'string') { // on electron, path is already set to the absolute path - const {webkitRelativePath} = file; Object.defineProperty(f, 'path', { value: typeof path === 'string' ? path @@ -104,11 +104,27 @@ export function toFileWithPath(file: FileWithPath, path?: string): FileWithPath }); } + //Always populate a relative path so that even electron apps have access to a relativePath value + Object.defineProperty(f, 'relativePath', { + value: typeof path === 'string' + ? path + // If is set, + // the File will have a {webkitRelativePath} property + // https://developer.mozilla.org/en-US/docs/Web/API/HTMLInputElement/webkitdirectory + : typeof webkitRelativePath === 'string' && webkitRelativePath.length > 0 + ? webkitRelativePath + : `/${file.name}`, //prepend forward slash (/) to ensure consistancy when path isn't supplied. + writable: false, + configurable: false, + enumerable: true + }) + return f; } export interface FileWithPath extends File { readonly path?: string; + readonly relativePath?: string; } function withMimeType(file: FileWithPath) { From 20a3fc3d87467640e0f0e7d9b1bff5980b43876b Mon Sep 17 00:00:00 2001 From: saul-prepared Date: Thu, 15 Feb 2024 17:02:44 +1100 Subject: [PATCH 2/3] fix: eslint issues --- src/file.spec.ts | 4 ++-- src/file.ts | 10 +++++----- 2 files changed, 7 insertions(+), 7 deletions(-) diff --git a/src/file.spec.ts b/src/file.spec.ts index 69006c0..3e97c08 100644 --- a/src/file.spec.ts +++ b/src/file.spec.ts @@ -71,7 +71,7 @@ describe('toFile()', () => { const path = '/test/test.json'; const file = new File([], 'test.json'); - //@ts-expect-error + // @ts-expect-error file.path = fullPath; const fileWithPath = toFileWithPath(file, path); expect(fileWithPath.path).toBe(fullPath); @@ -108,7 +108,7 @@ describe('toFile()', () => { const name = 'test.json'; const file = new File([], name); const fileWithPath = toFileWithPath(file); - expect(fileWithPath.relativePath).toBe("/"+name); + expect(fileWithPath.relativePath).toBe('/' + name); }); it('sets the {type} from extension', () => { diff --git a/src/file.ts b/src/file.ts index 50a7e39..94365eb 100644 --- a/src/file.ts +++ b/src/file.ts @@ -104,7 +104,7 @@ export function toFileWithPath(file: FileWithPath, path?: string): FileWithPath }); } - //Always populate a relative path so that even electron apps have access to a relativePath value + // Always populate a relative path so that even electron apps have access to a relativePath value Object.defineProperty(f, 'relativePath', { value: typeof path === 'string' ? path @@ -113,10 +113,10 @@ export function toFileWithPath(file: FileWithPath, path?: string): FileWithPath // https://developer.mozilla.org/en-US/docs/Web/API/HTMLInputElement/webkitdirectory : typeof webkitRelativePath === 'string' && webkitRelativePath.length > 0 ? webkitRelativePath - : `/${file.name}`, //prepend forward slash (/) to ensure consistancy when path isn't supplied. - writable: false, - configurable: false, - enumerable: true + : `/${file.name}`, // prepend forward slash (/) to ensure consistancy when path isn't supplied. + writable: false, + configurable: false, + enumerable: true }) return f; From 1d461846be2d7dea898d2ea48fa0c0a3af4a45b4 Mon Sep 17 00:00:00 2001 From: saul-prepared Date: Mon, 19 Feb 2024 13:05:22 +1100 Subject: [PATCH 3/3] fix: change to use fromDataTransferItem to prevent breaking change. Add functionality to fromDataTransferItem instead. --- src/file-selector.spec.ts | 36 ++++++++++++++++++++++++++++++++++-- src/file-selector.ts | 10 ++++++++-- 2 files changed, 42 insertions(+), 4 deletions(-) diff --git a/src/file-selector.spec.ts b/src/file-selector.spec.ts index a590035..f64608c 100644 --- a/src/file-selector.spec.ts +++ b/src/file-selector.spec.ts @@ -1,6 +1,6 @@ -import {FileWithPath} from './file'; +import { FileWithPath } from './file'; import {fromEvent} from './file-selector'; - +const toFileWithPathSpy = jest.spyOn(jest.requireActual('./file'), 'toFileWithPath'); it('returns a Promise', async () => { const evt = new Event('test'); @@ -109,6 +109,37 @@ it('should return files from DataTransfer {items} if the passed event is a DragE expect(file.path).toBe(name); }); +it('should call toFilePath with undefined path if {webkitGetAsEntry} is not a function', async () => { + toFileWithPathSpy.mockClear(); + const name = 'test.json'; + const mockFile = createFile(name, {ping: true}, { + type: 'application/json' + }); + + const item = dataTransferItemFromFile(mockFile); + const evt = dragEvtFromFilesAndItems([], [item]); + await fromEvent(evt); + expect(toFileWithPathSpy).toBeCalledTimes(1); + expect(toFileWithPathSpy).toBeCalledWith(mockFile, undefined); +}); + +it('should call toFilePath with {fullPath} path if file can be converted into an Entry', async () => { + toFileWithPathSpy.mockClear(); + const name = 'test.json'; + const fullPath = '/testfolder/test.json' + const mockFile = createFile(name, {ping: true}, { + type: 'application/json' + }); + + const file = fileSystemFileEntryFromFile(mockFile); + file.fullPath = fullPath + const item = dataTransferItemFromEntry(file, mockFile); + const evt = dragEvtFromFilesAndItems([], [item]); + await fromEvent(evt); + expect(toFileWithPathSpy).toBeCalledTimes(1); + expect(toFileWithPathSpy).toBeCalledWith(mockFile, fullPath); +}); + it('skips DataTransfer {items} that are of kind "string"', async () => { const name = 'test.json'; const mockFile = createFile(name, {ping: true}, { @@ -445,6 +476,7 @@ interface DirEntry extends Entry { interface Entry { isDirectory: boolean; isFile: boolean; + fullPath?: string; } interface DirReader { diff --git a/src/file-selector.ts b/src/file-selector.ts index 86de49d..a1efaec 100644 --- a/src/file-selector.ts +++ b/src/file-selector.ts @@ -110,7 +110,7 @@ function toFilePromises(item: DataTransferItem) { return fromDirEntry(entry) as any; } - return fromFileEntry(entry); + return fromDataTransferItem(item); } function flatten(items: any[]): T[] { @@ -122,10 +122,16 @@ function flatten(items: any[]): T[] { function fromDataTransferItem(item: DataTransferItem) { const file = item.getAsFile(); + + let fileAsEntry: FileSystemEntry | null = null; + if (typeof item.webkitGetAsEntry === 'function') { + fileAsEntry = item.webkitGetAsEntry(); + } + if (!file) { return Promise.reject(`${item} is not a File`); } - const fwp = toFileWithPath(file); + const fwp = toFileWithPath(file, fileAsEntry?.fullPath ?? undefined); return Promise.resolve(fwp); }