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 options = OpenOptions::new();
136            options.read(true).write(true);
137
138            #[cfg(windows)]
139            {
140                use std::os::windows::fs::OpenOptionsExt;
141                // FILE_FLAG_WRITE_THROUGH: Ensures data is written through any intermediate
142                // caches directly to the physical disk.
143                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    /// Entry point for the shredding operation.
181    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        // Professional Guard: Interactive Confirmation
190        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            // Dual-Pass Walk:
228            // Pass 1: Count files for the progress bar (minimal RAM, only integers)
229            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            // Pass 2: Process files in parallel using "True Streaming" via par_bridge.
241            // This avoids collecting millions of paths into a Vec, keeping RAM usage constant.
242            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() // Parallelize the iterator directly
247                .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            // Post-cleanup: Remove directories bottom-up if not keeping.
254            if res.is_ok() && !keep && !self.dry_run && !self.is_cancelled() {
255                // We do a final single-threaded walk to remove empty directories
256                // from the bottom up.
257                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}