use log::debug;
use std::collections::HashSet;
use std::fs;
use std::path::Path;
use structopt::StructOpt;
use walkdir::WalkDir;
const DEFAULT_FILES: &'static str = r"
Jenkinsfile,
Makefile,
Gulpfile.js,
Gruntfile.js,
gulpfile.js,
.DS_Store,
.tern-project,
.gitattributes,
.editorconfig,
.eslintrc,
eslint,
.eslintrc.js,
.eslintrc.json,
.eslintrc.yml,
.eslintignore,
.stylelintrc,
stylelint.config.js,
.stylelintrc.json,
.stylelintrc.yaml,
.stylelintrc.yml,
.stylelintrc.js,
.htmllintrc,
htmllint.js,
.lint,
.npmrc,
.npmignore,
.jshintrc,
.flowconfig,
.documentup.json,
.yarn-metadata.json,
.travis.yml,
appveyor.yml,
.gitlab-ci.yml,
circle.yml,
.coveralls.yml,
CHANGES,
changelog,
License,
LICENSE.txt,
LICENSE,
LICENSE-MIT,
LICENSE.BSD,
license,
LICENCE.txt,
LICENCE,
LICENCE-MIT,
LICENCE.BSD,
licence,
AUTHORS,
CONTRIBUTORS,
.yarn-integrity,
.yarnclean,
_config.yml,
.babelrc,
.yo-rc.json,
jest.config.js,
karma.conf.js,
wallaby.js,
wallaby.conf.js,
.prettierrc,
.prettierrc.yml,
.prettierrc.toml,
.prettierrc.js,
.prettierrc.json,
prettier.config.js,
.appveyor.yml,
tsconfig.json,
tslint.json,
";
const DEFAULT_DIRS: &'static str = r"
__tests__,
test,
tests,
testing,
benchmark,
powered-test,
docs,
doc,
.idea,
.vscode,
website,
images,
assets,
example,
examples,
coverage,
.nyc_output,
.circleci,
.github,
";
const DEFAULT_EXTS: &'static str = r"
markdown,
md,
mkd,
ts,
jst,
coffee,
tgz,
swp,
";
#[derive(Debug, StructOpt)]
#[structopt(name = "node-prune")]
pub struct Config {
#[structopt(short, long, default_value = "node_modules")]
pub path: String,
#[structopt(short, long)]
pub verbose: bool,
}
#[derive(Debug)]
pub struct Stats {
pub files_total: u64,
pub files_removed: u64,
pub removed_size: u64,
}
#[derive(Debug)]
pub struct Prune<'a> {
dir: &'a Path,
files: HashSet<String>,
exts: HashSet<String>,
dirs: HashSet<String>,
}
impl<'a> Prune<'a> {
pub fn new() -> Self {
Self {
dir: Path::new("node_modules"),
files: split(DEFAULT_FILES),
dirs: split(DEFAULT_DIRS),
exts: split(DEFAULT_EXTS),
}
}
pub fn run(&self) -> Result<Stats, walkdir::Error> {
let mut stats = Stats {
files_total: 0,
removed_size: 0,
files_removed: 0,
};
let mut walker = WalkDir::new(self.dir).into_iter();
loop {
let entry = match walker.next() {
Some(Ok(entry)) => entry,
Some(Err(err)) => return Err(err),
None => break,
};
let filepath = entry.path();
if !self.need_prune(filepath) {
continue;
}
stats.files_total += 1;
stats.removed_size += entry.metadata().unwrap().len();
if filepath.is_dir() {
let s = dir_stats(filepath)?;
stats.files_total += s.files_total;
stats.files_removed += s.files_removed;
stats.removed_size += s.removed_size;
match fs::remove_dir_all(filepath) {
Ok(_) => debug!("removed {}", filepath.display()),
Err(err) => {
debug!("removed dir {} failed with err {}", filepath.display(), err)
}
};
walker.skip_current_dir();
continue;
}
match fs::remove_file(filepath) {
Ok(_) => debug!("removed {}", filepath.display()),
Err(err) => debug!(
"removed file {} failed with err {}",
filepath.display(),
err
),
}
}
Ok(stats)
}
fn need_prune(&self, filepath: &Path) -> bool {
let filename = filepath.file_name().unwrap().to_str().unwrap();
if filepath.is_dir() {
return self.dirs.contains(filename);
}
if self.files.contains(filename) {
return true;
}
if let Some(extension) = filepath.extension() {
let ext = extension.to_str().unwrap();
if self.exts.contains(ext) {
return true;
}
}
false
}
}
fn dir_stats(dir: &Path) -> Result<Stats, walkdir::Error> {
let walker = WalkDir::new(dir).into_iter().filter_map(|e| e.ok());
let mut stats = Stats {
files_total: 0,
files_removed: 0,
removed_size: 0,
};
for entry in walker {
let metadata = entry.metadata()?;
stats.files_total += 1;
stats.files_removed += 1;
stats.removed_size += metadata.len();
}
Ok(stats)
}
fn split(paths: &str) -> HashSet<String> {
paths
.split(",")
.map(|x| x.trim().to_string())
.filter(|x| !x.is_empty())
.collect()
}
#[cfg(test)]
mod tests {
use super::{dir_stats, split};
use std::path::Path;
#[test]
fn dir_stats_happy_path() {
let path = Path::new("src");
let stats = dir_stats(path).unwrap();
assert_eq!(stats.files_total, 4);
assert_eq!(stats.files_removed, 4);
}
#[test]
fn dir_not_exits() {
let path = Path::new("not_exist");
let stats = dir_stats(path).unwrap();
assert_eq!(stats.files_removed, 0);
assert_eq!(stats.files_total, 0);
assert_eq!(stats.files_removed, 0);
}
#[test]
fn split_happypath() {
let paths = String::from("prettier,eslint,typescript,prettier");
let files = split(&paths[..]);
assert_eq!(files.len(), 3);
}
#[test]
fn split_string_with_consecutive_commas() {
let paths = String::from("prettier,,");
let files = split(&paths[..]);
assert_eq!(files.len(), 1);
}
#[test]
fn split_string_with_trim() {
let paths = String::from(" prettier ,javascript es6");
let files = split(&paths[..]);
assert_eq!(files.len(), 2);
assert!(files.contains("prettier"));
assert!(files.contains("javascript es6"));
}
}