Skip to content

Commit 951f774

Browse files
authored
Merge pull request #754 from streamich/rm
add .rm(), .rmSync(), and .promises.rm() methods
2 parents ef6a375 + 31d043b commit 951f774

File tree

4 files changed

+295
-0
lines changed

4 files changed

+295
-0
lines changed
Lines changed: 135 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,135 @@
1+
import { create } from '../util';
2+
3+
describe('rmSync', () => {
4+
it('remove directory with two files', async () => {
5+
const vol = create({
6+
'/foo/bar': 'baz',
7+
'/foo/baz': 'qux',
8+
'/oof': 'zab',
9+
});
10+
11+
await vol.promises.rm('/foo', {force: true, recursive: true});
12+
13+
expect(vol.toJSON()).toEqual({
14+
'/oof': 'zab',
15+
});
16+
});
17+
18+
it('removes a single file', async () => {
19+
const vol = create({
20+
'/a/b/c.txt': 'content',
21+
});
22+
23+
await vol.promises.rm('/a/b/c.txt');
24+
25+
expect(vol.toJSON()).toEqual({
26+
'/a/b': null,
27+
});
28+
});
29+
30+
describe('when file does not exist', () => {
31+
it('throws by default', async () => {
32+
const vol = create({
33+
'/foo.txt': 'content',
34+
});
35+
36+
let error;
37+
try {
38+
await vol.promises.rm('/bar.txt');
39+
throw new Error('Not this');
40+
} catch (err) {
41+
error = err;
42+
}
43+
44+
expect(error).toEqual(new Error("ENOENT: no such file or directory, stat '/bar.txt'"));
45+
});
46+
47+
it('does not throw if "force" is set to true', async () => {
48+
const vol = create({
49+
'/foo.txt': 'content',
50+
});
51+
52+
await vol.promises.rm('/bar.txt', {force: true});
53+
});
54+
});
55+
56+
describe('when deleting a directory', () => {
57+
it('throws by default', async () => {
58+
const vol = create({
59+
'/usr/bin/bash': '...',
60+
});
61+
62+
let error;
63+
try {
64+
await vol.promises.rm('/usr/bin')
65+
throw new Error('Not this');
66+
} catch (err) {
67+
error = err;
68+
}
69+
70+
expect(error).toEqual(new Error("[ERR_FS_EISDIR]: Path is a directory: rm returned EISDIR (is a directory) /usr/bin"));
71+
});
72+
73+
it('throws by when force flag is set', async () => {
74+
const vol = create({
75+
'/usr/bin/bash': '...',
76+
});
77+
78+
let error;
79+
try {
80+
await vol.promises.rm('/usr/bin', {force: true});
81+
throw new Error('Not this');
82+
} catch (err) {
83+
error = err;
84+
}
85+
86+
expect(error).toEqual(new Error("[ERR_FS_EISDIR]: Path is a directory: rm returned EISDIR (is a directory) /usr/bin"));
87+
});
88+
89+
it('deletes all directory contents when recursive flag is set', async () => {
90+
const vol = create({
91+
'/usr/bin/bash': '...',
92+
});
93+
94+
await vol.promises.rm('/usr/bin', {recursive: true});
95+
96+
expect(vol.toJSON()).toEqual({'/usr': null});
97+
});
98+
99+
it('deletes all directory contents recursively when recursive flag is set', async () => {
100+
const vol = create({
101+
'/a/a/a': '1',
102+
'/a/a/b': '2',
103+
'/a/a/c': '3',
104+
'/a/b/a': '4',
105+
'/a/b/b': '5',
106+
'/a/c/a': '6',
107+
});
108+
109+
await vol.promises.rm('/a/a', {recursive: true});
110+
111+
expect(vol.toJSON()).toEqual({
112+
'/a/b/a': '4',
113+
'/a/b/b': '5',
114+
'/a/c/a': '6',
115+
});
116+
117+
await vol.promises.rm('/a/c', {recursive: true});
118+
119+
expect(vol.toJSON()).toEqual({
120+
'/a/b/a': '4',
121+
'/a/b/b': '5',
122+
});
123+
124+
await vol.promises.rm('/a/b', {recursive: true});
125+
126+
expect(vol.toJSON()).toEqual({
127+
'/a': null,
128+
});
129+
130+
await vol.promises.rm('/a', {recursive: true});
131+
132+
expect(vol.toJSON()).toEqual({});
133+
});
134+
});
135+
});
Lines changed: 111 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,111 @@
1+
import { create } from '../util';
2+
3+
describe('rmSync', () => {
4+
it('remove directory with two files', () => {
5+
const vol = create({
6+
'/foo/bar': 'baz',
7+
'/foo/baz': 'qux',
8+
'/oof': 'zab',
9+
});
10+
11+
vol.rmSync('/foo', {force: true, recursive: true});
12+
13+
expect(vol.toJSON()).toEqual({
14+
'/oof': 'zab',
15+
});
16+
});
17+
18+
it('removes a single file', () => {
19+
const vol = create({
20+
'/a/b/c.txt': 'content',
21+
});
22+
23+
vol.rmSync('/a/b/c.txt');
24+
25+
expect(vol.toJSON()).toEqual({
26+
'/a/b': null,
27+
});
28+
});
29+
30+
describe('when file does not exist', () => {
31+
it('throws by default', () => {
32+
const vol = create({
33+
'/foo.txt': 'content',
34+
});
35+
36+
expect(() => vol.rmSync('/bar.txt')).toThrowError(new Error("ENOENT: no such file or directory, stat '/bar.txt'"));
37+
});
38+
39+
it('does not throw if "force" is set to true', () => {
40+
const vol = create({
41+
'/foo.txt': 'content',
42+
});
43+
44+
vol.rmSync('/bar.txt', {force: true});
45+
});
46+
});
47+
48+
describe('when deleting a directory', () => {
49+
it('throws by default', () => {
50+
const vol = create({
51+
'/usr/bin/bash': '...',
52+
});
53+
54+
expect(() => vol.rmSync('/usr/bin')).toThrowError(new Error("[ERR_FS_EISDIR]: Path is a directory: rm returned EISDIR (is a directory) /usr/bin"));
55+
});
56+
57+
it('throws by when force flag is set', () => {
58+
const vol = create({
59+
'/usr/bin/bash': '...',
60+
});
61+
62+
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"));
63+
});
64+
65+
it('deletes all directory contents when recursive flag is set', () => {
66+
const vol = create({
67+
'/usr/bin/bash': '...',
68+
});
69+
70+
vol.rmSync('/usr/bin', {recursive: true});
71+
72+
expect(vol.toJSON()).toEqual({'/usr': null});
73+
});
74+
75+
it('deletes all directory contents recursively when recursive flag is set', () => {
76+
const vol = create({
77+
'/a/a/a': '1',
78+
'/a/a/b': '2',
79+
'/a/a/c': '3',
80+
'/a/b/a': '4',
81+
'/a/b/b': '5',
82+
'/a/c/a': '6',
83+
});
84+
85+
vol.rmSync('/a/a', {recursive: true});
86+
87+
expect(vol.toJSON()).toEqual({
88+
'/a/b/a': '4',
89+
'/a/b/b': '5',
90+
'/a/c/a': '6',
91+
});
92+
93+
vol.rmSync('/a/c', {recursive: true});
94+
95+
expect(vol.toJSON()).toEqual({
96+
'/a/b/a': '4',
97+
'/a/b/b': '5',
98+
});
99+
100+
vol.rmSync('/a/b', {recursive: true});
101+
102+
expect(vol.toJSON()).toEqual({
103+
'/a': null,
104+
});
105+
106+
vol.rmSync('/a', {recursive: true});
107+
108+
expect(vol.toJSON()).toEqual({});
109+
});
110+
});
111+
});

