acme_disk_use/
disk_use.rs

1//! High-level disk usage analysis interface combining cache and scanner
2
3use std::{io, path::Path};
4
5use crate::cache::CacheManager;
6use crate::scanner::{self, DirStat};
7
8/// Main interface for disk usage analysis with caching support
9pub struct DiskUse {
10    cache_manager: CacheManager,
11}
12
13impl DiskUse {
14    /// Create a new DiskUse instance with the specified cache file path
15    pub fn new(cache_path: impl AsRef<Path>) -> Self {
16        Self {
17            cache_manager: CacheManager::new(cache_path),
18        }
19    }
20
21    /// Create a new DiskUse instance using the default cache location
22    pub fn new_with_default_cache() -> Self {
23        Self::new(crate::get_default_cache_path())
24    }
25
26    /// Scan a directory and return its total size in bytes
27    ///
28    /// This method automatically:
29    /// - Loads from cache
30    /// - Scans only changed directories
31    /// - Saves the updated cache
32    pub fn scan(&mut self, path: impl AsRef<Path>) -> io::Result<u64> {
33        self.scan_with_options(path, false)
34    }
35
36    /// Scan a directory with options for ignoring cache
37    ///
38    /// # Arguments
39    /// * `path` - The directory path to scan
40    /// * `ignore_cache` - If true, performs a fresh scan without using cache
41    pub fn scan_with_options(
42        &mut self,
43        path: impl AsRef<Path>,
44        ignore_cache: bool,
45    ) -> io::Result<u64> {
46        let path = path.as_ref();
47
48        // Normalize path to avoid issues with symlinks and /private on macOS
49        let path_buf = path.canonicalize().unwrap_or_else(|_| path.to_path_buf());
50
51        // Get existing cache entry for this root (unless ignoring cache)
52        let old_entry = if ignore_cache {
53            None
54        } else {
55            self.cache_manager.get(&path_buf)
56        };
57
58        // Scan the directory (will use cache for unchanged subdirectories)
59        let new_entry = scanner::scan_directory(path, old_entry)?;
60
61        // Get the total size before potentially moving new_entry
62        let total_size = new_entry.total_size();
63
64        // Update the cache with new results (unless ignoring cache)
65        if !ignore_cache {
66            self.cache_manager.update(&path_buf, new_entry);
67            // Cache will auto-save on drop
68        }
69
70        Ok(total_size)
71    }
72
73    /// Get detailed statistics for a previously scanned path
74    pub fn get_stats(&self, path: impl AsRef<Path>) -> Option<&DirStat> {
75        self.cache_manager.get(path.as_ref())
76    }
77
78    /// Get file count for a path
79    ///
80    /// # Arguments
81    /// * `path` - The path to get file count for
82    /// * `ignore_cache` - If true, counts files directly from filesystem instead of using cache
83    pub fn get_file_count(&self, path: impl AsRef<Path>, ignore_cache: bool) -> io::Result<u64> {
84        if ignore_cache {
85            scanner::count_files(path.as_ref())
86        } else {
87            Ok(self
88                .get_stats(path)
89                .map(|stats| stats.file_count())
90                .unwrap_or(0))
91        }
92    }
93
94    /// Save the current cache to disk
95    pub fn save_cache(&mut self) -> io::Result<()> {
96        self.cache_manager.save()
97    }
98
99    /// Clear all cache contents
100    pub fn clear_cache(&mut self) -> io::Result<()> {
101        self.cache_manager.clear()
102    }
103
104    /// Delete the cache file
105    pub fn delete_cache(&self) -> io::Result<()> {
106        self.cache_manager.delete()
107    }
108
109    /// Get the cache file path
110    pub fn cache_path(&self) -> &Path {
111        self.cache_manager.path()
112    }
113}
114
115#[cfg(test)]
116mod tests {
117    use super::*;
118    use std::fs;
119    use tempfile::TempDir;
120
121    fn create_test_directory_structure(base: &Path) -> io::Result<()> {
122        fs::create_dir_all(base.join("subdir1"))?;
123        fs::create_dir_all(base.join("subdir2/nested"))?;
124
125        fs::write(base.join("file1.txt"), "Hello World")?;
126        fs::write(base.join("file2.txt"), "Test content")?;
127        fs::write(base.join("subdir1/nested_file.txt"), "Nested content here")?;
128        fs::write(base.join("subdir2/another.txt"), "More content")?;
129        fs::write(base.join("subdir2/nested/deep.txt"), "Deep file content")?;
130
131        Ok(())
132    }
133
134    #[test]
135    fn test_disk_use_with_cache() -> io::Result<()> {
136        let temp_dir = TempDir::new()?;
137        let test_dir = temp_dir.path().join("test");
138        let cache_file = temp_dir.path().join("cache.bin");
139
140        fs::create_dir(&test_dir)?;
141        create_test_directory_structure(&test_dir)?;
142
143        let canonical_test_dir = test_dir.canonicalize()?;
144
145        {
146            let mut disk_use = DiskUse::new(&cache_file);
147            let size1 = disk_use.scan(&canonical_test_dir)?;
148            assert_eq!(size1, 71);
149
150            // Force save by explicitly calling save_cache
151            disk_use.save_cache()?;
152        } // Drop happens here, ensuring save
153
154        assert!(cache_file.exists());
155
156        {
157            let mut disk_use = DiskUse::new(&cache_file);
158            let _size2 = disk_use.scan(&canonical_test_dir)?;
159            assert_eq!(_size2, 71);
160
161            let file_count = disk_use.get_file_count(&canonical_test_dir, false)?;
162            assert_eq!(file_count, 5);
163        }
164
165        Ok(())
166    }
167
168    #[test]
169    fn test_disk_use_ignore_cache() -> io::Result<()> {
170        let temp_dir = TempDir::new()?;
171        let test_dir = temp_dir.path().join("test");
172        let cache_file = temp_dir.path().join("cache.json");
173
174        fs::create_dir(&test_dir)?;
175        create_test_directory_structure(&test_dir)?;
176
177        let mut disk_use = DiskUse::new(&cache_file);
178
179        let size1 = disk_use.scan(&test_dir)?;
180        assert_eq!(size1, 71);
181
182        fs::write(test_dir.join("new_file.txt"), "New content")?;
183
184        let _size2 = disk_use.scan(&test_dir)?;
185
186        let size3 = disk_use.scan_with_options(&test_dir, true)?;
187        assert_eq!(size3, 82);
188
189        Ok(())
190    }
191
192    #[test]
193    fn test_cache_management() -> io::Result<()> {
194        let temp_dir = TempDir::new()?;
195        let test_dir = temp_dir.path().join("test");
196        let cache_file = temp_dir.path().join("cache.bin");
197
198        fs::create_dir(&test_dir)?;
199        create_test_directory_structure(&test_dir)?;
200
201        {
202            let mut disk_use = DiskUse::new(&cache_file);
203
204            disk_use.scan(&test_dir)?;
205            disk_use.save_cache()?; // Explicit save
206        } // Drop saves too
207
208        assert!(cache_file.exists());
209
210        {
211            let mut disk_use = DiskUse::new(&cache_file);
212            disk_use.clear_cache()?;
213
214            disk_use.delete_cache()?;
215        }
216
217        assert!(!cache_file.exists());
218
219        Ok(())
220    }
221}