From 71567c9d97b3c738cc425d4058b32cbc3d740952 Mon Sep 17 00:00:00 2001 From: streamich Date: Wed, 14 Jun 2023 20:01:45 +0200 Subject: [PATCH] =?UTF-8?q?feat:=20=F0=9F=8E=B8=20implement=20.getFileHand?= =?UTF-8?q?le()=20method?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../NodeFileSystemDirectoryHandle.ts | 46 +++++++++++--- .../NodeFileSystemDirectoryHandle.test.ts | 61 +++++++++++++++++++ src/node-to-fsa/util.ts | 9 +++ 3 files changed, 106 insertions(+), 10 deletions(-) diff --git a/src/node-to-fsa/NodeFileSystemDirectoryHandle.ts b/src/node-to-fsa/NodeFileSystemDirectoryHandle.ts index 4c2a5843f..c7f9ae624 100644 --- a/src/node-to-fsa/NodeFileSystemDirectoryHandle.ts +++ b/src/node-to-fsa/NodeFileSystemDirectoryHandle.ts @@ -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"; @@ -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. @@ -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; @@ -75,12 +76,11 @@ 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; @@ -88,13 +88,39 @@ export class NodeFileSystemDirectoryHandle extends NodeFileSystemHandle { } /** + * 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 { - throw new Error('Not implemented'); + public async getFileHandle(name: string, options?: GetFileHandleOptions): Promise { + 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; + } } /** diff --git a/src/node-to-fsa/__tests__/NodeFileSystemDirectoryHandle.test.ts b/src/node-to-fsa/__tests__/NodeFileSystemDirectoryHandle.test.ts index 775029b6d..a267800fa 100644 --- a/src/node-to-fsa/__tests__/NodeFileSystemDirectoryHandle.test.ts +++ b/src/node-to-fsa/__tests__/NodeFileSystemDirectoryHandle.test.ts @@ -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); + }); +}); diff --git a/src/node-to-fsa/util.ts b/src/node-to-fsa/util.ts index 964fa3565..feb8107d0 100644 --- a/src/node-to-fsa/util.ts +++ b/src/node-to-fsa/util.ts @@ -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');