simple_fs/safer_remove/
safer_remove_impl.rs1use crate::SPath;
2use crate::error::{Cause, PathAndCause};
3use crate::safer_remove::SaferRemoveOptions;
4use crate::{Error, Result};
5use std::fs;
6
7pub fn safer_remove_dir<'a>(dir_path: &SPath, options: impl Into<SaferRemoveOptions<'a>>) -> Result<bool> {
17 let options = options.into();
18
19 if !dir_path.exists() {
21 return Ok(false);
22 }
23
24 check_path_for_deletion_safety::<true>(dir_path, &options)?;
25
26 fs::remove_dir_all(dir_path.as_std_path()).map_err(|e| {
28 Error::DirNotSafeToRemove(PathAndCause {
29 path: dir_path.to_string(),
30 cause: Cause::Io(Box::new(e)),
31 })
32 })?;
33
34 Ok(true)
35}
36
37pub fn safer_remove_file<'a>(file_path: &SPath, options: impl Into<SaferRemoveOptions<'a>>) -> Result<bool> {
47 let options = options.into();
48
49 if !file_path.exists() {
51 return Ok(false);
52 }
53
54 check_path_for_deletion_safety::<false>(file_path, &options)?;
55
56 fs::remove_file(file_path.as_std_path()).map_err(|e| {
58 Error::FileNotSafeToRemove(PathAndCause {
59 path: file_path.to_string(),
60 cause: Cause::Io(Box::new(e)),
61 })
62 })?;
63
64 Ok(true)
65}
66
67fn check_path_for_deletion_safety<const IS_DIR: bool>(path: &SPath, options: &SaferRemoveOptions<'_>) -> Result<()> {
76 let resolved = path.canonicalize()?;
78 let resolved_str = resolved.as_str();
79 let path_str = path.as_str();
80
81 let mut error_causes = Vec::new();
83
84 if options.restrict_to_current_dir {
86 let current_dir = std::env::current_dir().map_err(|e| {
87 let pac = PathAndCause {
88 path: path.to_string(),
89 cause: Cause::Io(Box::new(e)),
90 };
91 if IS_DIR {
92 Error::DirNotSafeToRemove(pac)
93 } else {
94 Error::FileNotSafeToRemove(pac)
95 }
96 })?;
97 let current_dir_path = SPath::from_std_path_buf(current_dir)?;
98 let current_resolved = current_dir_path.canonicalize()?;
99 let current_str = current_resolved.as_str();
100
101 if !resolved_str.starts_with(current_str) {
102 error_causes.push(format!("is not below current directory '{current_resolved}'"));
103 }
104 }
105
106 if let Some(patterns) = options.must_contain_any {
108 if patterns.is_empty() {
109 error_causes.push("must_contain_any cannot be an empty list (use None to disable)".to_string());
110 } else {
111 let has_any = patterns.iter().any(|s| path_str.contains(s));
112 if !has_any {
113 error_causes.push(format!("does not contain any of the required patterns: {patterns:?}"));
114 }
115 }
116 }
117
118 if let Some(patterns) = options.must_contain_all {
120 if patterns.is_empty() {
121 error_causes.push("must_contain_all cannot be an empty list (use None to disable)".to_string());
122 } else {
123 let missing: Vec<_> = patterns.iter().filter(|s| !path_str.contains(*s)).collect();
124 if !missing.is_empty() {
125 error_causes.push(format!("does not contain all required patterns, missing: {missing:?}"));
126 }
127 }
128 }
129
130 if !error_causes.is_empty() {
131 let cause_msg = format!("Safety check failed: {}", error_causes.join("; "));
132 let path_and_cause = PathAndCause {
133 path: path.to_string(),
134 cause: Cause::Custom(cause_msg),
135 };
136
137 if IS_DIR {
138 return Err(Error::DirNotSafeToRemove(path_and_cause));
139 } else {
140 return Err(Error::FileNotSafeToRemove(path_and_cause));
141 }
142 }
143
144 Ok(())
145}
146
147