NaturalCycles/nodejs-lib

View on GitHub
src/fs/del.ts

Summary

Maintainability
A
2 hrs
Test Coverage
F
10%
import { pFilter, pMap, _since } from '@naturalcycles/js-lib'
import { dimGrey, yellow } from '../colors/colors'
import { fs2, globby } from '../index'

export interface DelOptions {
  /**
   * Globby patterns.
   */
  patterns: string[]

  /**
   * @default 0 (infinite)
   */
  concurrency?: number

  verbose?: boolean

  silent?: boolean

  debug?: boolean

  dry?: boolean
}

export type DelSingleOption = string

const DEF_OPT: DelOptions = {
  patterns: [],
  concurrency: Number.POSITIVE_INFINITY,
}

/**
 * Delete files that match input patterns.
 *
 * @experimental
 */
export async function del(_opt: DelOptions | DelSingleOption): Promise<void> {
  const started = Date.now()

  // Convert DelSingleOption to DelOptions
  if (typeof _opt === 'string') {
    _opt = {
      patterns: [_opt],
    }
  }

  const opt = {
    ...DEF_OPT,
    ..._opt,
    concurrency: _opt.concurrency || DEF_OPT.concurrency,
  }
  const { patterns, concurrency, verbose, silent, debug, dry } = opt

  if (debug) {
    console.log(opt)
  }

  // 1. glob only files, expand dirs, delete

  const filenames = await globby(patterns, {
    dot: true,
    expandDirectories: true,
    onlyFiles: true,
  })

  if (verbose || debug || dry) {
    console.log(`Will delete ${yellow(filenames.length)} files:`, filenames)
  }

  if (dry) return

  await pMap(filenames, filepath => fs2.removePath(filepath), { concurrency })

  // 2. glob only dirs, expand, delete only empty!
  let dirnames = await globby(patterns, {
    dot: true,
    expandDirectories: true,
    onlyDirectories: true,
  })

  // Add original patterns (if any of them are dirs)
  dirnames = dirnames.concat(
    await pFilter(patterns, async pattern => {
      return (await fs2.pathExistsAsync(pattern)) && (await fs2.lstatAsync(pattern)).isDirectory()
    }),
  )

  const dirnamesSorted = dirnames.sort().reverse()

  // console.log({ dirnamesSorted })

  const deletedDirs: string[] = []
  for await (const dirpath of dirnamesSorted) {
    if (await isEmptyDir(dirpath)) {
      // console.log(`empty dir: ${dirpath}`)
      await fs2.removePathAsync(dirpath)
      deletedDirs.push(dirpath)
    }
  }

  if (verbose || debug) console.log({ deletedDirs })

  if (!silent) {
    console.log(
      `del deleted ${yellow(filenames.length)} files and ${yellow(
        deletedDirs.length,
      )} dirs ${dimGrey(_since(started))}`,
    )
  }
}

export function delSync(_opt: DelOptions | DelSingleOption): void {
  const started = Date.now()

  // Convert DelSingleOption to DelOptions
  if (typeof _opt === 'string') {
    _opt = {
      patterns: [_opt],
    }
  }

  const opt = {
    ...DEF_OPT,
    ..._opt,
  }
  const { patterns, verbose, silent, debug, dry } = opt

  if (debug) {
    console.log(opt)
  }

  // 1. glob only files, expand dirs, delete

  const filenames = globby.sync(patterns, {
    dot: true,
    expandDirectories: true,
    onlyFiles: true,
  })

  if (verbose || debug || dry) {
    console.log(`Will delete ${yellow(filenames.length)} files:`, filenames)
  }

  if (dry) return

  filenames.forEach(filepath => fs2.removePath(filepath))

  // 2. glob only dirs, expand, delete only empty!
  let dirnames = globby.sync(patterns, {
    dot: true,
    expandDirectories: true,
    onlyDirectories: true,
  })

  // Add original patterns (if any of them are dirs)
  dirnames = dirnames.concat(patterns.filter(p => fs2.pathExists(p) && fs2.lstat(p).isDirectory()))

  const dirnamesSorted = dirnames.sort().reverse()

  // console.log({ dirnamesSorted })

  const deletedDirs: string[] = []
  for (const dirpath of dirnamesSorted) {
    if (isEmptyDirSync(dirpath)) {
      // console.log(`empty dir: ${dirpath}`)
      fs2.removePath(dirpath)
      deletedDirs.push(dirpath)
    }
  }

  if (verbose || debug) console.log({ deletedDirs })

  if (!silent) {
    console.log(
      `del deleted ${yellow(filenames.length)} files and ${yellow(
        deletedDirs.length,
      )} dirs ${dimGrey(_since(started))}`,
    )
  }
}

// Improved algorithm:
// 1. glob only files, expand dirs, delete
// 2. glob only dirs, expand, delete only empty!
// 3. test each original pattern, if it exists and is directory and is empty - delete

async function isEmptyDir(dir: string): Promise<boolean> {
  return (await fs2.readdirAsync(dir)).length === 0
}

function isEmptyDirSync(dir: string): boolean {
  return fs2.readdir(dir).length === 0
}