Skip to content

Commit

Permalink
feat: 🎸 implement rmdir() method
Browse files Browse the repository at this point in the history
  • Loading branch information
streamich committed Jun 20, 2023
1 parent 2461853 commit c06734b
Show file tree
Hide file tree
Showing 4 changed files with 82 additions and 35 deletions.
54 changes: 38 additions & 16 deletions src/fsa-to-node/FsaNodeFs.ts
@@ -1,13 +1,14 @@
import { createPromisesApi } from '../node/promises';
import { getDefaultOptsAndCb, getMkdirOptions } from '../node/options';
import { createError, genRndStr6, modeToNumber, nullCheck, pathToFilename, validateCallback } from '../node/util';
import { getDefaultOptsAndCb, getMkdirOptions, getRmdirOptions } from '../node/options';
import { createError, genRndStr6, nullCheck, pathToFilename, validateCallback } from '../node/util';
import { pathToLocation } from './util';
import { MODE } from '../node/constants';
import { strToEncoding } from '../encoding';
import type { FsCallbackApi, FsPromisesApi } from '../node/types';
import type * as misc from '../node/types/misc';
import type * as opts from '../node/types/options';
import type * as fsa from '../fsa/types';
import { MODE } from '../node/constants';
import { strToEncoding } from '../encoding';
import {FsaToNodeConstants} from './constants';

const notImplemented: (...args: unknown[]) => unknown = () => {
throw new Error('Not implemented');
Expand Down Expand Up @@ -208,10 +209,16 @@ export class FsaNodeFs implements FsCallbackApi {
* @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): Promise<fsa.IFileSystemDirectoryHandle> {
private async getDir(path: string[], create: boolean, funcName?: string): Promise<fsa.IFileSystemDirectoryHandle> {
let curr: fsa.IFileSystemDirectoryHandle = this.root;
const options: fsa.GetDirectoryHandleOptions = { create };
for (const name of path) curr = await curr.getDirectoryHandle(name, options);
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;
}

Expand All @@ -238,11 +245,6 @@ export class FsaNodeFs implements FsCallbackApi {
callback(err);
return;
}
case 'TypeMismatchError': {
const err = createError('ENOTDIR', 'mkdir', folder.join('/'));
callback(err);
return;
}
}
}
callback(error);
Expand All @@ -265,11 +267,31 @@ export class FsaNodeFs implements FsCallbackApi {
});
};

rmdir(path: misc.PathLike, callback: misc.TCallback<void>);
rmdir(path: misc.PathLike, options: opts.IRmdirOptions, callback: misc.TCallback<void>);
rmdir(path: misc.PathLike, a: misc.TCallback<void> | opts.IRmdirOptions, b?: misc.TCallback<void>) {
throw new Error('Not implemented');
}
public readonly rmdir: FsCallbackApi['rmdir'] = (path: misc.PathLike, a: misc.TCallback<void> | opts.IRmdirOptions, b?: misc.TCallback<void>) => {
const options: opts.IRmdirOptions = getRmdirOptions(a);
const callback: misc.TCallback<void> = validateCallback(typeof a === 'function' ? a : b);
const [folder, name] = pathToLocation(pathToFilename(path));
this.getDir(folder, false, 'rmdir')
.then(dir => dir.getDirectoryHandle(name).then(() => dir))
.then(dir => dir.removeEntry(name, {recursive: options.recursive ?? false}))
.then(() => callback(null), error => {
if (error && typeof error === 'object') {
switch (error.name) {
case 'NotFoundError': {
const err = createError('ENOENT', 'rmdir', folder.join('/'));
callback(err);
return;
}
case 'InvalidModificationError': {
const err = createError('ENOTEMPTY', 'rmdir', folder.join('/'));
callback(err);
return;
}
}
}
callback(error);
});
};

rm(path: misc.PathLike, callback: misc.TCallback<void>): void;
rm(path: misc.PathLike, options: opts.IRmOptions, callback: misc.TCallback<void>): void;
Expand Down
30 changes: 30 additions & 0 deletions src/fsa-to-node/__tests__/FsaNodeFs.test.ts
Expand Up @@ -72,3 +72,33 @@ describe('.mkdtemp()', () => {
expect(mfs.statSync('/mountpoint/' + dirname).isDirectory()).toBe(true);
});
});

