UNPKG

find-remove

Version:

recursively finds files and/or directories by filter options from a start directory onwards and deletes these according to plenty of options you can configure. useful if you want to clean up stuff within a directory in your node.js app.

304 lines (248 loc) 7.7 kB
import fs from "fs"; import path from "path"; import { rimrafSync } from "rimraf"; let now: number | undefined; let testRun: boolean | undefined; interface Options { test?: boolean; limit?: number; totalRemoved?: number; maxLevel?: number; dir?: string | string[]; regex?: boolean; prefix?: string; ignore?: string | string[]; extensions?: string | string[]; files?: string | string[]; age?: { seconds?: number }; } function isOlder(path: string, ageSeconds: number) { if (!now) return false; const stats = fs.statSync(path); const mtime = stats.mtime.getTime(); const expirationTime = mtime + ageSeconds * 1000; return now > expirationTime; } function getLimit(options: Options = {}) { if (options.limit !== undefined) { return options.limit; } return -1; } function getTotalRemoved(options: Options = {}) { if (options.totalRemoved !== undefined) { return options.totalRemoved; } return -2; } function isOverTheLimit(options: Options = {}) { return getTotalRemoved(options) >= getLimit(options); } function getMaxLevel(options: Options = {}) { if (options.maxLevel !== undefined) { return options.maxLevel; } return -1; } function getAgeSeconds(options: Options = {}) { if (!options.age) { return null; } return options.age.seconds ?? null; } function doDeleteDirectory( currentDir: string, currentLevel: number, options: Options = {}, ) { let doDelete = false; const dir = options.dir; if (dir) { const ageSeconds = getAgeSeconds(options); const basename = path.basename(currentDir); if (Array.isArray(dir)) { doDelete = dir.indexOf("*") !== -1 || dir.indexOf(basename) !== -1; } else if ( (options.regex && basename.match(new RegExp(dir))) || basename === dir || dir === "*" ) { doDelete = true; } if (doDelete && options.limit !== undefined) { doDelete = !isOverTheLimit(options); } if (doDelete && options.maxLevel !== undefined && currentLevel > 0) { doDelete = currentLevel <= getMaxLevel(options); } if (ageSeconds && doDelete) { doDelete = isOlder(currentDir, ageSeconds); } } return doDelete; } function doDeleteFile(currentFile: string, options: Options = {}) { // by default it deletes nothing let doDelete = false; const extensions = options.extensions ? options.extensions : null; const files = options.files ? options.files : null; const prefix = options.prefix ? options.prefix : null; const ignore = options.ignore ?? null; // return the last portion of a path, the filename aka basename const basename = path.basename(currentFile); if (files) { if (Array.isArray(files)) { doDelete = files.indexOf("*.*") !== -1 || files.indexOf(basename) !== -1; } else { if ((options.regex && basename.match(new RegExp(files))) || files === "*.*") { doDelete = true; } else { doDelete = basename === files; } } } if (!doDelete && extensions) { const currentExt = path.extname(currentFile); if (Array.isArray(extensions)) { doDelete = extensions.indexOf(currentExt) !== -1; } else { doDelete = currentExt === extensions; } } if (!doDelete && prefix) { doDelete = basename.indexOf(prefix) === 0; } if (doDelete && options.limit !== undefined) { doDelete = !isOverTheLimit(options); } if (doDelete && ignore) { if (Array.isArray(ignore)) { doDelete = !(ignore.indexOf(basename) !== -1); } else { doDelete = !(basename === ignore); } } if (doDelete) { const ageSeconds = getAgeSeconds(options); if (ageSeconds) { doDelete = isOlder(currentFile, ageSeconds); } } return doDelete; } function hasStats(dir: string) { try { fs.lstatSync(dir); return true; } catch (err) { return false; } } /** * FindRemoveSync(currentDir, options) takes any start directory and searches files from there for removal. * the selection of files for removal depends on the given options. when no options are given, or only the maxLevel * parameter is given, then everything is removed as if there were no filters. * * Beware: everything happens synchronously. * * * @param {string} currentDir any directory to operate within. it will seek files and/or directories recursively from there. * beware that it deletes the given currentDir when no options or only the maxLevel parameter are given. * @param options json object with optional properties like extensions, files, ignore, maxLevel and age.seconds. * @return {Object} json object of files and/or directories that were found and successfully removed. * @api public */ const findRemoveSync = function ( currentDir: string, options: Options = {}, currentLevel?: number, ) { let removed: Record<string, boolean> = {}; if (isOverTheLimit(options)) { // Return early in that case return removed; } let deleteDirectory = false; const dirExists = fs.existsSync(currentDir); const dirHasStats = hasStats(currentDir); if (dirExists && !dirHasStats) { // Must be a broken symlink. Flag it for deletion. See: // https://github.com/binarykitchen/find-remove/issues/42 deleteDirectory = true; } else if (dirExists) { const maxLevel = getMaxLevel(options); if (options.limit !== undefined) { options.totalRemoved = options.totalRemoved !== undefined ? getTotalRemoved(options) : 0; } if (currentLevel === undefined) { currentLevel = 0; } else { currentLevel++; } if (currentLevel < 1) { now = new Date().getTime(); testRun = options.test; } else { // check directories before deleting files inside. // this to maintain the original creation time, // because linux modifies creation date of folders when files within have been deleted. deleteDirectory = doDeleteDirectory(currentDir, currentLevel, options); } if (maxLevel === -1 || currentLevel < maxLevel) { const filesInDir = fs.readdirSync(currentDir); filesInDir.forEach(function (file) { const currentFile = path.join(currentDir, file); let skip = false; let stat; try { // Add extra checks for invalid symlinks using lstatSync stat = fs.lstatSync(currentFile); } catch (exc) { // ignore skip = true; } if (skip) { // ignore, do nothing } else if (stat?.isDirectory()) { // the recursive call const result = findRemoveSync(currentFile, options, currentLevel); // merge results removed = { ...removed, ...result }; if (options.totalRemoved !== undefined) { options.totalRemoved += Object.keys(result).length; } } else if (doDeleteFile(currentFile, options)) { let unlinked; if (!testRun) { try { fs.unlinkSync(currentFile); unlinked = true; } catch (exc) { // ignore } } else { unlinked = true; } if (unlinked) { removed[currentFile] = true; if (options.totalRemoved !== undefined) { options.totalRemoved++; } } } }); } } if (deleteDirectory) { if (!testRun) { rimrafSync(currentDir); } if (options.totalRemoved === undefined) { // for limit of files - we do not want to count the directories removed[currentDir] = true; } } return removed; }; export default findRemoveSync;