Skip to content

Commit

Permalink
feat: 🎸 implement .mkdtemp() method
Browse files Browse the repository at this point in the history
  • Loading branch information
streamich committed Jun 20, 2023
1 parent 1a32314 commit cd54e9b
Show file tree
Hide file tree
Showing 6 changed files with 92 additions and 81 deletions.
21 changes: 14 additions & 7 deletions src/fsa-to-node/FsaNodeFs.ts
Original file line number Diff line number Diff line change
@@ -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');
Expand Down Expand Up @@ -248,11 +250,16 @@ export class FsaNodeFs implements FsCallbackApi {
);
};

mkdtemp(prefix: string, callback: misc.TCallback<void>): void;
mkdtemp(prefix: string, options: opts.IOptions, callback: misc.TCallback<void>);
mkdtemp(prefix: string, a: misc.TCallback<void> | opts.IOptions, b?: misc.TCallback<void>) {
throw new Error('Not implemented');
}
public readonly mkdtemp: FsCallbackApi['mkdtemp'] = (prefix: string, a: misc.TCallback<string> | opts.IOptions, b?: misc.TCallback<string>) => {
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<void>);
rmdir(path: misc.PathLike, options: opts.IRmdirOptions, callback: misc.TCallback<void>);
Expand Down
9 changes: 9 additions & 0 deletions src/fsa-to-node/__tests__/FsaNodeFs.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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);
});
});
43 changes: 43 additions & 0 deletions src/node/options.ts
Original file line number Diff line number Diff line change
@@ -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,
Expand All @@ -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<T extends opts.IOptions>(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<TOpts>(defaults: TOpts): (opts) => TOpts {
return options => getOptions(defaults, options);
}

export function optsAndCbGenerator<TOpts, TResult>(getOpts): (options, callback?) => [TOpts, misc.TCallback<TResult>] {
return (options, callback?) =>
typeof options === 'function' ? [getOpts(), options] : [getOpts(options), validateCallback(callback)];
}

export const optsDefaults: opts.IOptions = {
encoding: 'utf8',
};

export const getDefaultOpts = optsGenerator<opts.IOptions>(optsDefaults);
export const getDefaultOptsAndCb = optsAndCbGenerator<opts.IOptions, any>(getDefaultOpts);
4 changes: 2 additions & 2 deletions src/node/types/callback.ts
Original file line number Diff line number Diff line change
Expand Up @@ -92,8 +92,8 @@ export interface FsCallbackApi {
);
mkdir(path: misc.PathLike, mode: opts.IMkdirOptions & { recursive: true }, callback: misc.TCallback<string>);
mkdir(path: misc.PathLike, mode: misc.TMode | opts.IMkdirOptions, callback: misc.TCallback<string>);
mkdtemp(prefix: string, callback: misc.TCallback<void>): void;
mkdtemp(prefix: string, options: opts.IOptions, callback: misc.TCallback<void>);
mkdtemp(prefix: string, callback: misc.TCallback<string>): void;
mkdtemp(prefix: string, options: opts.IOptions, callback: misc.TCallback<string>);
rmdir(path: misc.PathLike, callback: misc.TCallback<void>);
rmdir(path: misc.PathLike, options: opts.IRmdirOptions, callback: misc.TCallback<void>);
rm(path: misc.PathLike, callback: misc.TCallback<void>): void;
Expand Down
6 changes: 6 additions & 0 deletions src/node/util.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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();
};
90 changes: 18 additions & 72 deletions src/volume.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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';
Expand Down Expand Up @@ -135,55 +133,10 @@ export function flagsToNumber(flags: TFlags | undefined): number {

// ---------------------------------------- Options

function getOptions<T extends IOptions>(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<TOpts>(defaults: TOpts): (opts) => TOpts {
return options => getOptions(defaults, options);
}

function optsAndCbGenerator<TOpts, TResult>(getOpts): (options, callback?) => [TOpts, TCallback<TResult>] {
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<IOptions>(optsDefaults);
const getDefaultOptsAndCb = optsAndCbGenerator<IOptions, any>(getDefaultOpts);

// Options for `fs.readFile` and `fs.readFileSync`.
export interface IReadFileOptions extends IOptions {
export interface IReadFileOptions extends opts.IOptions {
flag?: string;
}
const readFileOptsDefaults: IReadFileOptions = {
Expand All @@ -192,7 +145,7 @@ const readFileOptsDefaults: IReadFileOptions = {
const getReadFileOptions = optsGenerator<IReadFileOptions>(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,
Expand All @@ -201,7 +154,7 @@ const writeFileDefaults: IWriteFileOptions = {
const getWriteFileOptions = optsGenerator<IWriteFileOptions>(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,
Expand Down Expand Up @@ -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;
}
Expand Down Expand Up @@ -277,11 +230,11 @@ export interface IRmOptions {
recursive?: boolean;
retryDelay?: number;
}
const getRmOpts = optsGenerator<IOptions>(optsDefaults);
const getRmOpts = optsGenerator<opts.IOptions>(optsDefaults);
const getRmOptsAndCb = optsAndCbGenerator<IRmOptions, any>(getRmOpts);

// Options for `fs.readdir` and `fs.readdirSync`
export interface IReaddirOptions extends IOptions {
export interface IReaddirOptions extends opts.IOptions {
withFileTypes?: boolean;
}
const readdirDefaults: IReaddirOptions = {
Expand Down Expand Up @@ -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);
Expand Down Expand Up @@ -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<TDataOut>);
readlink(path: PathLike, options: IOptions, callback: TCallback<TDataOut>);
readlink(path: PathLike, a: TCallback<TDataOut> | IOptions, b?: TCallback<TDataOut>) {
readlink(path: PathLike, options: opts.IOptions, callback: TCallback<TDataOut>);
readlink(path: PathLike, a: TCallback<TDataOut> | opts.IOptions, b?: TCallback<TDataOut>) {
const [opts, callback] = getDefaultOptsAndCb(a, b);
const filename = pathToFilename(path);
this.wrapAsync(this.readlinkBase, [filename, opts.encoding], callback);
Expand Down Expand Up @@ -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);
Expand All @@ -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');
Expand All @@ -1908,9 +1854,9 @@ export class Volume {
return this.mkdtempBase(prefix, encoding);
}

mkdtemp(prefix: string, callback: TCallback<void>);
mkdtemp(prefix: string, options: IOptions, callback: TCallback<void>);
mkdtemp(prefix: string, a: TCallback<void> | IOptions, b?: TCallback<void>) {
mkdtemp(prefix: string, callback: TCallback<string>);
mkdtemp(prefix: string, options: opts.IOptions, callback: TCallback<string>);
mkdtemp(prefix: string, a: TCallback<string> | opts.IOptions, b?: TCallback<string>) {
const [{ encoding }, callback] = getDefaultOptsAndCb(a, b);

if (!prefix || typeof prefix !== 'string') throw new TypeError('filename prefix is required');
Expand Down

0 comments on commit cd54e9b

Please sign in to comment.