1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
extern crate stacker;

use std::fs;
use std::io;
use std::path::Path;

#[cfg(target_os = "windows")]
fn fix_permissions(path: &Path) -> io::Result<()> {
	let mut permissions = fs::metadata(&path)?.permissions();
	permissions.set_readonly(false);
	fs::set_permissions(&path, permissions)
}

#[cfg(not(target_os = "windows"))]
fn fix_permissions(_: &Path) -> io::Result<()> {
	Ok(())
}


/// Force-removes a file/directory and all descendants.
///
/// In contrast to `std::fs::remove_dir_all`, it will remove
/// empty directories that lack read access on Linux,
/// and will remove "read-only" files and directories on Windows.
///
/// The current implementation is suboptimal in terms
/// of performance (e.g. number of syscalls made), but it should work.
pub fn force_remove_all<P: AsRef<Path>>(path: P, ignore_not_existing: bool) -> io::Result<()> {
	let path = path.as_ref();
	if ignore_not_existing && !path.exists() {
		return Ok(())
	}
	fix_permissions(path)?;
	if fs::remove_file(path).is_ok() {
		Ok(())
	} else if fs::remove_dir(path).is_ok() {
		Ok(())
	} else {
		for child in fs::read_dir(&path)? {
			let child = child?;
			let path = child.path();
			stacker::maybe_grow(4 * 1024, 16 * 1024, ||
				force_remove_all(&path, false),
			)?;
		}
		fs::remove_dir(path)
	}
}


#[cfg(not(target_os = "windows"))]  // windows may not have `rm`, `sh` and `chmod`
#[cfg(test)]
mod tests {
	use std::process::Command;

	fn rm_rf_success() -> bool {
		Command::new("rm").arg("-rf").arg("target").output().unwrap().status.success()
	}

	fn rust_remove_success() -> bool {
		super::force_remove_all("target", true).is_ok()
	}

	fn clean() {
		Command::new("sh").arg("-c").arg("chmod 777 target; rm -rf target").output().unwrap();
	}

	fn test_eq_behavior_fn<F>(up: &F, test_name: &str) where F: Fn() -> () {
		std::env::set_current_dir("target").unwrap();
		clean();
		up();
		let rm_success = rm_rf_success();
		clean();
		up();
		let rust_success = rust_remove_success();
		clean();
		eprintln!("Running test {}. `rm -rf` success: {}, `force_remove` success: {}",
			test_name, rm_success, rust_success);
		assert_eq!(rm_success, rust_success,
			"`rm -rf` and `force_remove` behaved differently for test: {}",
			test_name
		);
		std::env::set_current_dir("..").unwrap();
	}

	fn test_eq_behavior(up: &str) {
		test_eq_behavior_fn(&|| {
			assert!(Command::new("sh").arg("-c").arg(up).status().unwrap().success());
		}, up);
	}

	#[test]
	fn behavior_test() {
		test_eq_behavior("touch target");
		test_eq_behavior("touch target; chmod 000 target");
		test_eq_behavior("touch target; chmod 777 target");

		test_eq_behavior("mkdir target");
		test_eq_behavior("mkdir target; chmod 000 target");
		test_eq_behavior("mkdir target; chmod 777 target");

		test_eq_behavior("mkdir -p target/subdir; chmod 000 target");
		test_eq_behavior("mkdir -p target/subdir; chmod 777 target");
		test_eq_behavior("mkdir -p target/subdir; chmod 444 target");
		test_eq_behavior("mkdir -p target/subdir; chmod 222 target");
		test_eq_behavior("mkdir -p target/subdir; chmod 111 target");

		test_eq_behavior("");
	}
}