src/promises.ts

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ import {
1313
IRealpathOptions,
1414
IWriteFileOptions,
1515
IStatOptions,
16+
IRmOptions,
1617
} from './volume';
1718
import Stats from './Stats';
1819
import Dirent from './Dirent';
@@ -86,6 +87,7 @@ export interface IPromisesAPI {
8687
realpath(path: PathLike, options?: IRealpathOptions | string): Promise<TDataOut>;
8788
rename(oldPath: PathLike, newPath: PathLike): Promise<void>;
8889
rmdir(path: PathLike): Promise<void>;
90+
rm(path: PathLike, options?: IRmOptions): Promise<void>;
8991
stat(path: PathLike, options?: IStatOptions): Promise<Stats>;
9092
symlink(target: PathLike, path: PathLike, type?: symlink.Type): Promise<void>;
9193
truncate(path: PathLike, len?: number): Promise<void>;
@@ -245,6 +247,10 @@ export default function createPromisesApi(vol: Volume): null | IPromisesAPI {
245247
return promisify(vol, 'rmdir')(path);
246248
},
247249

250+
rm(path: PathLike, options?: IRmOptions): Promise<void> {
251+
return promisify(vol, 'rm')(path, options);
252+
},
253+
248254
stat(path: PathLike, options?: IStatOptions): Promise<Stats> {
249255
return promisify(vol, 'stat')(path, options);
250256
},

src/volume.ts

Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -99,6 +99,7 @@ const EACCES = 'EACCES';
9999
const EISDIR = 'EISDIR';
100100
const ENOTEMPTY = 'ENOTEMPTY';
101101
const ENOSYS = 'ENOSYS';
102+
const ERR_FS_EISDIR = 'ERR_FS_EISDIR';
102103

103104
function formatError(errorCode: string, func = '', path = '', path2 = '') {
104105
let pathFormatted = '';
@@ -130,6 +131,8 @@ function formatError(errorCode: string, func = '', path = '', path2 = '') {
130131
return `EMFILE: too many open files, ${func}${pathFormatted}`;
131132
case ENOSYS:
132133
return `ENOSYS: function not implemented, ${func}${pathFormatted}`;
134+
case ERR_FS_EISDIR:
135+
return `[ERR_FS_EISDIR]: Path is a directory: ${func} returned EISDIR (is a directory) ${path}`
133136
default:
134137
return `${errorCode}: error occurred, ${func}${pathFormatted}`;
135138
}
@@ -342,6 +345,15 @@ const getRmdirOptions = (options): IRmdirOptions => {
342345
return Object.assign({}, rmdirDefaults, options);
343346
};
344347

348+
export interface IRmOptions {
349+
force?: boolean;
350+
maxRetries?: number;
351+
recursive?: boolean;
352+
retryDelay?: number;
353+
}
354+
const getRmOpts = optsGenerator<IOptions>(optsDefaults);
355+
const getRmOptsAndCb = optsAndCbGenerator<IRmOptions, any>(getRmOpts);
356+
345357
// Options for `fs.readdir` and `fs.readdirSync`
346358
export interface IReaddirOptions extends IOptions {
347359
withFileTypes?: boolean;
@@ -799,6 +811,10 @@ export class Volume {
799811
return file;
800812
}
801813

814+
/**
815+
* @todo This is not used anymore. Remove.
816+
*/
817+
/*
802818
private getNodeByIdOrCreate(id: TFileId, flags: number, perm: number): Node {
803819
if (typeof id === 'number') {
804820
const file = this.getFileByFd(id);
@@ -822,6 +838,7 @@ export class Volume {
822838
throw createError(ENOENT, 'getNodeByIdOrCreate', pathToFilename(id));
823839
}
824840
}
841+
*/
825842

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

1991+
private rmBase(filename: string, options: IRmOptions = {}): void {
1992+
const link = this.getResolvedLink(filename);
1993+
if (!link) {
1994+
// "stat" is used to match Node's native error message.
1995+
if (!options.force) throw createError(ENOENT, 'stat', filename);
1996+
return;
1997+
}
1998+
if (link.getNode().isDirectory()) {
1999+
if (!options.recursive) {
2000+
throw createError(ERR_FS_EISDIR, 'rm', filename);
2001+
}
2002+
}
2003+
this.deleteLink(link);
2004+
}
2005+
2006+
public rmSync(path: PathLike, options?: IRmOptions): void {
2007+
this.rmBase(pathToFilename(path), options);
2008+
}
2009+
2010+
public rm(path: PathLike, callback: TCallback<void>): void;
2011+
public rm(path: PathLike, options: IRmOptions, callback: TCallback<void>): void;
2012+
public rm(path: PathLike, a: TCallback<void> | IRmOptions, b?: TCallback<void>): void {
2013+
const [opts, callback] = getRmOptsAndCb(a, b);
2014+
this.wrapAsync(this.rmBase, [pathToFilename(path), opts], callback);
2015+
}
2016+
19742017
private fchmodBase(fd: number, modeNum: number) {
19752018
const file = this.getFileByFdOrThrow(fd, 'fchmod');
19762019
file.chmod(modeNum);

0 commit comments

Comments
 (0)