diff --git a/src/Dir.ts b/src/Dir.ts new file mode 100644 index 000000000..2febe385f --- /dev/null +++ b/src/Dir.ts @@ -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) { + 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(obj: T, fn: keyof T): (...args: any[]) => Promise { + return (...args) => + new Promise((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; + close(callback?: (err?: Error) => void): void; + close(callback?: unknown): void | Promise { + 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; + read(callback?: (err: Error | null, dir?: IDirent | null) => void): void; + read(callback?: unknown): void | Promise { + 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 { + 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 { + throw new Error('Not implemented'); + }, + }; + } +} diff --git a/src/__tests__/volume.test.ts b/src/__tests__/volume.test.ts index a8b1ffb3b..bc5ba0ff0 100644 --- a/src/__tests__/volume.test.ts +++ b/src/__tests__/volume.test.ts @@ -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; diff --git a/src/node/options.ts b/src/node/options.ts index 656144b8c..a5f63e1b2 100644 --- a/src/node/options.ts +++ b/src/node/options.ts @@ -81,6 +81,14 @@ export const getReaddirOptsAndCb = optsAndCbGenerator(opendirDefaults); +export const getOpendirOptsAndCb = optsAndCbGenerator(getOpendirOptions); + const appendFileDefaults: opts.IAppendFileOptions = { encoding: 'utf8', mode: MODE.DEFAULT, diff --git a/src/volume.ts b/src/volume.ts index ac5e0ccc2..43f31406f 100644 --- a/src/volume.ts +++ b/src/volume.ts @@ -38,6 +38,8 @@ import { getRealpathOptions, getWriteFileOptions, writeFileDefaults, + getOpendirOptsAndCb, + getOpendirOptions, } from './node/options'; import { validateCallback, @@ -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 { @@ -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); + opendir(path: PathLike, options: opts.IOpendirOptions | string, callback: TCallback); + 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) {