Skip to main content

sys_shred/core/
mod.rs

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
23/// The primary coordinator for the file destruction lifecycle.
24pub 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    /// Initializes a new `Shredder` with the specified destruction policy.
39    #[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    /// Signals the shredder to stop current operations as soon as possible.
76    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    /// Returns the accumulated audit report.
85    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        // Safety: Check if it's a symlink. We only delete the link, NOT the target data.
106        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        // Safety: Skip special files (FIFOs, Sockets, etc.) to prevent hanging.
115        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    /// Entry point for the shredding operation.
170    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        // Professional Guard: Interactive Confirmation
179        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            // For directories, we still need a count for the progress bar
213            // but we'll collect only the directory entries for bottom-up cleanup.
214            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            // Parallel execution using the pre-collected entries for now,
229            // but filtered for files.
230            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}