Skip to content

Commit

Permalink
Merge pull request #754 from streamich/rm
Browse files Browse the repository at this point in the history
add .rm(), .rmSync(), and .promises.rm() methods
  • Loading branch information
streamich authored Sep 19, 2021
2 parents ef6a375 + 31d043b commit 951f774
Show file tree
Hide file tree
Showing 4 changed files with 295 additions and 0 deletions.
135 changes: 135 additions & 0 deletions src/__tests__/volume/rmPromise.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,135 @@
import { create } from '../util';

describe('rmSync', () => {
it('remove directory with two files', async () => {
const vol = create({
'/foo/bar': 'baz',
'/foo/baz': 'qux',
'/oof': 'zab',
});

await vol.promises.rm('/foo', {force: true, recursive: true});

expect(vol.toJSON()).toEqual({
'/oof': 'zab',
});
});

it('removes a single file', async () => {
const vol = create({
'/a/b/c.txt': 'content',
});

await vol.promises.rm('/a/b/c.txt');

expect(vol.toJSON()).toEqual({
'/a/b': null,
});
});

describe('when file does not exist', () => {
it('throws by default', async () => {
const vol = create({
'/foo.txt': 'content',
});

let error;
try {
await vol.promises.rm('/bar.txt');
throw new Error('Not this');
} catch (err) {
error = err;
}

expect(error).toEqual(new Error("ENOENT: no such file or directory, stat '/bar.txt'"));
});

it('does not throw if "force" is set to true', async () => {
const vol = create({
'/foo.txt': 'content',
});

await vol.promises.rm('/bar.txt', {force: true});
});
});

describe('when deleting a directory', () => {
it('throws by default', async () => {
const vol = create({
'/usr/bin/bash': '...',
});

let error;
try {
await vol.promises.rm('/usr/bin')
throw new Error('Not this');
} catch (err) {
error = err;
}

expect(error).toEqual(new Error("[ERR_FS_EISDIR]: Path is a directory: rm returned EISDIR (is a directory) /usr/bin"));
});

it('throws by when force flag is set', async () => {
const vol = create({
'/usr/bin/bash': '...',
});

let error;
try {
await vol.promises.rm('/usr/bin', {force: true});
throw new Error('Not this');
} catch (err) {
error = err;
}

expect(error).toEqual(new Error("[ERR_FS_EISDIR]: Path is a directory: rm returned EISDIR (is a directory) /usr/bin"));
});

it('deletes all directory contents when recursive flag is set', async () => {
const vol = create({
'/usr/bin/bash': '...',
});

await vol.promises.rm('/usr/bin', {recursive: true});

expect(vol.toJSON()).toEqual({'/usr': null});
});

it('deletes all directory contents recursively when recursive flag is set', async () => {
const vol = create({
'/a/a/a': '1',
'/a/a/b': '2',
'/a/a/c': '3',
'/a/b/a': '4',
'/a/b/b': '5',
'/a/c/a': '6',
});

await vol.promises.rm('/a/a', {recursive: true});

expect(vol.toJSON()).toEqual({
'/a/b/a': '4',
'/a/b/b': '5',
'/a/c/a': '6',
});

await vol.promises.rm('/a/c', {recursive: true});

expect(vol.toJSON()).toEqual({
'/a/b/a': '4',
'/a/b/b': '5',
});

await vol.promises.rm('/a/b', {recursive: true});

expect(vol.toJSON()).toEqual({
'/a': null,
});

await vol.promises.rm('/a', {recursive: true});

expect(vol.toJSON()).toEqual({});
});
});
});
111 changes: 111 additions & 0 deletions src/__tests__/volume/rmSync.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,111 @@
import { create } from '../util';