describe('.rmdir()', () => {
test('can remove an empty folder', async () => {
const { fs, mfs } = setup({ folder: { file: 'test' }, 'empty-folder': null });
await fs.promises.rmdir('/empty-folder');
expect(mfs.__vol.toJSON()).toStrictEqual({'/mountpoint/folder/file': 'test'});
});

test('throws when attempts to remove non-empty folder', async () => {
const { fs, mfs } = setup({ folder: { file: 'test' }, 'empty-folder': null });
try {
await fs.promises.rmdir('/folder');
throw new Error('Expected error');
} catch (error) {
expect(error.code).toBe('ENOTEMPTY');
expect(mfs.__vol.toJSON()).toStrictEqual({
'/mountpoint/folder/file': 'test',
'/mountpoint/empty-folder': null,
});
}
});

test('can remove non-empty directory recursively', async () => {
const { fs, mfs } = setup({ folder: { subfolder: {file: 'test'} }, 'empty-folder': null });
await fs.promises.rmdir('/folder', {recursive: true});
expect(mfs.__vol.toJSON()).toStrictEqual({
'/mountpoint/empty-folder': null,
});
});
});
8 changes: 8 additions & 0 deletions src/node/options.ts
Expand Up @@ -53,3 +53,11 @@ export const optsDefaults: opts.IOptions = {

export const getDefaultOpts = optsGenerator<opts.IOptions>(optsDefaults);
export const getDefaultOptsAndCb = optsAndCbGenerator<opts.IOptions, any>(getDefaultOpts);

const rmdirDefaults: opts.IRmdirOptions = {
recursive: false,
};

export const getRmdirOptions = (options): opts.IRmdirOptions => {
return Object.assign({}, rmdirDefaults, options);
};
25 changes: 6 additions & 19 deletions src/volume.ts
Expand Up @@ -20,6 +20,7 @@ import {
getDefaultOptsAndCb,
getMkdirOptions,
getOptions,
getRmdirOptions,
optsAndCbGenerator,
optsDefaults,
optsGenerator,
Expand Down Expand Up @@ -218,20 +219,6 @@ export interface IMkdirOptions {
recursive?: boolean;
}

// Options for `fs.rmdir` and `fs.rmdirSync`
export interface IRmdirOptions {
/** @deprecated */
recursive?: boolean;
maxRetries?: number;
retryDelay?: number;
}
const rmdirDefaults: IRmdirOptions = {
recursive: false,
};
const getRmdirOptions = (options): IRmdirOptions => {
return Object.assign({}, rmdirDefaults, options);
};

export interface IRmOptions {
force?: boolean;
maxRetries?: number;
Expand Down Expand Up @@ -1874,7 +1861,7 @@ export class Volume {
this.wrapAsync(this.mkdtempBase, [prefix, encoding], callback);
}

private rmdirBase(filename: string, options?: IRmdirOptions) {
private rmdirBase(filename: string, options?: opts.IRmdirOptions) {
const opts = getRmdirOptions(options);
const link = this.getLinkAsDirOrThrow(filename, 'rmdir');

Expand All @@ -1884,14 +1871,14 @@ export class Volume {
this.deleteLink(link);
}

rmdirSync(path: PathLike, options?: IRmdirOptions) {
rmdirSync(path: PathLike, options?: opts.IRmdirOptions) {
this.rmdirBase(pathToFilename(path), options);
}

rmdir(path: PathLike, callback: TCallback<void>);
rmdir(path: PathLike, options: IRmdirOptions, callback: TCallback<void>);
rmdir(path: PathLike, a: TCallback<void> | IRmdirOptions, b?: TCallback<void>) {
const opts: IRmdirOptions = getRmdirOptions(a);
rmdir(path: PathLike, options: opts.IRmdirOptions, callback: TCallback<void>);
rmdir(path: PathLike, a: TCallback<void> | opts.IRmdirOptions, b?: TCallback<void>) {
const opts: opts.IRmdirOptions = getRmdirOptions(a);
const callback: TCallback<void> = validateCallback(typeof a === 'function' ? a : b);
this.wrapAsync(this.rmdirBase, [pathToFilename(path), opts], callback);
}
Expand Down

0 comments on commit c06734b

Please sign in to comment.