Skip to main content

null_e/trash/
record.rs

1//! Trash records - tracking deleted items for recovery
2
3use crate::error::{DevSweepError, Result};
4use chrono::{DateTime, Utc};
5use serde::{Deserialize, Serialize};
6use std::path::PathBuf;
7
8/// Record of a trashed item
9#[derive(Debug, Clone, Serialize, Deserialize)]
10pub struct TrashRecord {
11    /// Unique identifier
12    pub id: String,
13    /// Original path before deletion
14    pub original_path: PathBuf,
15    /// Size in bytes
16    pub size: u64,
17    /// When it was deleted
18    pub deleted_at: DateTime<Utc>,
19    /// Project name it belonged to
20    pub project_name: String,
21    /// Type of artifact
22    pub artifact_kind: String,
23    /// Delete method used
24    pub method: String,
25}
26
27impl TrashRecord {
28    /// Create a new trash record
29    pub fn new(
30        original_path: PathBuf,
31        size: u64,
32        project_name: impl Into<String>,
33        artifact_kind: impl Into<String>,
34    ) -> Self {
35        Self {
36            id: uuid::Uuid::new_v4().to_string(),
37            original_path,
38            size,
39            deleted_at: Utc::now(),
40            project_name: project_name.into(),
41            artifact_kind: artifact_kind.into(),
42            method: "trash".into(),
43        }
44    }
45}
46
47/// Storage for trash records
48pub struct TrashRecordStore {
49    records_path: PathBuf,
50}
51
52impl TrashRecordStore {
53    /// Create a new record store
54    pub fn new() -> Result<Self> {
55        let records_path = dirs::data_dir()
56            .ok_or_else(|| DevSweepError::Trash("Cannot find data directory".into()))?
57            .join("devsweep")
58            .join("trash_records.json");
59
60        // Ensure directory exists
61        if let Some(parent) = records_path.parent() {
62            std::fs::create_dir_all(parent)?;
63        }
64
65        Ok(Self { records_path })
66    }
67
68    /// Load all records
69    pub fn load(&self) -> Result<Vec<TrashRecord>> {
70        if !self.records_path.exists() {
71            return Ok(Vec::new());
72        }
73
74        let content = std::fs::read_to_string(&self.records_path)?;
75        let records: Vec<TrashRecord> = serde_json::from_str(&content)?;
76        Ok(records)
77    }
78
79    /// Save all records
80    pub fn save(&self, records: &[TrashRecord]) -> Result<()> {
81        let content = serde_json::to_string_pretty(records)?;
82        std::fs::write(&self.records_path, content)?;
83        Ok(())
84    }
85
86    /// Add a record
87    pub fn add(&self, record: TrashRecord) -> Result<()> {
88        let mut records = self.load()?;
89        records.push(record);
90        self.save(&records)
91    }
92
93    /// Remove a record by ID
94    pub fn remove(&self, id: &str) -> Result<Option<TrashRecord>> {
95        let mut records = self.load()?;
96        let pos = records.iter().position(|r| r.id == id);
97
98        if let Some(pos) = pos {
99            let removed = records.remove(pos);
100            self.save(&records)?;
101            Ok(Some(removed))
102        } else {
103            Ok(None)
104        }
105    }
106
107    /// Clear all records
108    pub fn clear(&self) -> Result<()> {
109        self.save(&[])
110    }
111
112    /// Get total size of tracked trash
113    pub fn total_size(&self) -> Result<u64> {
114        let records = self.load()?;
115        Ok(records.iter().map(|r| r.size).sum())
116    }
117
118    /// Get records older than a certain number of days
119    pub fn get_old_records(&self, days: u32) -> Result<Vec<TrashRecord>> {
120        let records = self.load()?;
121        let cutoff = Utc::now() - chrono::Duration::days(days as i64);
122
123        Ok(records
124            .into_iter()
125            .filter(|r| r.deleted_at < cutoff)
126            .collect())
127    }
128}
129
130impl Default for TrashRecordStore {
131    fn default() -> Self {
132        Self::new().expect("Failed to create trash record store")
133    }
134}
135
136#[cfg(test)]
137mod tests {
138    use super::*;
139    use tempfile::TempDir;
140
141    #[test]
142    fn test_trash_record_creation() {
143        let record = TrashRecord::new(
144            PathBuf::from("/test/node_modules"),
145            1000,
146            "my-project",
147            "dependencies",
148        );
149
150        assert!(!record.id.is_empty());
151        assert_eq!(record.original_path, PathBuf::from("/test/node_modules"));
152        assert_eq!(record.size, 1000);
153        assert_eq!(record.project_name, "my-project");
154    }
155
156    // Note: More comprehensive tests would require mocking the filesystem
157}