Skip to content

Commit

Permalink
feat!: always set a {relativePath} close #69
Browse files Browse the repository at this point in the history
BREAKING CHANGE: `{path}` is now relative when falling back to the file name
  • Loading branch information
saul-prepared authored and rolandjitsu committed Nov 2, 2024
1 parent 72d5fa6 commit 0dff2f9
Show file tree
Hide file tree
Showing 4 changed files with 96 additions and 25 deletions.
39 changes: 32 additions & 7 deletions src/file-selector.spec.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
import {FileWithPath} from './file';
import {fromEvent} from './file-selector';


it('returns a Promise', async () => {
const evt = new Event('test');
expect(fromEvent(evt)).toBeInstanceOf(Promise);
Expand Down Expand Up @@ -34,7 +33,7 @@ it('should return the evt {target} {files} if the passed event is an input evt',
expect(file.type).toBe(mockFile.type);
expect(file.size).toBe(mockFile.size);
expect(file.lastModified).toBe(mockFile.lastModified);
expect(file.path).toBe(name);
expect(file.path).toBe(`./${name}`);
});

it('should return an empty array if the evt {target} has no {files} prop', async () => {
Expand All @@ -59,7 +58,7 @@ it('should return files if the arg is a list of FileSystemFileHandle', async ()
expect(file.type).toBe(mockFile.type);
expect(file.size).toBe(mockFile.size);
expect(file.lastModified).toBe(mockFile.lastModified);
expect(file.path).toBe(name);
expect(file.path).toBe(`./${name}`);
});

it('should return an empty array if the passed event is not a DragEvent', async () => {
Expand All @@ -85,7 +84,7 @@ it('should return {files} from DataTransfer if {items} is not defined (e.g. IE11
expect(file.type).toBe(mockFile.type);
expect(file.size).toBe(mockFile.size);
expect(file.lastModified).toBe(mockFile.lastModified);
expect(file.path).toBe(name);
expect(file.path).toBe(`./${name}`);
});

it('should return files from DataTransfer {items} if the passed event is a DragEvent', async () => {
Expand All @@ -106,7 +105,32 @@ it('should return files from DataTransfer {items} if the passed event is a DragE
expect(file.type).toBe(mockFile.type);
expect(file.size).toBe(mockFile.size);
expect(file.lastModified).toBe(mockFile.lastModified);
expect(file.path).toBe(name);
expect(file.path).toBe(`./${name}`);
});

it('should use the {fullPath} for {path} if {webkitGetAsEntry} is supported and the items are FileSystemFileEntry', async () => {
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]);

const files = await fromEvent(evt);
expect(files).toHaveLength(1);
expect(files.every(file => file instanceof File)).toBe(true);

const [f] = files as FileWithPath[];

expect(f.name).toBe(mockFile.name);
expect(f.type).toBe(mockFile.type);
expect(f.size).toBe(mockFile.size);
expect(f.lastModified).toBe(mockFile.lastModified);
expect(f.path).toBe(fullPath);
});

it('skips DataTransfer {items} that are of kind "string"', async () => {
Expand All @@ -127,7 +151,7 @@ it('skips DataTransfer {items} that are of kind "string"', async () => {
expect(file.type).toBe(mockFile.type);
expect(file.size).toBe(mockFile.size);
expect(file.lastModified).toBe(mockFile.lastModified);
expect(file.path).toBe(name);
expect(file.path).toBe(`./${name}`);
});

it('can read a tree of directories recursively and return a flat list of FileWithPath objects', async () => {
Expand Down Expand Up @@ -277,7 +301,7 @@ it('should use getAsFileSystemHandle when available', async () => {
expect(file.type).toBe(f.type);
expect(file.size).toBe(f.size);
expect(file.lastModified).toBe(f.lastModified);
expect(file.path).toBe(name);
expect(file.path).toBe(`./${name}`);
});

function dragEvtFromItems(items: DataTransferItem | DataTransferItem[], type: string = 'drop'): DragEvent {
Expand Down Expand Up @@ -477,6 +501,7 @@ interface DirEntry extends Entry {
interface Entry {
isDirectory: boolean;
isFile: boolean;
fullPath?: string;
}

interface DirReader {
Expand Down
6 changes: 3 additions & 3 deletions src/file-selector.ts
Original file line number Diff line number Diff line change
Expand Up @@ -110,7 +110,7 @@ function toFilePromises(item: DataTransferItem) {
return fromDirEntry(entry) as any;
}

return fromDataTransferItem(item);
return fromDataTransferItem(item, entry);
}

function flatten<T>(items: any[]): T[] {
Expand All @@ -120,7 +120,7 @@ function flatten<T>(items: any[]): T[] {
], []);
}

function fromDataTransferItem(item: DataTransferItem) {
function fromDataTransferItem(item: DataTransferItem, entry?: FileSystemEntry | null) {
if (typeof (item as any).getAsFileSystemHandle === 'function') {
return (item as any).getAsFileSystemHandle()
.then(async (h: any) => {
Expand All @@ -133,7 +133,7 @@ function fromDataTransferItem(item: DataTransferItem) {
if (!file) {
return Promise.reject(`${item} is not a File`);
}
const fwp = toFileWithPath(file);
const fwp = toFileWithPath(file, entry?.fullPath ?? undefined);
return Promise.resolve(fwp);
}

Expand Down
40 changes: 39 additions & 1 deletion src/file.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -52,7 +52,7 @@ describe('toFile()', () => {
const name = 'test.json';
const file = new File([], name);
const fileWithPath = toFileWithPath(file);
expect(fileWithPath.path).toBe(name);
expect(fileWithPath.path).toBe(`./${name}`);
});

it('uses the File {webkitRelativePath} as {path} if it exists', () => {
Expand All @@ -66,6 +66,44 @@ 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: string[] = [];
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('sets the {type} from extension', () => {
const types = Array.from(COMMON_MIME_TYPES.values());
const files = Array.from(COMMON_MIME_TYPES.keys())
Expand Down
36 changes: 22 additions & 14 deletions src/file.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1203,21 +1203,17 @@ export const COMMON_MIME_TYPES = new Map([

export function toFileWithPath(file: FileWithPath, path?: string, h?: FileSystemHandle): FileWithPath {
const f = withMimeType(file);
const {webkitRelativePath} = file;
const p = typeof path === 'string'
? path
// If <input webkitdirectory> 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}`;
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
// If <input webkitdirectory> 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,
writable: false,
configurable: false,
enumerable: true
});
setObjProp(f, 'path', p);
}
if (h !== undefined) {
Object.defineProperty(f, 'handle', {
Expand All @@ -1227,12 +1223,15 @@ export function toFileWithPath(file: FileWithPath, path?: string, h?: FileSystem
enumerable: true
});
}
// Always populate a relative path so that even electron apps have access to a relativePath value
setObjProp(f, 'relativePath', p);
return f;
}

export interface FileWithPath extends File {
readonly path?: string;
readonly handle?: FileSystemFileHandle;
readonly relativePath?: string;
}

function withMimeType(file: FileWithPath) {
Expand All @@ -1255,3 +1254,12 @@ function withMimeType(file: FileWithPath) {

return file;
}

function setObjProp(f: FileWithPath, key: string, value: string) {
Object.defineProperty(f, key, {
value,
writable: false,
configurable: false,
enumerable: true
})
}

0 comments on commit 0dff2f9

Please sign in to comment.