diff --git a/src/fsa-to-node/FsaNodeFs.ts b/src/fsa-to-node/FsaNodeFs.ts index c1b65a7ee..a716cee07 100644 --- a/src/fsa-to-node/FsaNodeFs.ts +++ b/src/fsa-to-node/FsaNodeFs.ts @@ -10,9 +10,9 @@ import type * as misc from '../node/types/misc'; import type * as opts from '../node/types/options'; import type * as fsa from '../fsa/types'; -const notImplemented: (...args: unknown[]) => unknown = () => { - throw new Error('Not implemented'); -}; +// const notImplemented: (...args: unknown[]) => unknown = () => { +// throw new Error('Not implemented'); +// }; /** * Constructs a Node.js `fs` API from a File System Access API @@ -20,9 +20,33 @@ const notImplemented: (...args: unknown[]) => unknown = () => { */ export class FsaNodeFs implements FsCallbackApi { public readonly promises: FsPromisesApi = createPromisesApi(this); + public readonly fs = new Map(); public constructor(protected readonly root: fsa.IFileSystemDirectoryHandle) {} + /** + * @param path Path from root to the new folder. + * @param create Whether to create the folders if they don't exist. + */ + private async getDir(path: string[], create: boolean, funcName?: string): Promise { + let curr: fsa.IFileSystemDirectoryHandle = this.root; + const options: fsa.GetDirectoryHandleOptions = { create }; + try { + for (const name of path) curr = await curr.getDirectoryHandle(name, options); + } catch (error) { + if (error && typeof error === 'object' && error.name === 'TypeMismatchError') + throw createError('ENOTDIR', funcName, path.join(FsaToNodeConstants.Separator)); + throw error; + } + return curr; + } + + // private async getFile(path: string[], name: string, funcName?: string): Promise { + // const dir = await this.getDir(path, false, funcName); + // const file = await dir.getFileHandle(name, { create: false }); + // return file; + // } + public readonly open: FsCallbackApi['open'] = ( path: misc.PathLike, flags: misc.TFlags, @@ -85,9 +109,30 @@ export class FsaNodeFs implements FsCallbackApi { throw new Error('Not implemented'); } - unlink(path: misc.PathLike, callback: misc.TCallback): void { - throw new Error('Not implemented'); - } + public readonly unlink: FsCallbackApi['unlink'] = (path: misc.PathLike, callback: misc.TCallback): void => { + const filename = pathToFilename(path); + const [folder, name] = pathToLocation(filename); + this.getDir(folder, false, 'unlink') + .then(dir => dir.removeEntry(name)) + .then( + () => callback(null), + error => { + if (error && typeof error === 'object') { + switch (error.name) { + case 'NotFoundError': { + callback(createError('ENOENT', 'unlink', filename)); + return; + } + case 'InvalidModificationError': { + callback(createError('EISDIR', 'unlink', filename)); + return; + } + } + } + callback(error); + }, + ); + }; symlink(target: misc.PathLike, path: misc.PathLike, callback: misc.TCallback); symlink(target: misc.PathLike, path: misc.PathLike, type: misc.symlink.Type, callback: misc.TCallback); @@ -205,23 +250,6 @@ export class FsaNodeFs implements FsCallbackApi { throw new Error('Not implemented'); } - /** - * @param path Path from root to the new folder. - * @param create Whether to create the folders if they don't exist. - */ - private async getDir(path: string[], create: boolean, funcName?: string): Promise { - let curr: fsa.IFileSystemDirectoryHandle = this.root; - const options: fsa.GetDirectoryHandleOptions = { create }; - try { - for (const name of path) curr = await curr.getDirectoryHandle(name, options); - } catch (error) { - if (error && typeof error === 'object' && error.name === 'TypeMismatchError') - throw createError('ENOTDIR', funcName, path.join(FsaToNodeConstants.Separator)); - throw error; - } - return curr; - } - public readonly mkdir: FsCallbackApi['mkdir'] = ( path: misc.PathLike, a: misc.TCallback | misc.TMode | opts.IMkdirOptions, diff --git a/src/fsa-to-node/__tests__/FsaNodeFs.test.ts b/src/fsa-to-node/__tests__/FsaNodeFs.test.ts index 5d5b3b3e6..f812677fd 100644 --- a/src/fsa-to-node/__tests__/FsaNodeFs.test.ts +++ b/src/fsa-to-node/__tests__/FsaNodeFs.test.ts @@ -151,3 +151,43 @@ describe('.rm()', () => { await fs.promises.rm('/lala/lulu', { recursive: true, force: true }); }); }); + +describe('.unlink()', () => { + test('can remove a file', async () => { + const { fs, mfs } = setup({ folder: { file: 'test' }, 'empty-folder': null }); + const res = await fs.promises.unlink('/folder/file'); + expect(res).toBe(undefined); + expect(mfs.__vol.toJSON()).toStrictEqual({ + '/mountpoint/folder': null, + '/mountpoint/empty-folder': null, + }); + }); + + test('cannot delete a folder', async () => { + const { fs, mfs } = setup({ folder: { file: 'test' }, 'empty-folder': null }); + try { + await fs.promises.unlink('/folder'); + throw new Error('Expected error'); + } catch (error) { + expect(error.code).toBe('EISDIR'); + expect(mfs.__vol.toJSON()).toStrictEqual({ + '/mountpoint/folder/file': 'test', + '/mountpoint/empty-folder': null, + }); + } + }); + + test('throws when deleting non-existing file', async () => { + const { fs, mfs } = setup({ folder: { file: 'test' }, 'empty-folder': null }); + try { + await fs.promises.unlink('/folder/not-a-file'); + throw new Error('Expected error'); + } catch (error) { + expect(error.code).toBe('ENOENT'); + expect(mfs.__vol.toJSON()).toStrictEqual({ + '/mountpoint/folder/file': 'test', + '/mountpoint/empty-folder': null, + }); + } + }); +});