From cd54e9ba0a8b297772c868964fe9659397cd725b Mon Sep 17 00:00:00 2001 From: streamich Date: Fri, 16 Jun 2023 12:41:05 +0200 Subject: [PATCH] =?UTF-8?q?feat:=20=F0=9F=8E=B8=20implement=20.mkdtemp()?= =?UTF-8?q?=20method?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/fsa-to-node/FsaNodeFs.ts | 21 +++-- src/fsa-to-node/__tests__/FsaNodeFs.test.ts | 9 +++ src/node/options.ts | 43 ++++++++++ src/node/types/callback.ts | 4 +- src/node/util.ts | 6 ++ src/volume.ts | 90 +++++---------------- 6 files changed, 92 insertions(+), 81 deletions(-) diff --git a/src/fsa-to-node/FsaNodeFs.ts b/src/fsa-to-node/FsaNodeFs.ts index 2a6f6b1a4..125928301 100644 --- a/src/fsa-to-node/FsaNodeFs.ts +++ b/src/fsa-to-node/FsaNodeFs.ts @@ -1,11 +1,13 @@ import { createPromisesApi } from '../node/promises'; -import { getMkdirOptions } from '../node/options'; -import { createError, modeToNumber, pathToFilename, validateCallback } from '../node/util'; +import { getDefaultOptsAndCb, getMkdirOptions } from '../node/options'; +import { createError, genRndStr6, modeToNumber, nullCheck, pathToFilename, validateCallback } from '../node/util'; import { pathToLocation } from './util'; 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'; const notImplemented: (...args: unknown[]) => unknown = () => { throw new Error('Not implemented'); @@ -248,11 +250,16 @@ export class FsaNodeFs implements FsCallbackApi { ); }; - mkdtemp(prefix: string, callback: misc.TCallback): void; - mkdtemp(prefix: string, options: opts.IOptions, callback: misc.TCallback); - mkdtemp(prefix: string, a: misc.TCallback | opts.IOptions, b?: misc.TCallback) { - throw new Error('Not implemented'); - } + public readonly mkdtemp: FsCallbackApi['mkdtemp'] = (prefix: string, a: misc.TCallback | opts.IOptions, b?: misc.TCallback) => { + const [{ encoding }, callback] = getDefaultOptsAndCb(a, b); + if (!prefix || typeof prefix !== 'string') throw new TypeError('filename prefix is required'); + if (!nullCheck(prefix)) return; + const filename = prefix + genRndStr6(); + this.mkdir(filename, MODE.DIR, (err) => { + if (err) callback(err); + else callback(null, strToEncoding(filename, encoding)); + }); + }; rmdir(path: misc.PathLike, callback: misc.TCallback); rmdir(path: misc.PathLike, options: opts.IRmdirOptions, callback: misc.TCallback); diff --git a/src/fsa-to-node/__tests__/FsaNodeFs.test.ts b/src/fsa-to-node/__tests__/FsaNodeFs.test.ts index 2fefff549..364fec5cc 100644 --- a/src/fsa-to-node/__tests__/FsaNodeFs.test.ts +++ b/src/fsa-to-node/__tests__/FsaNodeFs.test.ts @@ -63,3 +63,12 @@ describe('.mkdir()', () => { } }); }); + +describe('.mkdtemp()', () => { + test('can create a temporary folder', async () => { + const { fs, mfs } = setup(); + const dirname = await fs.promises.mkdtemp('prefix--') as string; + expect(dirname.startsWith('prefix--')).toBe(true); + expect(mfs.statSync('/mountpoint/' + dirname).isDirectory()).toBe(true); + }); +}); diff --git a/src/node/options.ts b/src/node/options.ts index 87d618b54..993779042 100644 --- a/src/node/options.ts +++ b/src/node/options.ts @@ -1,5 +1,8 @@ import type * as opts from './types/options'; import { MODE } from './constants'; +import {assertEncoding} from '../encoding'; +import * as misc from './types/misc'; +import {validateCallback} from './util'; const mkdirDefaults: opts.IMkdirOptions = { mode: MODE.DIR, @@ -10,3 +13,43 @@ export const getMkdirOptions = (options): opts.IMkdirOptions => { if (typeof options === 'number') return Object.assign({}, mkdirDefaults, { mode: options }); return Object.assign({}, mkdirDefaults, options); }; + +const ERRSTR_OPTS = tipeof => `Expected options to be either an object or a string, but got ${tipeof} instead`; + +export function getOptions(defaults: T, options?: T | string): T { + let opts: T; + if (!options) return defaults; + else { + const tipeof = typeof options; + switch (tipeof) { + case 'string': + opts = Object.assign({}, defaults, { encoding: options as string }); + break; + case 'object': + opts = Object.assign({}, defaults, options); + break; + default: + throw TypeError(ERRSTR_OPTS(tipeof)); + } + } + + if (opts.encoding !== 'buffer') assertEncoding(opts.encoding); + + return opts; +} + +export function optsGenerator(defaults: TOpts): (opts) => TOpts { + return options => getOptions(defaults, options); +} + +export function optsAndCbGenerator(getOpts): (options, callback?) => [TOpts, misc.TCallback] { + return (options, callback?) => + typeof options === 'function' ? [getOpts(), options] : [getOpts(options), validateCallback(callback)]; +} + +export const optsDefaults: opts.IOptions = { + encoding: 'utf8', +}; + +export const getDefaultOpts = optsGenerator(optsDefaults); +export const getDefaultOptsAndCb = optsAndCbGenerator(getDefaultOpts); diff --git a/src/node/types/callback.ts b/src/node/types/callback.ts index e221c7320..59591150a 100644 --- a/src/node/types/callback.ts +++ b/src/node/types/callback.ts @@ -92,8 +92,8 @@ export interface FsCallbackApi { ); mkdir(path: misc.PathLike, mode: opts.IMkdirOptions & { recursive: true }, callback: misc.TCallback); mkdir(path: misc.PathLike, mode: misc.TMode | opts.IMkdirOptions, callback: misc.TCallback); - mkdtemp(prefix: string, callback: misc.TCallback): void; - mkdtemp(prefix: string, options: opts.IOptions, callback: misc.TCallback); + mkdtemp(prefix: string, callback: misc.TCallback): void; + mkdtemp(prefix: string, options: opts.IOptions, callback: misc.TCallback); rmdir(path: misc.PathLike, callback: misc.TCallback); rmdir(path: misc.PathLike, options: opts.IRmdirOptions, callback: misc.TCallback); rm(path: misc.PathLike, callback: misc.TCallback): void; diff --git a/src/node/util.ts b/src/node/util.ts index f1a8ba196..d576aa054 100644 --- a/src/node/util.ts +++ b/src/node/util.ts @@ -140,3 +140,9 @@ export function createError(errorCode: string, func = '', path = '', path2 = '', return error; } + +export function genRndStr6(): string { + const str = (Math.random() + 1).toString(36).substring(2, 8); + if (str.length === 6) return str; + else return genRndStr6(); +}; diff --git a/src/volume.ts b/src/volume.ts index c6bb55012..b71388cf7 100644 --- a/src/volume.ts +++ b/src/volume.ts @@ -9,13 +9,14 @@ import setTimeoutUnref, { TSetTimeout } from './setTimeoutUnref'; import { Readable, Writable } from 'stream'; import { constants } from './constants'; import { EventEmitter } from 'events'; -import { TEncodingExtended, TDataOut, assertEncoding, strToEncoding, ENCODING_UTF8 } from './encoding'; +import { TEncodingExtended, TDataOut, strToEncoding, ENCODING_UTF8 } from './encoding'; import * as errors from './internal/errors'; import * as util from 'util'; +import * as opts from './node/types/options'; import { createPromisesApi } from './node/promises'; import { ERRSTR, MODE } from './node/constants'; -import { getMkdirOptions } from './node/options'; -import { validateCallback, modeToNumber, pathToFilename, nullCheck, createError } from './node/util'; +import { getDefaultOpts, getDefaultOptsAndCb, getMkdirOptions, getOptions, optsAndCbGenerator, optsDefaults, optsGenerator } from './node/options'; +import { validateCallback, modeToNumber, pathToFilename, nullCheck, createError, genRndStr6 } from './node/util'; import type { PathLike, symlink } from 'fs'; const resolveCrossPlatform = pathModule.resolve; @@ -61,9 +62,6 @@ const kMinPoolSpace = 128; // ---------------------------------------- Error messages -// TODO: Use `internal/errors.js` in the future. - -const ERRSTR_OPTS = tipeof => `Expected options to be either an object or a string, but got ${tipeof} instead`; // const ERRSTR_FLAG = flag => `Unknown file open flag: ${flag}`; const ENOENT = 'ENOENT'; @@ -135,55 +133,10 @@ export function flagsToNumber(flags: TFlags | undefined): number { // ---------------------------------------- Options -function getOptions(defaults: T, options?: T | string): T { - let opts: T; - if (!options) return defaults; - else { - const tipeof = typeof options; - switch (tipeof) { - case 'string': - opts = Object.assign({}, defaults, { encoding: options as string }); - break; - case 'object': - opts = Object.assign({}, defaults, options); - break; - default: - throw TypeError(ERRSTR_OPTS(tipeof)); - } - } - - if (opts.encoding !== 'buffer') assertEncoding(opts.encoding); - - return opts; -} - -function optsGenerator(defaults: TOpts): (opts) => TOpts { - return options => getOptions(defaults, options); -} - -function optsAndCbGenerator(getOpts): (options, callback?) => [TOpts, TCallback] { - return (options, callback?) => - typeof options === 'function' ? [getOpts(), options] : [getOpts(options), validateCallback(callback)]; -} - // General options with optional `encoding` property that most commands accept. -export interface IOptions { - encoding?: BufferEncoding | TEncodingExtended; -} - -export interface IFileOptions extends IOptions { - mode?: TMode; - flag?: TFlags; -} - -const optsDefaults: IOptions = { - encoding: 'utf8', -}; -const getDefaultOpts = optsGenerator(optsDefaults); -const getDefaultOptsAndCb = optsAndCbGenerator(getDefaultOpts); // Options for `fs.readFile` and `fs.readFileSync`. -export interface IReadFileOptions extends IOptions { +export interface IReadFileOptions extends opts.IOptions { flag?: string; } const readFileOptsDefaults: IReadFileOptions = { @@ -192,7 +145,7 @@ const readFileOptsDefaults: IReadFileOptions = { const getReadFileOptions = optsGenerator(readFileOptsDefaults); // Options for `fs.writeFile` and `fs.writeFileSync` -export interface IWriteFileOptions extends IFileOptions {} +export interface IWriteFileOptions extends opts.IFileOptions {} const writeFileDefaults: IWriteFileOptions = { encoding: 'utf8', mode: MODE.DEFAULT, @@ -201,7 +154,7 @@ const writeFileDefaults: IWriteFileOptions = { const getWriteFileOptions = optsGenerator(writeFileDefaults); // Options for `fs.appendFile` and `fs.appendFileSync` -export interface IAppendFileOptions extends IFileOptions {} +export interface IAppendFileOptions extends opts.IFileOptions {} const appendFileDefaults: IAppendFileOptions = { encoding: 'utf8', mode: MODE.DEFAULT, @@ -246,7 +199,7 @@ export interface IWriteStreamOptions { } // Options for `fs.watch` -export interface IWatchOptions extends IOptions { +export interface IWatchOptions extends opts.IOptions { persistent?: boolean; recursive?: boolean; } @@ -277,11 +230,11 @@ export interface IRmOptions { recursive?: boolean; retryDelay?: number; } -const getRmOpts = optsGenerator(optsDefaults); +const getRmOpts = optsGenerator(optsDefaults); const getRmOptsAndCb = optsAndCbGenerator(getRmOpts); // Options for `fs.readdir` and `fs.readdirSync` -export interface IReaddirOptions extends IOptions { +export interface IReaddirOptions extends opts.IOptions { withFileTypes?: boolean; } const readdirDefaults: IReaddirOptions = { @@ -593,13 +546,6 @@ export class Volume { this.releasedInos.push(node.ino); } - // Generates 6 character long random string, used by `mkdtemp`. - genRndStr() { - const str = (Math.random() + 1).toString(36).substring(2, 8); - if (str.length === 6) return str; - else return this.genRndStr(); - } - // Returns a `Link` (hard link) referenced by path "split" into steps. getLink(steps: string[]): Link | null { return this.root.walk(steps); @@ -1689,15 +1635,15 @@ export class Volume { return strToEncoding(str, encoding); } - readlinkSync(path: PathLike, options?: IOptions): TDataOut { + readlinkSync(path: PathLike, options?: opts.IOptions): TDataOut { const opts = getDefaultOpts(options); const filename = pathToFilename(path); return this.readlinkBase(filename, opts.encoding); } readlink(path: PathLike, callback: TCallback); - readlink(path: PathLike, options: IOptions, callback: TCallback); - readlink(path: PathLike, a: TCallback | IOptions, b?: TCallback) { + readlink(path: PathLike, options: opts.IOptions, callback: TCallback); + readlink(path: PathLike, a: TCallback | opts.IOptions, b?: TCallback) { const [opts, callback] = getDefaultOptsAndCb(a, b); const filename = pathToFilename(path); this.wrapAsync(this.readlinkBase, [filename, opts.encoding], callback); @@ -1886,7 +1832,7 @@ export class Volume { } private mkdtempBase(prefix: string, encoding?: TEncodingExtended, retry: number = 5): TDataOut { - const filename = prefix + this.genRndStr(); + const filename = prefix + genRndStr6(); try { this.mkdirBase(filename, MODE.DIR); return strToEncoding(filename, encoding); @@ -1898,7 +1844,7 @@ export class Volume { } } - mkdtempSync(prefix: string, options?: IOptions): TDataOut { + mkdtempSync(prefix: string, options?: opts.IOptions): TDataOut { const { encoding } = getDefaultOpts(options); if (!prefix || typeof prefix !== 'string') throw new TypeError('filename prefix is required'); @@ -1908,9 +1854,9 @@ export class Volume { return this.mkdtempBase(prefix, encoding); } - mkdtemp(prefix: string, callback: TCallback); - mkdtemp(prefix: string, options: IOptions, callback: TCallback); - mkdtemp(prefix: string, a: TCallback | IOptions, b?: TCallback) { + mkdtemp(prefix: string, callback: TCallback); + mkdtemp(prefix: string, options: opts.IOptions, callback: TCallback); + mkdtemp(prefix: string, a: TCallback | opts.IOptions, b?: TCallback) { const [{ encoding }, callback] = getDefaultOptsAndCb(a, b); if (!prefix || typeof prefix !== 'string') throw new TypeError('filename prefix is required');