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 file = OpenOptions::new().read(true).write(true).open(path)?;
136 let mut overwriter =
137 Overwriter::new(&mut file, self.verify, Arc::clone(&self.cancelled));
138 overwriter.execute(self.method.clone(), self.passes)?;
139 drop(file);
140
141 let obfuscated_path = MetadataHandler::obfuscate_filename(path)?;
142 if self.trim {
143 let _ = MetadataHandler::trim(&obfuscated_path);
144 }
145 MetadataHandler::truncate(&obfuscated_path)?;
146
147 if !keep {
148 Unlinker::unlink(&obfuscated_path)?;
149 }
150 Ok(())
151 })();
152
153 self.record_event(ShredEvent {
154 path: path.to_path_buf(),
155 timestamp: Utc::now(),
156 method: self.method.clone(),
157 success: res.is_ok(),
158 error: res.as_ref().err().map(|e| e.to_string()),
159 });
160
161 if res.is_ok() {
162 if let Some(ref pr) = self.progress {
163 pr.inc_file_complete();
164 }
165 }
166 res
167 }
168
169 pub fn shred(&self, path: &Path, recursive: bool, keep: bool) -> ShredResult<()> {
171 if !path.exists() {
172 return Err(ShredError::InvalidPath(format!(
173 "Path does not exist: {:?}",
174 path
175 )));
176 }
177
178 if !self.force && !self.dry_run {
180 let prompt = if path.is_dir() && recursive {
181 format!(
182 "Are you sure you want to RECURSIVELY destroy all contents in {:?}?",
183 path
184 )
185 } else {
186 format!("Are you sure you want to permanently destroy {:?}?", path)
187 };
188
189 if !Confirm::new()
190 .with_prompt(prompt)
191 .default(false)
192 .interact()
193 .unwrap_or(false)
194 {
195 return Err(ShredError::Cli("Operation cancelled by user".to_string()));
196 }
197 }
198
199 if path.is_file() {
200 if let Some(ref pr) = self.progress {
201 pr.start_files(1);
202 }
203 self.shred_file(path, keep)
204 } else if path.is_dir() {
205 if !recursive {
206 return Err(ShredError::InvalidPath(format!(
207 "Target {:?} is a directory. Use --recursive.",
208 path
209 )));
210 }
211
212 let mut entries = Vec::new();
215 let mut file_count = 0;
216
217 for e in WalkDir::new(path).into_iter().flatten() {
218 if e.file_type().is_file() {
219 file_count += 1;
220 }
221 entries.push(e.into_path());
222 }
223
224 if let Some(ref pr) = self.progress {
225 pr.start_files(file_count);
226 }
227
228 entries
231 .par_iter()
232 .filter(|p| p.is_file())
233 .try_for_each(|f| self.shred_file(f, keep))?;
234
235 if !keep && !self.dry_run && !self.is_cancelled() {
236 entries.sort_by_key(|b| std::cmp::Reverse(b.as_os_str().len()));
237 for dir in entries {
238 if dir.is_dir() && dir.exists() {
239 let _ = fs::remove_dir(dir);
240 }
241 }
242 }
243 Ok(())
244 } else {
245 Err(ShredError::InvalidPath(format!(
246 "Invalid target type: {:?}",
247 path
248 )))
249 }
250 }
251}