Skip to content

Commit

Permalink
Merge pull request #1043 from ddziara/volume-opendir
Browse files Browse the repository at this point in the history
Volume implementation of opendir method
  • Loading branch information
streamich committed Jul 27, 2024
2 parents c9d7497 + f5aac21 commit 687e557
Show file tree
Hide file tree
Showing 4 changed files with 188 additions and 2 deletions.
151 changes: 151 additions & 0 deletions src/Dir.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,151 @@
import { Link } from './node';
import { validateCallback } from './node/util';
import * as opts from './node/types/options';
import Dirent from './Dirent';
import type { IDir, IDirent, TCallback } from './node/types/misc';

/**
* A directory stream, like `fs.Dir`.
*/
export class Dir implements IDir {
private iteratorInfo: IterableIterator<[string, Link | undefined]>[] = [];

constructor(
protected readonly link: Link,
protected options: opts.IOpendirOptions,
) {
this.path = link.getParentPath();
this.iteratorInfo.push(link.children[Symbol.iterator]());
}

private wrapAsync(method: (...args) => void, args: any[], callback: TCallback<any>) {
validateCallback(callback);
setImmediate(() => {
let result;
try {
result = method.apply(this, args);
} catch (err) {
callback(err);
return;
}
callback(null, result);
});
}

private isFunction(x: any): x is Function {
return typeof x === 'function';
}

private promisify<T>(obj: T, fn: keyof T): (...args: any[]) => Promise<any> {
return (...args) =>
new Promise<void>((resolve, reject) => {
if (this.isFunction(obj[fn])) {
obj[fn].bind(obj)(...args, (error: Error, result: any) => {
if (error) reject(error);
resolve(result);
});
} else {
reject('Not a function');
}
});
}

private closeBase(): void {}

private readBase(iteratorInfo: IterableIterator<[string, Link | undefined]>[]): IDirent | null {
let done: boolean | undefined;
let value: [string, Link | undefined];
let name: string;
let link: Link | undefined;
do {
do {
({ done, value } = iteratorInfo[iteratorInfo.length - 1].next());
if (!done) {
[name, link] = value;
} else {
break;
}
} while (name === '.' || name === '..');
if (done) {
iteratorInfo.pop();
if (iteratorInfo.length === 0) {
break;
} else {
done = false;
}
} else {
if (this.options.recursive && link!.children.size) {
iteratorInfo.push(link!.children[Symbol.iterator]());
}
return Dirent.build(link!, this.options.encoding);
}
} while (!done);
return null;
}

// ------------------------------------------------------------- IDir

public readonly path: string;

closeBaseAsync(callback: (err?: Error) => void): void {
this.wrapAsync(this.closeBase, [], callback);
}

close(): Promise<void>;
close(callback?: (err?: Error) => void): void;
close(callback?: unknown): void | Promise<void> {
if (typeof callback === 'function') {
this.closeBaseAsync(callback as (err?: Error) => void);
} else {
return this.promisify(this, 'closeBaseAsync')();
}
}

closeSync(): void {
this.closeBase();
}

readBaseAsync(callback: (err: Error | null, dir?: IDirent | null) => void): void {
this.wrapAsync(this.readBase, [this.iteratorInfo], callback);
}

read(): Promise<IDirent | null>;
read(callback?: (err: Error | null, dir?: IDirent | null) => void): void;
read(callback?: unknown): void | Promise<IDirent | null> {
if (typeof callback === 'function') {
this.readBaseAsync(callback as (err: Error | null, dir?: IDirent | null) => void);
} else {
return this.promisify(this, 'readBaseAsync')();
}
}

readSync(): IDirent | null {
return this.readBase(this.iteratorInfo);
}

[Symbol.asyncIterator](): AsyncIterableIterator<IDirent> {
const iteratorInfo: IterableIterator<[string, Link | undefined]>[] = [];
const _this = this;
iteratorInfo.push(_this.link.children[Symbol.iterator]());
// auxiliary object so promisify() can be used
const o = {
readBaseAsync(callback: (err: Error | null, dir?: IDirent | null) => void): void {
_this.wrapAsync(_this.readBase, [iteratorInfo], callback);
},
};
return {
async next() {
const dirEnt = await _this.promisify(o, 'readBaseAsync')();

if (dirEnt !== null) {
return { done: false, value: dirEnt };
} else {
return { done: true, value: undefined };
}
},
[Symbol.asyncIterator](): AsyncIterableIterator<IDirent> {
throw new Error('Not implemented');
},
};
}
}
1 change: 1 addition & 0 deletions src/__tests__/volume.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import { tryGetChild, tryGetChildNode } from './util';
import { genRndStr6 } from '../node/util';
import queueMicrotask from '../queueMicrotask';
import { constants } from '../constants';
import { IDirent } from '../node/types/misc';

