Skip to content

Commit

Permalink
feat: 🎸 implement .removeEntry() method
Browse files Browse the repository at this point in the history
  • Loading branch information
streamich committed Jun 14, 2023
1 parent 0628d56 commit dca57a2
Show file tree
Hide file tree
Showing 4 changed files with 114 additions and 10 deletions.
42 changes: 36 additions & 6 deletions src/node-to-fsa/NodeFileSystemDirectoryHandle.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import {NodeFileSystemHandle} from "./NodeFileSystemHandle";
import {assertName, basename, ctx as createCtx, newNotAllowedError, newNotFoundError, newTypeMismatchError} from "./util";
import {NodeFileSystemFileHandle} from "./NodeFileSystemFileHandle";
import type {NodeFsaContext, NodeFsaFs} from "./types";
import type Dirent from "../Dirent";

/**
* @see https://developer.mozilla.org/en-US/docs/Web/API/FileSystemDirectoryHandle
Expand All @@ -23,7 +24,7 @@ export class NodeFileSystemDirectoryHandle extends NodeFileSystemHandle {
*/
public async * keys(): AsyncIterableIterator<string> {
const list = await this.fs.promises.readdir(this.path);
for (const name of list) yield name;
for (const name of list) yield '' + name;
}

/**
Expand All @@ -32,8 +33,9 @@ export class NodeFileSystemDirectoryHandle extends NodeFileSystemHandle {
public async * entries(): AsyncIterableIterator<[string, NodeFileSystemHandle]> {
const {path, fs, ctx} = this;
const list = await fs.promises.readdir(path, {withFileTypes: true});
for (const dirent of list) {
const name = dirent.name;
for (const d of list) {
const dirent = d as Dirent;
const name = dirent.name + '';
const newPath = path + ctx.separator! + name;
if (dirent.isDirectory()) yield [name, new NodeFileSystemDirectoryHandle(fs, newPath, ctx)];
else if (dirent.isFile()) yield [name, new NodeFileSystemFileHandle(fs, name, ctx)];
Expand Down Expand Up @@ -97,7 +99,7 @@ export class NodeFileSystemDirectoryHandle extends NodeFileSystemHandle {
* @param options An optional object containing options for the retrieved file.
*/
public async getFileHandle(name: string, options?: GetFileHandleOptions): Promise<NodeFileSystemFileHandle> {
assertName(name, 'getDirectoryHandle', 'FileSystemDirectoryHandle');
assertName(name, 'getFileHandle', 'FileSystemDirectoryHandle');
const filename = this.path + this.ctx.separator! + name;
try {
const stats = await this.fs.promises.stat(filename);
Expand All @@ -124,13 +126,41 @@ export class NodeFileSystemDirectoryHandle extends NodeFileSystemHandle {
}

/**
* Attempts to remove an entry if the directory handle contains a file or
* directory called the name specified.
*
* @see https://developer.mozilla.org/en-US/docs/Web/API/FileSystemDirectoryHandle/removeEntry
* @param name A string representing the {@link FileSystemHandle} name of the
* entry you wish to remove.
* @param options An optional object containing options.
*/
public removeEntry(name: string, options?: RemoveEntryOptions): Promise<void> {
throw new Error('Not implemented');
public async removeEntry(name: string, {recursive = false}: RemoveEntryOptions = {}): Promise<void> {
assertName(name, 'removeEntry', 'FileSystemDirectoryHandle');
const filename = this.path + this.ctx.separator! + name;
const promises = this.fs.promises;
try {
const stats = await promises.stat(filename);
if (stats.isFile()) {
await promises.unlink(filename);
} else if (stats.isDirectory()) {
await promises.rmdir(filename, {recursive});
} else throw newTypeMismatchError();
} catch (error) {
if (error instanceof DOMException) throw error;
if (error && typeof error === 'object') {
switch (error.code) {
case 'ENOENT': {
throw newNotFoundError();
}
case 'EPERM':
case 'EACCES':
throw newNotAllowedError();
case 'ENOTEMPTY':
throw new DOMException('The object can not be modified in this way.', 'InvalidModificationError');
}
}
throw error;
}
}

/**
Expand Down
76 changes: 74 additions & 2 deletions src/node-to-fsa/__tests__/NodeFileSystemDirectoryHandle.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ import {NodeFileSystemHandle} from '../NodeFileSystemHandle';

const setup = (json: DirectoryJSON = {}) => {
const fs = memfs(json, '/');
const dir = new NodeFileSystemDirectoryHandle(fs, '/');
const dir = new NodeFileSystemDirectoryHandle(fs as any, '/');
return {dir, fs};
};

Expand Down Expand Up @@ -225,7 +225,7 @@ describe('.getFileHandle()', () => {
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.`);
expect(error.message).toBe(`Failed to execute 'getFileHandle' on 'FileSystemDirectoryHandle': Name is not allowed.`);
}
});
}
Expand All @@ -249,3 +249,75 @@ describe('.getFileHandle()', () => {
expect(subdir).toBeInstanceOf(NodeFileSystemFileHandle);
});
});

describe('.removeEntry()', () => {
test('throws "NotFoundError" DOMException if file not found', async () => {
const {dir} = setup({a: null});
try {
await dir.removeEntry('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.');
}
});

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.removeEntry(invalidName);
throw new Error('Not this error.');
} catch (error) {
expect(error).toBeInstanceOf(TypeError);
expect(error.message).toBe(`Failed to execute 'removeEntry' on 'FileSystemDirectoryHandle': Name is not allowed.`);
}
});
}

test('can delete a file', async () => {
const {dir, fs} = setup({file: 'contents', subdir: null});
expect(fs.statSync('/file').isFile()).toBe(true);
const res = await dir.removeEntry('file');
expect(fs.existsSync('/file')).toBe(false);
expect(res).toBe(undefined);
});

test('can delete a folder', async () => {
const {dir, fs} = setup({dir: null});
expect(fs.statSync('/dir').isDirectory()).toBe(true);
const res = await dir.removeEntry('dir');
expect(fs.existsSync('/dir')).toBe(false);
expect(res).toBe(undefined);
});

test('throws "InvalidModificationError" DOMException if directory has contents', async () => {
const {dir, fs} = setup({
'dir/file': 'contents',
});
expect(fs.statSync('/dir').isDirectory()).toBe(true);
let res: any;
try {
res = await dir.removeEntry('dir');
throw new Error('Not this error.');
} catch (error) {
expect(res).toBe(undefined);
expect(error).toBeInstanceOf(DOMException);
expect(error.name).toBe('InvalidModificationError');
expect(error.message).toBe('The object can not be modified in this way.');
}
});

test('can recursively delete a folder with "recursive" flag', async () => {
const {dir, fs} = setup({
'dir/file': 'contents',
});
expect(fs.statSync('/dir').isDirectory()).toBe(true);
const res = await dir.removeEntry('dir', {recursive: true});
expect(fs.existsSync('/dir')).toBe(false);
expect(res).toBe(undefined);
});
});
4 changes: 3 additions & 1 deletion src/node-to-fsa/types.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,9 @@
import type {IFs} from "..";

/**
* Required Node.js `fs` module functions for File System Access API.
*/
export type NodeFsaFs = Pick<typeof import('fs'), 'promises'>;
export type NodeFsaFs = Pick<IFs, 'promises'>;

export interface NodeFsaContext {
separator: '/' | '\\';
Expand Down
2 changes: 1 addition & 1 deletion src/promises.ts
Original file line number Diff line number Diff line change
Expand Up @@ -88,7 +88,7 @@ export interface IPromisesAPI {
readlink(path: PathLike, options?: IOptions): Promise<TDataOut>;
realpath(path: PathLike, options?: IRealpathOptions | string): Promise<TDataOut>;
rename(oldPath: PathLike, newPath: PathLike): Promise<void>;
rmdir(path: PathLike): Promise<void>;
rmdir(path: PathLike, options?: IRmdirOptions): Promise<void>;
rm(path: PathLike, options?: IRmOptions): Promise<void>;
stat(path: PathLike, options?: IStatOptions): Promise<Stats>;
symlink(target: PathLike, path: PathLike, type?: symlink.Type): Promise<void>;
Expand Down

0 comments on commit dca57a2

Please sign in to comment.