Skip to content

Commit

Permalink
feat: 🎸 implement .getFileHandle() method
Browse files Browse the repository at this point in the history
  • Loading branch information
streamich committed Jun 14, 2023
1 parent 528c807 commit 40bdc13
Show file tree
Hide file tree
Showing 3 changed files with 106 additions and 10 deletions.
46 changes: 36 additions & 10 deletions src/node-to-fsa/NodeFileSystemDirectoryHandle.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import {NodeFileSystemHandle} from "./NodeFileSystemHandle";
import {assertName, basename, ctx as createCtx} from "./util";
import {assertName, basename, ctx as createCtx, newNotAllowedError, newNotFoundError, newTypeMismatchError} from "./util";
import {NodeFileSystemFileHandle} from "./NodeFileSystemFileHandle";
import type {NodeFsaContext, NodeFsaFs} from "./types";

Expand Down Expand Up @@ -51,6 +51,9 @@ export class NodeFileSystemDirectoryHandle extends NodeFileSystemHandle {
}

/**
* Returns a {@link NodeFileSystemDirectoryHandle} for a subdirectory with the specified
* name within the directory handle on which the method is called.
*
* @see https://developer.mozilla.org/en-US/docs/Web/API/FileSystemDirectoryHandle/getDirectoryHandle
* @param name A string representing the {@link NodeFileSystemHandle} name of
* the subdirectory you wish to retrieve.
Expand All @@ -62,9 +65,7 @@ export class NodeFileSystemDirectoryHandle extends NodeFileSystemHandle {
const filename = this.path + this.ctx.separator! + name;
try {
const stats = await this.fs.promises.stat(filename);
if (!stats.isDirectory()) {
throw new DOMException('The path supplied exists, but was not an entry of requested type.', 'TypeMismatchError');
}
if (!stats.isDirectory()) throw newTypeMismatchError();
return new NodeFileSystemDirectoryHandle(this.fs, filename, this.ctx);
} catch (error) {
if (error instanceof DOMException) throw error;
Expand All @@ -75,26 +76,51 @@ export class NodeFileSystemDirectoryHandle extends NodeFileSystemHandle {
await this.fs.promises.mkdir(filename);
return new NodeFileSystemDirectoryHandle(this.fs, filename, this.ctx);
}
throw new DOMException('A requested file or directory could not be found at the time an operation was processed.', 'NotFoundError');
throw newNotFoundError();
}
case 'EPERM':
case 'EACCES': {
throw new DOMException('Permission not granted.', 'NotAllowedError');
}
case 'EACCES':
throw newNotAllowedError();
}
}
throw error;
}
}

/**
* Returns a {@link FileSystemFileHandle} for a file with the specified name,
* within the directory the method is called.
*
* @see https://developer.mozilla.org/en-US/docs/Web/API/FileSystemDirectoryHandle/getFileHandle
* @param name A string representing the {@link NodeFileSystemHandle} name of
* the file you wish to retrieve.
* @param options An optional object containing options for the retrieved file.
*/
public getFileHandle(name: string, options?: GetFileHandleOptions): Promise<NodeFileSystemDirectoryHandle> {
throw new Error('Not implemented');
public async getFileHandle(name: string, options?: GetFileHandleOptions): Promise<NodeFileSystemFileHandle> {
assertName(name, 'getDirectoryHandle', 'FileSystemDirectoryHandle');
const filename = this.path + this.ctx.separator! + name;
try {
const stats = await this.fs.promises.stat(filename);
if (!stats.isFile()) throw newTypeMismatchError();
return new NodeFileSystemFileHandle(this.fs, filename, this.ctx);
} catch (error) {
if (error instanceof DOMException) throw error;
if (error && typeof error === 'object') {
switch (error.code) {
case 'ENOENT': {
if (options && options.create) {
await this.fs.promises.writeFile(filename, '');
return new NodeFileSystemFileHandle(this.fs, filename, this.ctx);
}
throw newNotFoundError();
}
case 'EPERM':
case 'EACCES':
throw newNotAllowedError();
}
}
throw error;
}
}

/**
Expand Down
61 changes: 61 additions & 0 deletions src/node-to-fsa/__tests__/NodeFileSystemDirectoryHandle.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -183,8 +183,69 @@ describe('.getDirectoryHandle()', () => {
expect(fs.existsSync('/subdir')).toBe(false);
const subdir = await dir.getDirectoryHandle('subdir', {create: true});
expect(fs.existsSync('/subdir')).toBe(true);
expect(fs.statSync('/subdir').isDirectory()).toBe(true);
expect(subdir.kind).toBe('directory');
expect(subdir.name).toBe('subdir');
expect(subdir).toBeInstanceOf(NodeFileSystemDirectoryHandle);
});
});

describe('.getFileHandle()', () => {
test('throws "NotFoundError" DOMException if file not found', async () => {
const {dir} = setup({a: null});
try {
await dir.getFileHandle('b');
throw new Error('Not this error.');
} catch (error) {
expect(error).toBeInstanceOf(DOMException);
expect(error.name).toBe('NotFoundError');
expect(error.message).toBe('A requested file or directory could not be found at the time an operation was processed.');
}
});

test('throws "TypeMismatchError" DOMException if entry is not a file', async () => {
const {dir} = setup({directory: null});
try {
await dir.getFileHandle('directory');
throw new Error('Not this error.');
} catch (error) {
expect(error).toBeInstanceOf(DOMException);
expect(error.name).toBe('TypeMismatchError');
expect(error.message).toBe('The path supplied exists, but was not an entry of requested type.');
}
});

const invalidNames = ['.', '..', '/', '/a', 'a/', 'a//b', 'a/.', 'a/..', 'a/b/.', 'a/b/..', '\\', '\\a', 'a\\', 'a\\\\b', 'a\\.'];

for (const invalidName of invalidNames) {
test(`throws on invalid file name: "${invalidName}"`, async () => {
const {dir} = setup({file: 'contents'});
try {
await dir.getFileHandle(invalidName);
throw new Error('Not this error.');
} catch (error) {
expect(error).toBeInstanceOf(TypeError);
expect(error.message).toBe(`Failed to execute 'getDirectoryHandle' on 'FileSystemDirectoryHandle': Name is not allowed.`);
}
});
}

test('can retrieve a child file', async () => {
const {dir} = setup({file: 'contents', subdir: null});
const subdir = await dir.getFileHandle('file');
expect(subdir.kind).toBe('file');
expect(subdir.name).toBe('file');
expect(subdir).toBeInstanceOf(NodeFileSystemFileHandle);
});

test('can create a file', async () => {
const {dir, fs} = setup({});
expect(fs.existsSync('/text.txt')).toBe(false);
const subdir = await dir.getFileHandle('text.txt', {create: true});
expect(fs.existsSync('/text.txt')).toBe(true);
expect(fs.statSync('/text.txt').isFile()).toBe(true);
expect(subdir.kind).toBe('file');
expect(subdir.name).toBe('text.txt');
expect(subdir).toBeInstanceOf(NodeFileSystemFileHandle);
});
});
9 changes: 9 additions & 0 deletions src/node-to-fsa/util.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,3 +21,12 @@ export const assertName = (name: string, method: string, klass: string) => {
const isInvalid = nameRegex.test(name);
if (isInvalid) throw new TypeError(`Failed to execute '${method}' on '${klass}': Name is not allowed.`);
};

export const newNotFoundError = () =>
new DOMException('A requested file or directory could not be found at the time an operation was processed.', 'NotFoundError');

export const newTypeMismatchError = () =>
new DOMException('The path supplied exists, but was not an entry of requested type.', 'TypeMismatchError');

export const newNotAllowedError = () =>
new DOMException('Permission not granted.', 'NotAllowedError');

0 comments on commit 40bdc13

Please sign in to comment.