Source

fs-dir.class.ts

import { Dirent, readdir, rmdir, mkdir, access, constants, rename } from 'fs';
import { basename, join } from 'path';
import { FSFile } from './fs-file.class';
import { FSAsyncIterable } from './fs-async-iterable.class';
import { FSPath, FSPathType } from '.';

const readDirPromise = (path: string) =>
  new Promise<Dirent[]>((resolve, reject) => {
    readdir(path, { withFileTypes: true }, (err, dirents) => {
      if (err) return reject(err);
      resolve(dirents);
    });
  });

async function* direntsGen(path: string) {
  const dirents = await readDirPromise(path);
  for (const dirent of dirents) {
    yield dirent;
  }
}
/**
 * Contains all methods for work with directories
 *
 */
export class FSDir {
  /**
   *
   * @param {string} path - valid filesystem path string
   */
  constructor(public readonly path: string) {}

  /**
   * Directory name
   */
  public readonly name: string = basename(this.path);

  /**
   * Return FSPath function for current path. Can be used to countinue joining
   * path segments to subdirs or files.
   */
  public readonly fspath: FSPathType = FSPath(this.path);

  /**
   * Return async iterator which iterates all dirents in dir.
   * Chaining map, filter and forEach
   * operators available. toArray operator can be used for resulting chain to array.
   * @returns {FSAsyncIterable<Dirent>}
   */
  public dirents() {
    return new FSAsyncIterable(direntsGen(this.path));
  }

  /**
   * Return async iterator which iterates all files in dir. Chaining map, filter and forEach
   * operators available. toArray operator can be used for resulting chain to array.
   * @returns {FSAsyncIterable}
   */
  public files() {
    return this.dirents()
      .filter(async dirent => dirent.isFile())
      .map(async dirent => new FSFile(join(this.path, dirent.name)));
  }

  private async *recursiveDirsGen() {
    for await (const dir of this.subdirs()) {
      yield dir;
      yield* dir.subdirs(true);
    }
  }
  /**
   * Return async iterator wich iterates each subdirs. Chaining map, filter and forEach
   * operators available. toArray operator can be used for resulting chain to array.
   * @param recursive - if true returns each subdir of any deep
   * @returns {FSAsyncIterable}
   * @example <caption>Extratct all module names and versions from node_modules</caption>
   * const { cwd } = require('fstb');
   * cwd
   *  .node_modules()
   *  .asDir()
   *  .subdirs(true)
   *  .map(async dir => dir.fspath['package.json']().asFile())
   *  .filter(async package_json => await package_json.isReadable())
   *  .map(async package_json => await package_json.read.json())
   *  .forEach(async content => console.log(`${content.name}@${content.version}`));
   */
  public subdirs(recursive: boolean = false): FSAsyncIterable<FSDir> {
    if (recursive) {
      return new FSAsyncIterable(this.recursiveDirsGen());
    } else {
      return this.dirents()
        .filter(async dirent => dirent.isDirectory())
        .map(async dirent => new FSDir(join(this.path, dirent.name)));
    }
  }
  /**
   * delete a directory
   */
  public async rmdir() {
    return new Promise<void>((resolve, reject) => {
      rmdir(this.path, err => {
        if (err) return reject(err);
        resolve();
      });
    });
  }

  /**
   * Asynchronously creates a directory.
   * @param recursive - if recursive is true, the first directory path created
   */
  public async mkdir(recursive: boolean = false) {
    return new Promise<FSDir>((resolve, reject) => {
      mkdir(this.path, { recursive }, err => {
        if (err) return reject(err);
        resolve(this);
      });
    });
  }

  /**
   * Checks is dir exits
   * @returns {Promise<boolean>}
   */
  public async isExists(): Promise<boolean> {
    return new Promise<boolean>(resolve => {
      access(this.path, constants.F_OK, err => {
        if (err) return resolve(false);
        resolve(true);
      });
    });
  }

  /**
   * Compute total size of this directory.
   * @example <caption>Total size of node_modules</caption>
   *   console.log(
   * 'Total size of node_modules is ' +
   *   Math.floor(
   *     (await cwd
   *       .node_modules()
   *       .asDir()
   *       .totalSize()) /
   *       1024 /
   *       1024
   *   ) +
   *   ' MBytes'
   * );
   */
  public async totalSize(): Promise<number> {
    let size = 0;
    await this.files()
      .map(async file => (await file.stat()).size)
      .forEach(async fileSize => {
        size += fileSize;
      });

    await this.subdirs()
      .map(async dir => await dir.totalSize())
      .forEach(async totalSize => {
        size += totalSize;
      });

    return size;
  }

  /**
   * Removes directory with all content
   */
  public async rimraf() {
    if (await this.isExists()) {
      await this.files().forEach(async file => {
        await file.unlink();
      });
      await this.subdirs().forEach(async dir => {
        await dir.rimraf();
      });
      await this.rmdir();
    }
  }
  /**
   * Recursively copy current directory to the target dir
   * @param targetDir - Directory in which will be copy current
   */

  public async copyTo(targetDir: FSDir) {
    const dir = await targetDir.fspath[this.name]()
      .asDir()
      .mkdir();

    await this.files().forEach(async file => {
      await file.copyTo(dir, { COPYFILE_EXCL: true });
    });

    await this.subdirs().forEach(async srcSubdir => {
      await srcSubdir.copyTo(dir);
    });
    return dir;
  }

  /**
   * Moves diretory to the new path
   * @param targetDir - target directory
   * @returns moved direrectory
   */
  public async moveTo(targetDir: FSDir):Promise<FSDir> {
    const dir = targetDir.fspath[this.name]().asDir()
    return new Promise((resolve, reject)=>{
      rename(this.path, dir.path, (err=>{
        if(err) return reject(err)
        resolve(dir)
      }))
    })
  }
}