const { O_RDWR, O_SYMLINK } = constants;

Expand Down
8 changes: 8 additions & 0 deletions src/node/options.ts
Original file line number Diff line number Diff line change
Expand Up @@ -81,6 +81,14 @@ export const getReaddirOptsAndCb = optsAndCbGenerator<opts.IReaddirOptions, misc
getReaddirOptions,
);

const opendirDefaults: opts.IOpendirOptions = {
encoding: 'utf8',
bufferSize: 32,
recursive: false,
};
export const getOpendirOptions = optsGenerator<opts.IOpendirOptions>(opendirDefaults);
export const getOpendirOptsAndCb = optsAndCbGenerator<opts.IOpendirOptions, misc.IDir>(getOpendirOptions);

const appendFileDefaults: opts.IAppendFileOptions = {
encoding: 'utf8',
mode: MODE.DEFAULT,
Expand Down
30 changes: 28 additions & 2 deletions src/volume.ts
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,8 @@ import {
getRealpathOptions,
getWriteFileOptions,
writeFileDefaults,
getOpendirOptsAndCb,
getOpendirOptions,
} from './node/options';
import {
validateCallback,
Expand All @@ -59,6 +61,7 @@ import {
import type { PathLike, symlink } from 'fs';
import type { FsPromisesApi, FsSynchronousApi } from './node/types';
import { fsSynchronousApiList } from './node/lists/fsSynchronousApiList';
import { Dir } from './Dir';

const resolveCrossPlatform = pathModule.resolve;
const {
Expand Down Expand Up @@ -2010,13 +2013,36 @@ export class Volume implements FsCallbackApi, FsSynchronousApi {
public cpSync: FsSynchronousApi['cpSync'] = notImplemented;
public lutimesSync: FsSynchronousApi['lutimesSync'] = notImplemented;
public statfsSync: FsSynchronousApi['statfsSync'] = notImplemented;
public opendirSync: FsSynchronousApi['opendirSync'] = notImplemented;

public cp: FsCallbackApi['cp'] = notImplemented;
public lutimes: FsCallbackApi['lutimes'] = notImplemented;
public statfs: FsCallbackApi['statfs'] = notImplemented;
public openAsBlob: FsCallbackApi['openAsBlob'] = notImplemented;
public opendir: FsCallbackApi['opendir'] = notImplemented;

private opendirBase(filename: string, options: opts.IOpendirOptions): Dir {
const steps = filenameToSteps(filename);
const link: Link | null = this.getResolvedLink(steps);
if (!link) throw createError(ENOENT, 'opendir', filename);

const node = link.getNode();
if (!node.isDirectory()) throw createError(ENOTDIR, 'scandir', filename);

return new Dir(link, options);
}

opendirSync(path: PathLike, options?: opts.IOpendirOptions | string): Dir {
const opts = getOpendirOptions(options);
const filename = pathToFilename(path);
return this.opendirBase(filename, opts);
}

opendir(path: PathLike, callback: TCallback<Dir>);
opendir(path: PathLike, options: opts.IOpendirOptions | string, callback: TCallback<Dir>);
opendir(path: PathLike, a?, b?) {
const [options, callback] = getOpendirOptsAndCb(a, b);
const filename = pathToFilename(path);
this.wrapAsync(this.opendirBase, [filename, options], callback);
}
}

function emitStop(self) {
Expand Down

0 comments on commit 687e557

Please sign in to comment.