1pub mod metadata;
2pub mod overwrite;
3pub mod report;
4pub mod unlink;
5
6use crate::cli::args::ShredMethod;
7use crate::error::{ShredError, ShredResult};
8use crate::ui::ProgressReporter;
9use chrono::Utc;
10use dialoguer::Confirm;
11use globset::{Glob, GlobSet, GlobSetBuilder};
12use metadata::MetadataHandler;
13use overwrite::Overwriter;
14use rayon::prelude::*;
15use report::{ShredEvent, ShredReport};
16use std::fs::{self, OpenOptions};
17use std::path::Path;
18use std::sync::atomic::{AtomicBool, Ordering};
19use std::sync::{Arc, Mutex};
20use unlink::Unlinker;
21use walkdir::WalkDir;
22
23pub struct Shredder {
25 method: ShredMethod,
26 passes: u32,
27 dry_run: bool,
28 verify: bool,
29 trim: bool,
30 force: bool,
31 exclude: GlobSet,
32 progress: Option<ProgressReporter>,
33 cancelled: Arc<AtomicBool>,
34 events: Arc<Mutex<Vec<ShredEvent>>>,
35}
36
37impl Shredder {
38 #[allow(clippy::too_many_arguments)]
40 pub fn new(
41 method: ShredMethod,
42 passes: u32,
43 dry_run: bool,
44 verify: bool,
45 trim: bool,
46 force: bool,
47 exclude_patterns: &[String],
48 show_progress: bool,
49 ) -> ShredResult<Self> {
50 let mut builder = GlobSetBuilder::new();
51 for pattern in exclude_patterns {
52 builder.add(Glob::new(pattern).map_err(|e| ShredError::Cli(e.to_string()))?);
53 }
54
55 Ok(Self {
56 method,
57 passes,
58 dry_run,
59 verify,
60 trim,
61 force,
62 exclude: builder
63 .build()
64 .map_err(|e| ShredError::Cli(e.to_string()))?,
65 progress: if show_progress {
66 Some(ProgressReporter::new())
67 } else {
68 None
69 },
70 cancelled: Arc::new(AtomicBool::new(false)),
71 events: Arc::new(Mutex::new(Vec::new())),
72 })
73 }
74
75 pub fn cancel(&self) {
77 self.cancelled.store(true, Ordering::SeqCst);
78 }
79
80 fn is_cancelled(&self) -> bool {
81 self.cancelled.load(Ordering::Relaxed)
82 }
83
84 pub fn generate_report(&self) -> ShredReport {
86 let events = self.events.lock().unwrap().clone();
87 ShredReport::new(events)
88 }
89
90 fn record_event(&self, event: ShredEvent) {
91 if let Ok(mut events) = self.events.lock() {
92 events.push(event);
93 }
94 }
95
96 fn should_exclude(&self, path: &Path) -> bool {
97 self.exclude.is_match(path)
98 }
99
100 fn shred_file(&self, path: &Path, keep: bool) -> ShredResult<()> {
101 if self.is_cancelled() {
102 return Ok(());
103 }
104
105 let metadata = fs::symlink_metadata(path)?;
107 if metadata.file_type().is_symlink() {
108 if !self.dry_run && !keep {
109 fs::remove_file(path)?;
110 }
111 return Ok(());
112 }
113
114 if !metadata.is_file() && !metadata.is_dir() {
116 return Ok(());
117 }
118
119 if self.should_exclude(path) {
120 return Ok(());
121 }
122
123 if self.dry_run {
124 self.record_event(ShredEvent {
125 path: path.to_path_buf(),
126 timestamp: Utc::now(),
127 method: self.method.clone(),
128 success: true,
129 error: None,
130 });
131 return Ok(());
132 }
133
134 let res = (|| -> ShredResult<()> {
135 let mut options = OpenOptions::new();
136 options.read(true).write(true);
137
138 #[cfg(windows)]
139 {
140 use std::os::windows::fs::OpenOptionsExt;
141 options.custom_flags(0x80000000);
144 }
145
146 let mut file = options.open(path)?;
147 let mut overwriter =
148 Overwriter::new(&mut file, self.verify, Arc::clone(&self.cancelled));
149 overwriter.execute(self.method.clone(), self.passes)?;
150 drop(file);
151
152 let obfuscated_path = MetadataHandler::obfuscate_filename(path)?;
153 if self.trim {
154 let _ = MetadataHandler::trim(&obfuscated_path);
155 }
156 MetadataHandler::truncate(&obfuscated_path)?;
157
158 if !keep {
159 Unlinker::unlink(&obfuscated_path)?;
160 }
161 Ok(())
162 })();
163
164 self.record_event(ShredEvent {
165 path: path.to_path_buf(),
166 timestamp: Utc::now(),
167 method: self.method.clone(),
168 success: res.is_ok(),
169 error: res.as_ref().err().map(|e| e.to_string()),
170 });
171
172 if res.is_ok() {
173 if let Some(ref pr) = self.progress {
174 pr.inc_file_complete();
175 }
176 }
177 res
178 }
179
180 pub fn shred(&self, path: &Path, recursive: bool, keep: bool) -> ShredResult<()> {
182 if !path.exists() {
183 return Err(ShredError::InvalidPath(format!(
184 "Path does not exist: {:?}",
185 path
186 )));
187 }
188
189 if !self.force && !self.dry_run {
191 let prompt = if path.is_dir() && recursive {
192 format!(
193 "Are you sure you want to RECURSIVELY destroy all contents in {:?}?",
194 path
195 )
196 } else {
197 format!("Are you sure you want to permanently destroy {:?}?", path)
198 };
199
200 if !Confirm::new()
201 .with_prompt(prompt)
202 .default(false)
203 .interact()
204 .unwrap_or(false)
205 {
206 return Err(ShredError::Cli("Operation cancelled by user".to_string()));
207 }
208 }
209
210 if path.is_file() {
211 if let Some(ref pr) = self.progress {
212 pr.start_files(1);
213 }
214 let res = self.shred_file(path, keep);
215 if let Some(ref pr) = self.progress {
216 pr.finish();
217 }
218 res
219 } else if path.is_dir() {
220 if !recursive {
221 return Err(ShredError::InvalidPath(format!(
222 "Target {:?} is a directory. Use --recursive.",
223 path
224 )));
225 }
226
227 let mut file_count = 0;
230 for entry in WalkDir::new(path).into_iter().flatten() {
231 if entry.file_type().is_file() && !self.should_exclude(entry.path()) {
232 file_count += 1;
233 }
234 }
235
236 if let Some(ref pr) = self.progress {
237 pr.start_files(file_count);
238 }
239
240 let res = WalkDir::new(path)
243 .into_iter()
244 .flatten()
245 .filter(|e| e.file_type().is_file() && !self.should_exclude(e.path()))
246 .par_bridge() .try_for_each(|entry| self.shred_file(entry.path(), keep));
248
249 if let Some(ref pr) = self.progress {
250 pr.finish();
251 }
252
253 if res.is_ok() && !keep && !self.dry_run && !self.is_cancelled() {
255 let mut dirs: Vec<_> = WalkDir::new(path)
258 .into_iter()
259 .flatten()
260 .filter(|e| e.file_type().is_dir())
261 .map(|e| e.into_path())
262 .collect();
263
264 dirs.sort_by_key(|b| std::cmp::Reverse(b.as_os_str().len()));
265 for dir in dirs {
266 if dir.exists() {
267 let _ = fs::remove_dir(dir);
268 }
269 }
270 }
271 res
272 } else {
273 Err(ShredError::InvalidPath(format!(
274 "Invalid target type: {:?}",
275 path
276 )))
277 }
278 }
279}