nameback_core/
rename_history.rs1use anyhow::Result;
2use serde::{Deserialize, Serialize};
3use std::collections::VecDeque;
4use std::fs;
5use std::path::PathBuf;
6
7#[derive(Debug, Clone, Serialize, Deserialize)]
9pub struct RenameOperation {
10 pub original_path: PathBuf,
12 pub new_path: PathBuf,
14 pub timestamp: u64,
16 pub undone: bool,
18}
19
20impl RenameOperation {
21 pub fn new(original_path: PathBuf, new_path: PathBuf) -> Self {
23 Self {
24 original_path,
25 new_path,
26 timestamp: std::time::SystemTime::now()
27 .duration_since(std::time::UNIX_EPOCH)
28 .unwrap()
29 .as_secs(),
30 undone: false,
31 }
32 }
33
34 pub fn undo(&mut self) -> Result<()> {
36 if self.undone {
37 anyhow::bail!("Operation already undone");
38 }
39
40 if !self.new_path.exists() {
42 anyhow::bail!(
43 "Cannot undo: File {} no longer exists",
44 self.new_path.display()
45 );
46 }
47
48 if self.original_path.exists() {
50 anyhow::bail!(
51 "Cannot undo: Original path {} is occupied",
52 self.original_path.display()
53 );
54 }
55
56 fs::rename(&self.new_path, &self.original_path)?;
58 self.undone = true;
59
60 log::info!(
61 "Undone rename: {} -> {}",
62 self.new_path.display(),
63 self.original_path.display()
64 );
65
66 Ok(())
67 }
68}
69
70#[derive(Debug, Clone, Serialize, Deserialize)]
72pub struct RenameHistory {
73 max_history: usize,
75 operations: VecDeque<RenameOperation>,
77 #[serde(skip)]
79 history_path: PathBuf,
80}
81
82impl RenameHistory {
83 pub fn new(history_path: PathBuf, max_history: usize) -> Self {
85 Self {
86 max_history,
87 operations: VecDeque::new(),
88 history_path,
89 }
90 }
91
92 pub fn load(history_path: PathBuf, max_history: usize) -> Result<Self> {
94 if history_path.exists() {
95 let data = fs::read_to_string(&history_path)?;
96 let mut history: RenameHistory = serde_json::from_str(&data)?;
97 history.history_path = history_path;
98 history.max_history = max_history;
99 Ok(history)
100 } else {
101 Ok(Self::new(history_path, max_history))
102 }
103 }
104
105 pub fn save(&self) -> Result<()> {
107 if let Some(parent) = self.history_path.parent() {
109 fs::create_dir_all(parent)?;
110 }
111
112 let data = serde_json::to_string_pretty(self)?;
113 fs::write(&self.history_path, data)?;
114 Ok(())
115 }
116
117 pub fn add(&mut self, operation: RenameOperation) {
119 self.operations.push_front(operation);
121
122 while self.operations.len() > self.max_history {
124 self.operations.pop_back();
125 }
126 }
127
128 pub fn operations(&self) -> &VecDeque<RenameOperation> {
130 &self.operations
131 }
132
133 pub fn last_undoable(&self) -> Option<&RenameOperation> {
135 self.operations.iter().find(|op| !op.undone)
136 }
137
138 pub fn undo_last(&mut self) -> Result<()> {
140 let last_idx = self
141 .operations
142 .iter()
143 .position(|op| !op.undone)
144 .ok_or_else(|| anyhow::anyhow!("No operations to undo"))?;
145
146 self.operations[last_idx].undo()?;
147 Ok(())
148 }
149
150 pub fn undo_at(&mut self, index: usize) -> Result<()> {
152 if index >= self.operations.len() {
153 anyhow::bail!("Invalid operation index");
154 }
155
156 self.operations[index].undo()?;
157 Ok(())
158 }
159
160 pub fn undoable_count(&self) -> usize {
162 self.operations.iter().filter(|op| !op.undone).count()
163 }
164
165 pub fn clear(&mut self) {
167 self.operations.clear();
168 }
169
170 pub fn stats(&self) -> HistoryStats {
172 HistoryStats {
173 total_operations: self.operations.len(),
174 undoable_operations: self.undoable_count(),
175 max_history: self.max_history,
176 }
177 }
178}
179
180#[derive(Debug)]
182pub struct HistoryStats {
183 pub total_operations: usize,
184 pub undoable_operations: usize,
185 pub max_history: usize,
186}
187
188#[cfg(test)]
189mod tests {
190 use super::*;
191 use tempfile::TempDir;
192
193 #[test]
194 fn test_rename_operation() -> Result<()> {
195 let temp_dir = TempDir::new()?;
196
197 let original = temp_dir.path().join("original.txt");
199 fs::write(&original, "test content")?;
200
201 let new = temp_dir.path().join("renamed.txt");
202
203 fs::rename(&original, &new)?;
205
206 let mut op = RenameOperation::new(original.clone(), new.clone());
208 assert!(!op.undone);
209
210 op.undo()?;
211
212 assert!(op.undone);
213 assert!(original.exists());
214 assert!(!new.exists());
215
216 Ok(())
217 }
218
219 #[test]
220 fn test_history_add_and_undo() -> Result<()> {
221 let temp_dir = TempDir::new()?;
222 let history_path = temp_dir.path().join("history.json");
223
224 let mut history = RenameHistory::new(history_path.clone(), 10);
225
226 let file1_orig = temp_dir.path().join("file1.txt");
228 let file1_new = temp_dir.path().join("file1_renamed.txt");
229 fs::write(&file1_orig, "content1")?;
230 fs::rename(&file1_orig, &file1_new)?;
231
232 let file2_orig = temp_dir.path().join("file2.txt");
233 let file2_new = temp_dir.path().join("file2_renamed.txt");
234 fs::write(&file2_orig, "content2")?;
235 fs::rename(&file2_orig, &file2_new)?;
236
237 history.add(RenameOperation::new(file1_orig.clone(), file1_new.clone()));
239 history.add(RenameOperation::new(file2_orig.clone(), file2_new.clone()));
240
241 assert_eq!(history.operations.len(), 2);
242 assert_eq!(history.undoable_count(), 2);
243
244 history.undo_last()?;
246 assert!(file2_orig.exists());
247 assert!(!file2_new.exists());
248 assert_eq!(history.undoable_count(), 1);
249
250 history.undo_last()?;
252 assert!(file1_orig.exists());
253 assert!(!file1_new.exists());
254 assert_eq!(history.undoable_count(), 0);
255
256 Ok(())
257 }
258
259 #[test]
260 fn test_history_persistence() -> Result<()> {
261 let temp_dir = TempDir::new()?;
262 let history_path = temp_dir.path().join("history.json");
263
264 let mut history = RenameHistory::new(history_path.clone(), 10);
266
267 let file_orig = temp_dir.path().join("file.txt");
268 let file_new = temp_dir.path().join("renamed.txt");
269
270 history.add(RenameOperation::new(file_orig.clone(), file_new.clone()));
271 history.save()?;
272
273 let loaded = RenameHistory::load(history_path, 10)?;
275 assert_eq!(loaded.operations.len(), 1);
276 assert_eq!(loaded.operations[0].original_path, file_orig);
277 assert_eq!(loaded.operations[0].new_path, file_new);
278
279 Ok(())
280 }
281
282 #[test]
283 fn test_history_max_size() {
284 let temp_dir = TempDir::new().unwrap();
285 let history_path = temp_dir.path().join("history.json");
286
287 let mut history = RenameHistory::new(history_path, 3);
288
289 for i in 0..5 {
291 let op = RenameOperation::new(
292 PathBuf::from(format!("file{}.txt", i)),
293 PathBuf::from(format!("renamed{}.txt", i)),
294 );
295 history.add(op);
296 }
297
298 assert_eq!(history.operations.len(), 3);
300
301 assert!(history.operations[0]
303 .original_path
304 .to_str()
305 .unwrap()
306 .contains("file4"));
307 }
308}