1use crate::error::{DevSweepError, Result};
4use chrono::{DateTime, Utc};
5use serde::{Deserialize, Serialize};
6use std::path::PathBuf;
7
8#[derive(Debug, Clone, Serialize, Deserialize)]
10pub struct TrashRecord {
11 pub id: String,
13 pub original_path: PathBuf,
15 pub size: u64,
17 pub deleted_at: DateTime<Utc>,
19 pub project_name: String,
21 pub artifact_kind: String,
23 pub method: String,
25}
26
27impl TrashRecord {
28 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
47pub struct TrashRecordStore {
49 records_path: PathBuf,
50}
51
52impl TrashRecordStore {
53 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 if let Some(parent) = records_path.parent() {
62 std::fs::create_dir_all(parent)?;
63 }
64
65 Ok(Self { records_path })
66 }
67
68 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 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 pub fn add(&self, record: TrashRecord) -> Result<()> {
88 let mut records = self.load()?;
89 records.push(record);
90 self.save(&records)
91 }
92
93 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 pub fn clear(&self) -> Result<()> {
109 self.save(&[])
110 }
111
112 pub fn total_size(&self) -> Result<u64> {
114 let records = self.load()?;
115 Ok(records.iter().map(|r| r.size).sum())
116 }
117
118 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 }