UNPKG

dir_tree

Version:

Creates a Searchable, Sortable & Printable Tree For Stated Directory Path.

715 lines (714 loc) 22.2 kB
var fs = require('fs'); var p = require('path'); var EventEmitter = require('events').EventEmitter; var util = require('util'); function DirTreeNode(dtn) { this.hide = false; this.name; this.path; this.path_from_root; this.created_on; this.modified_on; this.is_a_dir; this.size; this.size_of_files; this.dirs; this.files; this.no_of_total_dirs; this.no_of_total_files; this.parent; this.unread_paths; this.no_of_total_unread_paths; if (dtn instanceof DirTreeNode) { this.name = dtn.name; this.path = dtn.path; this.path_from_root = dtn.path_from_root; this.created_on = dtn.created_on; this.modified_on = dtn.modified_on; if (this.is_a_dir = dtn.is_a_dir) { this.size = 0; this.size_of_files = 0; this.dirs = []; this.files = []; this.no_of_total_dirs = 0; this.no_of_total_files = 0; this.no_of_total_unread_paths = 0; } else this.size = dtn.size; } } DirTreeNode.alphabetic_comparator = function(e1, e2) { var n1 = e1.path_from_root.toLowerCase(); var n2 = e2.path_from_root.toLowerCase(); return n1 > n2 ? 1 : n1 < n2 ? -1 : 0; }; DirTreeNode.prototype.dirs_creation_order = function() { if (!this.is_a_dir) throw new Error('Not a Directory!'); var extra_length_rel = 1 + this.path_from_root.length; const func = function(dir) { return Array.prototype.concat.apply([dir.path_from_root.substr(extra_length_rel)], dir.dirs.map(func)); } var arr = func(this); arr.shift(); return arr; }; DirTreeNode.prototype.total_files = function(arr) { if (!this.is_a_dir) throw new Error('Not a Directory!'); return this.total_files_(arr === undefined ? [] : arr, this.parent ? 1 + this.parent.path_from_root.length : 0).sort(DirTreeNode.alphabetic_comparator); }; DirTreeNode.prototype.total_files_ = function(arr, extra_length_from) { var overview = function(file) { return { path_from_root: file.path_from_root.substr(extra_length_from), path: file.path, name: file.name, size: file.size, created_on: file.created_on, modified_on: file.modified_on }; }; Array.prototype.push.apply(arr, this.files.map(overview)); this.dirs.forEach(function(dir) { dir.total_files_(arr, extra_length_from); }); return arr; }; DirTreeNode.prototype.total_dirs = function(arr) { if (!this.is_a_dir) throw new Error('Not a Directory!'); return this.total_dirs_(arr === undefined ? [] : arr, this.parent ? 1 + this.parent.path_from_root.length : 0).sort(DirTreeNode.alphabetic_comparator); }; DirTreeNode.prototype.total_dirs_ = function(arr, extra_length_from) { var overview = function(dir) { return { path_from_root: dir.path_from_root.substr(extra_length_from), path: dir.path, name: dir.name, size: dir.size, no_of_dirs: dir.dirs.length, no_of_files: dir.files.length, no_of_total_dirs: dir.no_of_total_dirs, no_of_total_files: dir.no_of_total_files, created_on: dir.created_on, modified_on: dir.modified_on }; }; Array.prototype.push.apply(arr, this.dirs.map(overview)); this.dirs.forEach(function(dir) { dir.total_dirs_(arr, extra_length_from); }); return arr; }; DirTreeNode.prototype.serial = function() { if (!this.is_a_dir) throw new Error('Not a Directory!'); return this.serial_(this.parent ? 1 + this.parent.path_from_root.length : 0); }; DirTreeNode.prototype.serial_ = function(extra_length) { return { path_from_root: this.path_from_root.substr(extra_length), path: this.path, name: this.name, size: this.size, size_of_files: this.size_of_files, no_of_total_files: this.no_of_total_files, no_of_total_dirs: this.no_of_total_dirs, created_on: this.created_on, modified_on: this.modified_on, unread_paths: this.unread_paths, no_of_total_unread_paths: this.no_of_total_unread_paths, files: this.files.map(function(file) { return { path_from_root: file.path_from_root.substr(extra_length), path: file.path, name: file.name, size: file.size, created_on: file.created_on, modified_on: file.modified_on }; }), dirs: this.dirs.map(function(dir) { return dir.serial_(extra_length); }) }; }; DirTreeNode.prototype.tree = function(indent) { if (!this.is_a_dir) throw new Error('Not a Directory!'); return indent == undefined ? this.tree_('\n') : indent + this.tree_('\n' + indent); }; DirTreeNode.prototype.tree_ = function(prefix) { var f = []; var d = []; for (var i = 0, pairs = [[f, d], [this.files, this.dirs]]; i < 2; ++i) pairs[1][i].forEach(function(e) { if (!e.hide) pairs[0][i].push(e); }); var nf = f.length; var nd = d.length; if (nf + nd == 0) return this.name; if (nd == 0) { var filenames = f.map(function(f) { return f.name }); var lastfilename = filenames.pop(); if (nf > 1) return this.name + prefix + '├──' + filenames.join(prefix + '├──') + prefix + '└──' + lastfilename; return this.name + prefix + '└──' + lastfilename; } if (nd == 1) { return this.name + f.map(function(f) { return prefix + '├──' + f.name }).join('') + prefix + '└⌂─' + d[0].tree_(prefix + ' '); } return this.name + f.map(function(f) { return prefix + '├──' + f.name }).join('') + d.slice(0, -1).map(function(d) { return prefix + '├⌂─' + d.tree_(prefix + '│ ') }).join('') + prefix + '└⌂─' + d.slice(-1)[0].tree_(prefix + ' '); }; DirTreeNode.prototype.sort = function(dir_comparator_func, file_comparator_func) { if (!this.is_a_dir) throw new Error('Not a Directory!'); if (dir_comparator_func == undefined || dir_comparator_func == null) dir_comparator_func = DirTreeNode.alphabetic_comparator; if (file_comparator_func == undefined || file_comparator_func == null) file_comparator_func = DirTreeNode.alphabetic_comparator; this.sort_(dir_comparator_func, file_comparator_func); }; DirTreeNode.prototype.sort_ = function(dir_comparator_func, file_comparator_func) { if (file_comparator_func) this.files.sort(file_comparator_func); if (dir_comparator_func) this.dirs.sort(dir_comparator_func); this.dirs.forEach(function(dir) { dir.sort_(dir_comparator_func, file_comparator_func); }); }; DirTreeNode.prototype.filter_files = function(filter) { if (!this.is_a_dir) throw new Error('Not a Directory!'); var idr; var edr; var ifr; var efr; var max_depth = Number.POSITIVE_INFINITY; if (filter instanceof Object) { idr = filter.idr; edr = filter.edr; ifr = filter.ifr; efr = filter.efr; max_depth = parseInt(filter.max_depth); } if (isNaN(max_depth) || max_depth < 1) max_depth = Number.POSITIVE_INFINITY; switch (idr) { case undefined: idr = null; case null: break; default: if (!(idr instanceof RegExp)) throw new Error('Invalid argument: idr should be either undefined or null or a RegExp Object!'); } switch (edr) { case undefined: edr = null; case null: break; default: if (!(edr instanceof RegExp)) throw new Error('Invalid argument: edr should be either undefined or null or a RegExp Object!'); } switch (ifr) { case undefined: ifr = null; case null: break; default: if (!(ifr instanceof RegExp)) throw new Error('Invalid argument: ifr should be either undefined or null or a RegExp Object!'); } switch (efr) { case undefined: efr = null; case null: break; default: if (!(efr instanceof RegExp)) throw new Error('Invalid argument: efr should be either undefined or null or a RegExp Object!'); } var dir_obj = new DirTreeNode(this); var extra_length_rel = 1 + this.path_from_root.length; var extra_length_from = this.parent ? 1 + this.parent.path_from_root.length : 0; this.filter_files_choose_files_(ifr, efr, extra_length_rel, extra_length_from, dir_obj); if (max_depth > 1) this.dirs.forEach(function(dir) { dir.filter_files_(idr, edr, ifr, efr, dir_obj, extra_length_rel, extra_length_from, 1, max_depth); }); if (dir_obj.no_of_total_files > 0) dir_obj.path_from_root = this.name; return dir_obj.no_of_total_files == 0 ? null : dir_obj; }; DirTreeNode.prototype.filter_files_choose_files_ = function(ifr, efr, extra_length_rel, extra_length_from, dir_obj) { this.files.forEach(function(file) { var file_path_relative_to_root = file.path_from_root.substr(extra_length_rel); if (efr != null) { efr.lastIndex = 0; if (efr.test(file_path_relative_to_root)) return; } if (ifr != null) { ifr.lastIndex = 0; if (!ifr.test(file_path_relative_to_root)) return; } var file_obj = new DirTreeNode(file); file_obj.path_from_root = file.path_from_root.substr(extra_length_from); dir_obj.files.push(file_obj); file_obj.parent = dir_obj; dir_obj.size_of_files += file.size; }); dir_obj.no_of_total_files += dir_obj.files.length; dir_obj.size = dir_obj.size_of_files; }; DirTreeNode.prototype.filter_files_ = function(idr, edr, ifr, efr, parent_obj, extra_length_rel, extra_length_from, depth, max_depth) { var this_path_relative_to_root = this.path_from_root.substr(extra_length_rel); if (edr != null) { edr.lastIndex = 0; if (edr.test(this_path_relative_to_root)) return; } var dir_obj; switch (idr) { default: { idr.lastIndex = 0; if (!idr.test(this_path_relative_to_root)) return; } case null: { dir_obj = new DirTreeNode(this); dir_obj.unread_paths = this.unread_paths.slice(0); dir_obj.no_of_total_unread_paths = dir_obj.unread_paths.length; this.filter_files_choose_files_(ifr, efr, extra_length_rel, extra_length_from, dir_obj); } } if (++depth < max_depth) this.dirs.forEach(function(dir) { dir.filter_files_(idr, edr, ifr, efr, dir_obj, extra_length_rel, extra_length_from, depth, max_depth); }); if (dir_obj.no_of_total_files > 0) { dir_obj.path_from_root = this.path_from_root.substr(extra_length_from); parent_obj.dirs.push(dir_obj); dir_obj.parent = parent_obj; parent_obj.no_of_total_dirs += 1 + dir_obj.no_of_total_dirs; parent_obj.no_of_total_files += dir_obj.no_of_total_files; parent_obj.no_of_total_unread_paths += dir_obj.no_of_total_unread_paths; parent_obj.size += dir_obj.size; } }; DirTreeNode.prototype.fileless_tree = function(indent) { if (!this.is_a_dir) throw new Error('Not a Directory!'); return indent == undefined ? this.fileless_tree_('\n') : indent + this.fileless_tree_('\n' + indent); }; DirTreeNode.prototype.fileless_tree_ = function(prefix) { switch (this.dirs.length) { case 0: return this.name; case 1: return this.name + prefix + '└──' + this.dirs[0].fileless_tree_(prefix + ' '); default: return this.name + this.dirs.slice(0, -1).map(function(d) { return prefix + '├──' + d.fileless_tree_(prefix + '│ ') }).join('') + prefix + '└──' + this.dirs.slice(-1)[0].fileless_tree_(prefix + ' '); } }; DirTreeNode.prototype.filter_dirs = function(idr, edr) { if (!this.is_a_dir) throw new Error('Not a Directory!'); switch (idr) { case undefined: idr = null; case null: break; default: if (!(idr instanceof RegExp)) throw new Error('Invalid argument: idr should be either undefined or null or a RegExp Object!'); } switch (edr) { case undefined: edr = null; case null: break; default: if (!(edr instanceof RegExp)) throw new Error('Invalid argument: edr should be either undefined or null or a RegExp Object!'); } var ret_arr = []; var extra_length = 1 + this.path_from_root.length; this.dirs.forEach(function(dir) { dir.filter_dirs_(idr, edr, ret_arr, extra_length); }); return ret_arr; }; DirTreeNode.prototype.filter_dirs_ = function(idr, edr, arr, extra_length) { var this_path_relative_to_root = this.path_from_root.substr(extra_length); if (edr != null) { edr.lastIndex = 0; if (edr.test(this_path_relative_to_root)) return; } if (idr != null) { idr.lastIndex = 0; if (idr.test(this_path_relative_to_root)) arr.push(this); } else arr.push(this); this.dirs.forEach(function(dir) { dir.filter_dirs_(idr, edr, arr, extra_length); }); }; DirTreeNode.prototype.total_unread_paths = function(arr) { if (!this.is_a_dir) throw new Error('Not a Directory!'); var arr = []; this.total_unread_paths_(arr); return arr; }; DirTreeNode.prototype.total_unread_paths_ = function(arr) { Array.prototype.push.apply(arr, this.unread_paths); this.dirs.forEach(function(dir) { dir.total_unread_paths_(arr); }); }; DirTreeNode.prototype.disjoin = function() { if (!this.parent) return; if (this.is_a_dir) { var arr = this.parent.dirs; arr.splice(arr.indexOf(this), 1); var parent = this; while (parent = parent.parent) { parent.size -= this.size; --parent.no_of_total_dirs; parent.no_of_total_unread_paths -= this.no_of_total_unread_paths; parent.no_of_total_files -= this.no_of_total_files; } } else { var arr = this.parent.files; arr.splice(arr.indexOf(this), 1); this.parent.size_of_files -= this.size; var parent = this; while (parent = parent.parent) { parent.size -= this.size; --parent.no_of_total_files; } } }; DirTreeNode.prototype.remove_fileless_dirs = function() { if (!this.is_a_dir) return; this.remove_fileless_dirs_(); }; DirTreeNode.prototype.remove_fileless_dirs_ = function() { var dirs = this.dirs; var l = dirs.length; var k = 0; var dels = new Array(l + 1); for (var i = 0; i < l; ++i) { var child_dir = dirs[i]; if (child_dir.no_of_total_files == 0) dels[k++] = i; else child_dir.remove_fileless_dirs_(); } dels[k] = dirs.length; for (var j = 0; j < k; ++j) { var i = dels[j]; var d = dirs[i - j]; var parent = this; while (parent) { parent.no_of_total_unread_paths -= d.no_of_total_unread_paths; parent.no_of_total_dirs -= 1 + d.no_of_total_dirs; parent = parent.parent; } for (; i < dels[j + 1]; ++i) { dirs[i - j] = dirs[i + 1]; } } dirs.length -= k; }; function DirTree(path, filter) { var idr; var edr; var ifr; var efr; var max_depth = Number.POSITIVE_INFINITY; if (filter instanceof Object) { idr = filter.idr; edr = filter.edr; ifr = filter.ifr; efr = filter.efr; max_depth = parseInt(filter.max_depth); } if (isNaN(max_depth) || max_depth < 1) max_depth = Number.POSITIVE_INFINITY; EventEmitter.call(this); var barrier = 0; var counter = 0; var start_returning = false; var root_dtn = new DirTreeNode(); var self = this; var patterns_specified = false; switch (idr) { case undefined: idr = null; case null: break; default: if (!(idr instanceof RegExp)) { emit_('error', new Error('Invalid argument: idr should be either undefined or null or a RegExp Object!')); return } patterns_specified = true; } switch (edr) { case undefined: edr = null; case null: break; default: if (!(edr instanceof RegExp)) { emit_('error', new Error('Invalid argument: edr should be either undefined or null or a RegExp Object!')); return } patterns_specified = true; } switch (ifr) { case undefined: ifr = null; case null: break; default: if (!(ifr instanceof RegExp)) { emit_('error', new Error('Invalid argument: ifr should be either undefined or null or a RegExp Object!')); return } patterns_specified = true; } switch (efr) { case undefined: efr = null; case null: break; default: if (!(efr instanceof RegExp)) { emit_('error', new Error('Invalid argument: efr should be either undefined or null or a RegExp Object!')); return } patterns_specified = true; } function emit_(event_name) { var args = []; for (var i = 1, l = arguments.length; i < l; ++i) args.push(arguments[i]); if (self.listeners(event_name).length == 0) { var cb_setter = function(event_name_) { if (event_name_ == event_name) { self.removeListener(event_name, cb_setter); arguments[1].apply(null, args); } }; self.on('newListener', cb_setter); } else self.emit.apply(self, [event_name].concat(args)); } function done() { if (++counter == barrier) { delete root_dtn.parent; if (!root_dtn.is_a_dir) emit_('error', new Error('Not a Directory!')); else { if (patterns_specified) { root_dtn.remove_fileless_dirs(); if (root_dtn.no_of_total_files == 0) { emit_('data', null); return; } } root_dtn.sort(); emit_('data', root_dtn); } } } var sep = p.sep; var resolved_root_path = p.resolve(path + sep).replace(/[\\/]$/, ''); var root_basename = p.basename(sep + resolved_root_path); function process_root_path(dtn, parent) { if (start_returning) return; ++barrier; fs.stat(resolved_root_path + sep, function(err, stats) { function handle_unread_path(err) { parent.unread_paths.push(resolved_root_path + sep); while (parent) { ++parent.no_of_total_unread_paths; parent = parent.parent; } done(); } if (err) { handle_unread_path(); return; } if (dtn.is_a_dir = stats.isDirectory()) { dtn.path_from_root = root_basename; dtn.parent = parent; dtn.path = resolved_root_path; dtn.name = root_basename; dtn.created_on = stats.birthtime; dtn.modified_on = stats.mtime; dtn.size = 0; dtn.size_of_files = 0; dtn.files = []; dtn.dirs = []; dtn.unread_paths = []; dtn.no_of_total_files = 0; dtn.no_of_total_dirs = 0; dtn.no_of_total_unread_paths = 0; parent.dirs.push(dtn); while (parent) { ++parent.no_of_total_dirs; parent = parent.parent; } fs.readdir(resolved_root_path + sep, function(err, basenames) { if (err) { handle_unread_path(); return; } basenames.forEach(function(basename) { process_path(new DirTreeNode(), dtn, resolved_root_path + sep + basename, root_basename + sep + basename, basename, basename, 1) }); done(); }); } else done(); }); } function process_path(dtn, parent, resolved_path, path_from_root, path_rel_to_root, basename, depth) { if (start_returning) return; ++barrier; fs.stat(resolved_path, function(err, stats) { function handle_unread_path(err) { parent.unread_paths.push(resolved_path); while (parent) { ++parent.no_of_total_unread_paths; parent = parent.parent; } done(); } if (err) { handle_unread_path(); return; } if (dtn.is_a_dir = stats.isDirectory()) { if (edr != null) { edr.lastIndex = 0; if (edr.test(path_rel_to_root)) { done(); return; } } if (idr != null) { idr.lastIndex = 0; if (!idr.test(path_rel_to_root)) { done(); return; } } dtn.path_from_root = path_from_root; dtn.parent = parent; dtn.path = resolved_path; dtn.name = basename; dtn.created_on = stats.birthtime; dtn.modified_on = stats.mtime; dtn.size = 0; dtn.size_of_files = 0; dtn.files = []; dtn.dirs = []; dtn.unread_paths = []; dtn.no_of_total_files = 0; dtn.no_of_total_dirs = 0; dtn.no_of_total_unread_paths = 0; parent.dirs.push(dtn); while (parent) { ++parent.no_of_total_dirs; parent = parent.parent; } fs.readdir(resolved_path, function(err, basenames) { if (err) { handle_unread_path(); return; } if (depth++ == max_depth) { done(); return; } basenames.forEach(function(basename) { process_path(new DirTreeNode(), dtn, resolved_path + sep + basename, path_from_root + sep + basename, path_rel_to_root + sep + basename, basename, depth); }); done(); }); } else if (stats.isFile()) { if (efr != null) { efr.lastIndex = 0; if (efr.test(path_rel_to_root)) { done(); return; } } if (ifr != null) { ifr.lastIndex = 0; if (!ifr.test(path_rel_to_root)) { done(); return; } } dtn.path_from_root = path_from_root; dtn.parent = parent; dtn.path = resolved_path; dtn.name = basename; dtn.created_on = stats.birthtime; dtn.modified_on = stats.mtime; dtn.size = stats.size; parent.files.push(dtn); parent.size_of_files += dtn.size; while (parent) { ++parent.no_of_total_files; parent.size += dtn.size; parent = parent.parent; } done(); } else done(); }); } try { process_root_path(root_dtn, { dirs: [], files: [], unread_paths: [], path_from_root: '', no_of_total_files: 0, no_of_total_dirs: 0, no_of_total_unread_paths: 0 }); } catch (error) { start_returning = true; emit_('error', error); } } DirTree.DirTreeNode = DirTreeNode; util.inherits(DirTree, EventEmitter); module.exports = DirTree;