describe('rmSync', () => {
it('remove directory with two files', () => {
const vol = create({
'/foo/bar': 'baz',
'/foo/baz': 'qux',
'/oof': 'zab',
});

vol.rmSync('/foo', {force: true, recursive: true});

expect(vol.toJSON()).toEqual({
'/oof': 'zab',
});
});

it('removes a single file', () => {
const vol = create({
'/a/b/c.txt': 'content',
});

vol.rmSync('/a/b/c.txt');

expect(vol.toJSON()).toEqual({
'/a/b': null,
});
});

describe('when file does not exist', () => {
it('throws by default', () => {
const vol = create({
'/foo.txt': 'content',
});

expect(() => vol.rmSync('/bar.txt')).toThrowError(new Error("ENOENT: no such file or directory, stat '/bar.txt'"));
});

it('does not throw if "force" is set to true', () => {
const vol = create({
'/foo.txt': 'content',
});

vol.rmSync('/bar.txt', {force: true});
});
});

describe('when deleting a directory', () => {
it('throws by default', () => {
const vol = create({
'/usr/bin/bash': '...',
});

expect(() => vol.rmSync('/usr/bin')).toThrowError(new Error("[ERR_FS_EISDIR]: Path is a directory: rm returned EISDIR (is a directory) /usr/bin"));
});

it('throws by when force flag is set', () => {
const vol = create({
'/usr/bin/bash': '...',
});

expect(() => vol.rmSync('/usr/bin', {force: true})).toThrowError(new Error("[ERR_FS_EISDIR]: Path is a directory: rm returned EISDIR (is a directory) /usr/bin"));
});

it('deletes all directory contents when recursive flag is set', () => {
const vol = create({
'/usr/bin/bash': '...',
});

vol.rmSync('/usr/bin', {recursive: true});

expect(vol.toJSON()).toEqual({'/usr': null});
});

it('deletes all directory contents recursively when recursive flag is set', () => {
const vol = create({
'/a/a/a': '1',
'/a/a/b': '2',
'/a/a/c': '3',
'/a/b/a': '4',
'/a/b/b': '5',
'/a/c/a': '6',
});

vol.rmSync('/a/a', {recursive: true});

expect(vol.toJSON()).toEqual({
'/a/b/a': '4',
'/a/b/b': '5',
'/a/c/a': '6',
});

vol.rmSync('/a/c', {recursive: true});

expect(vol.toJSON()).toEqual({
'/a/b/a': '4',
'/a/b/b': '5',
});

vol.rmSync('/a/b', {recursive: true});

expect(vol.toJSON()).toEqual({
'/a': null,
});

vol.rmSync('/a', {recursive: true});

expect(vol.toJSON()).toEqual({});
});
});
});
6 changes: 6 additions & 0 deletions src/promises.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ import {
IRealpathOptions,
IWriteFileOptions,
IStatOptions,
IRmOptions,
} from './volume';
import Stats from './Stats';
import Dirent from './Dirent';
Expand Down Expand Up @@ -86,6 +87,7 @@ export interface IPromisesAPI {
realpath(path: PathLike, options?: IRealpathOptions | string): Promise<TDataOut>;
rename(oldPath: PathLike, newPath: PathLike): Promise<void>;
rmdir(path: PathLike): 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>;
truncate(path: PathLike, len?: number): Promise<void>;
Expand Down Expand Up @@ -245,6 +247,10 @@ export default function createPromisesApi(vol: Volume): null | IPromisesAPI {
return promisify(vol, 'rmdir')(path);
},

rm(path: PathLike, options?: IRmOptions): Promise<void> {
return promisify(vol, 'rm')(path, options);
},

stat(path: PathLike, options?: IStatOptions): Promise<Stats> {
return promisify(vol, 'stat')(path, options);
},
Expand Down
43 changes: 43 additions & 0 deletions src/volume.ts
Original file line number Diff line number Diff line change
Expand Up @@ -99,6 +99,7 @@ const EACCES = 'EACCES';
const EISDIR = 'EISDIR';
const ENOTEMPTY = 'ENOTEMPTY';
const ENOSYS = 'ENOSYS';
const ERR_FS_EISDIR = 'ERR_FS_EISDIR';

function formatError(errorCode: string, func = '', path = '', path2 = '') {
let pathFormatted = '';
Expand Down Expand Up @@ -130,6 +131,8 @@ function formatError(errorCode: string, func = '', path = '', path2 = '') {
return `EMFILE: too many open files, ${func}${pathFormatted}`;
case ENOSYS:
return `ENOSYS: function not implemented, ${func}${pathFormatted}`;
case ERR_FS_EISDIR:
return `[ERR_FS_EISDIR]: Path is a directory: ${func} returned EISDIR (is a directory) ${path}`
default:
return `${errorCode}: error occurred, ${func}${pathFormatted}`;
}
Expand Down Expand Up @@ -342,6 +345,15 @@ const getRmdirOptions = (options): IRmdirOptions => {
return Object.assign({}, rmdirDefaults, options);
};

export interface IRmOptions {
force?: boolean;
maxRetries?: number;
recursive?: boolean;
retryDelay?: number;
}
const getRmOpts = optsGenerator<IOptions>(optsDefaults);
const getRmOptsAndCb = optsAndCbGenerator<IRmOptions, any>(getRmOpts);

// Options for `fs.readdir` and `fs.readdirSync`
export interface IReaddirOptions extends IOptions {
withFileTypes?: boolean;
Expand Down Expand Up @@ -799,6 +811,10 @@ export class Volume {
return file;
}

/**
* @todo This is not used anymore. Remove.
*/
/*
private getNodeByIdOrCreate(id: TFileId, flags: number, perm: number): Node {
if (typeof id === 'number') {
const file = this.getFileByFd(id);
Expand All @@ -822,6 +838,7 @@ export class Volume {
throw createError(ENOENT, 'getNodeByIdOrCreate', pathToFilename(id));
}
}
*/

private wrapAsync(method: (...args) => void, args: any[], callback: TCallback<any>) {
validateCallback(callback);
Expand Down Expand Up @@ -1971,6 +1988,32 @@ export class Volume {
this.wrapAsync(this.rmdirBase, [pathToFilename(path), opts], callback);
}

private rmBase(filename: string, options: IRmOptions = {}): void {
const link = this.getResolvedLink(filename);
if (!link) {
// "stat" is used to match Node's native error message.
if (!options.force) throw createError(ENOENT, 'stat', filename);
return;
}
if (link.getNode().isDirectory()) {
if (!options.recursive) {
throw createError(ERR_FS_EISDIR, 'rm', filename);
}
}
this.deleteLink(link);
}

public rmSync(path: PathLike, options?: IRmOptions): void {
this.rmBase(pathToFilename(path), options);
}

public rm(path: PathLike, callback: TCallback<void>): void;
public rm(path: PathLike, options: IRmOptions, callback: TCallback<void>): void;
public rm(path: PathLike, a: TCallback<void> | IRmOptions, b?: TCallback<void>): void {
const [opts, callback] = getRmOptsAndCb(a, b);
this.wrapAsync(this.rmBase, [pathToFilename(path), opts], callback);
}

private fchmodBase(fd: number, modeNum: number) {
const file = this.getFileByFdOrThrow(fd, 'fchmod');
file.chmod(modeNum);
Expand Down

0 comments on commit 951f774

Please sign in to comment.