nameback_core/
rename_history.rs

1use anyhow::Result;
2use serde::{Deserialize, Serialize};
3use std::collections::VecDeque;
4use std::fs;
5use std::path::PathBuf;
6
7/// A single rename operation in the history
8#[derive(Debug, Clone, Serialize, Deserialize)]
9pub struct RenameOperation {
10    /// Original file path (before rename)
11    pub original_path: PathBuf,
12    /// New file path (after rename)
13    pub new_path: PathBuf,
14    /// Timestamp when the rename occurred (Unix timestamp)
15    pub timestamp: u64,
16    /// Whether this operation has been undone
17    pub undone: bool,
18}
19
20impl RenameOperation {
21    /// Create a new rename operation
22    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    /// Undo this rename operation (rename back to original)
35    pub fn undo(&mut self) -> Result<()> {
36        if self.undone {
37            anyhow::bail!("Operation already undone");
38        }
39
40        // Check if the new path still exists
41        if !self.new_path.exists() {
42            anyhow::bail!(
43                "Cannot undo: File {} no longer exists",
44                self.new_path.display()
45            );
46        }
47
48        // Check if the original path is available
49        if self.original_path.exists() {
50            anyhow::bail!(
51                "Cannot undo: Original path {} is occupied",
52                self.original_path.display()
53            );
54        }
55
56        // Perform the undo (rename back to original)
57        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/// Rename history tracker
71#[derive(Debug, Clone, Serialize, Deserialize)]
72pub struct RenameHistory {
73    /// Maximum number of operations to keep in history
74    max_history: usize,
75    /// History of rename operations (newest first)
76    operations: VecDeque<RenameOperation>,
77    /// Path where history is persisted
78    #[serde(skip)]
79    history_path: PathBuf,
80}
81
82impl RenameHistory {
83    /// Create a new history tracker
84    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    /// Load history from disk, or create new if doesn't exist
93    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    /// Save history to disk
106    pub fn save(&self) -> Result<()> {
107        // Create parent directory if needed
108        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    /// Add a rename operation to the history
118    pub fn add(&mut self, operation: RenameOperation) {
119        // Add to front (newest first)
120        self.operations.push_front(operation);
121
122        // Trim to max size
123        while self.operations.len() > self.max_history {
124            self.operations.pop_back();
125        }
126    }
127
128    /// Get all operations (newest first)
129    pub fn operations(&self) -> &VecDeque<RenameOperation> {
130        &self.operations
131    }
132
133    /// Get the most recent operation that can be undone
134    pub fn last_undoable(&self) -> Option<&RenameOperation> {
135        self.operations.iter().find(|op| !op.undone)
136    }
137
138    /// Undo the most recent operation
139    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    /// Undo a specific operation by index
151    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    /// Get count of undoable operations
161    pub fn undoable_count(&self) -> usize {
162        self.operations.iter().filter(|op| !op.undone).count()
163    }
164
165    /// Clear all history
166    pub fn clear(&mut self) {
167        self.operations.clear();
168    }
169
170    /// Get history statistics
171    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/// History statistics
181#[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        // Create a test file
198        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        // Perform rename
204        fs::rename(&original, &new)?;
205
206        // Create operation and undo it
207        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        // Create test files
227        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        // Add operations
238        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        // Undo last (file2)
245        history.undo_last()?;
246        assert!(file2_orig.exists());
247        assert!(!file2_new.exists());
248        assert_eq!(history.undoable_count(), 1);
249
250        // Undo last (file1)
251        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        // Create history and add operation
265        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        // Load history and verify
274        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        // Add more operations than max_history
290        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        // Should only keep the 3 most recent
299        assert_eq!(history.operations.len(), 3);
300
301        // Newest should be at front
302        assert!(history.operations[0]
303            .original_path
304            .to_str()
305            .unwrap()
306            .contains("file4"));
307    }
308}