Skip to main content

apif_utils/
file_utils.rs

1// Cross-platform file utilities
2
3use anyhow::Result;
4use std::path::{Path, PathBuf};
5
6/// File utilities for cross-platform operations
7pub struct FileUtils;
8
9impl FileUtils {
10    /// Collect all .gctf files from a directory, optionally excluding patterns
11    pub fn collect_test_files(path: &Path, exclude_patterns: &[String]) -> Vec<PathBuf> {
12        let mut files = Vec::new();
13        let walker = walkdir::WalkDir::new(path).follow_links(true).into_iter();
14        for entry in walker.flatten() {
15            let path = entry.path();
16            if path
17                .extension()
18                .is_some_and(|ext| ext == "gctf" || ext == "apif")
19                && !is_excluded(path, exclude_patterns)
20            {
21                files.push(path.to_path_buf());
22            }
23        }
24        files
25    }
26
27    /// Sort files by name or modification time
28    pub fn sort_files(files: &mut [PathBuf], sort_by: &str) {
29        match sort_by {
30            "name" => files.sort_by(|a, b| a.file_name().cmp(&b.file_name())),
31            "mtime" | "time" => files.sort_by(|a, b| {
32                Self::get_mtime(b)
33                    .unwrap_or(0)
34                    .cmp(&Self::get_mtime(a).unwrap_or(0))
35            }),
36            "random" => {
37                use rand::SeedableRng;
38                use rand::rngs::StdRng;
39                use rand::seq::SliceRandom;
40                let mut rng = StdRng::from_rng(&mut rand::rng());
41                files.shuffle(&mut rng);
42            }
43            _ => {}
44        }
45    }
46
47    /// Get file modification time as Unix timestamp
48    pub fn get_mtime(path: &Path) -> Result<i64> {
49        let metadata = std::fs::metadata(path)?;
50        let mtime = metadata
51            .modified()?
52            .duration_since(std::time::UNIX_EPOCH)
53            .unwrap_or_default()
54            .as_secs() as i64;
55        Ok(mtime)
56    }
57
58    /// Get file size in bytes
59    pub fn get_file_size(path: &Path) -> Result<u64> {
60        let metadata = std::fs::metadata(path)?;
61        Ok(metadata.len())
62    }
63
64    /// Resolve a relative path against a base file's directory
65    pub fn resolve_relative_path(base_file_path: &Path, relative_path: &str) -> PathBuf {
66        let base_dir = base_file_path.parent().unwrap_or(Path::new("."));
67        base_dir.join(relative_path)
68    }
69}
70
71fn is_excluded(path: &Path, exclude_patterns: &[String]) -> bool {
72    if exclude_patterns.is_empty() {
73        return false;
74    }
75    let path_str = path.to_string_lossy();
76    exclude_patterns.iter().any(|pattern| {
77        if let Ok(glob) = globset::Glob::new(pattern).map(|g| g.compile_matcher()) {
78            glob.is_match(path_str.as_ref())
79        } else {
80            path_str.contains(pattern.as_str())
81        }
82    })
83}
84
85#[cfg(test)]
86mod tests {
87    use super::*;
88    use std::fs;
89
90    #[test]
91    #[cfg(not(miri))]
92    fn test_collect_test_files_empty_dir() {
93        let dir = std::env::temp_dir().join("gctf_test_empty");
94        let _ = fs::remove_dir_all(&dir);
95        fs::create_dir_all(&dir).unwrap();
96        let files = FileUtils::collect_test_files(&dir, &[]);
97        assert!(files.is_empty());
98        let _ = fs::remove_dir_all(&dir);
99    }
100
101    #[test]
102    #[cfg(not(miri))]
103    fn test_collect_test_files_with_gctf() {
104        let dir = std::env::temp_dir().join("gctf_test_files");
105        let _ = fs::remove_dir_all(&dir);
106        fs::create_dir_all(&dir).unwrap();
107        fs::write(dir.join("test.gctf"), "content").unwrap();
108        fs::write(dir.join("other.txt"), "content").unwrap();
109        let files = FileUtils::collect_test_files(&dir, &[]);
110        assert_eq!(files.len(), 1);
111        let _ = fs::remove_dir_all(&dir);
112    }
113
114    #[test]
115    fn test_resolve_relative_path() {
116        let base = Path::new("/home/user/tests/test.gctf");
117        let resolved = FileUtils::resolve_relative_path(base, "data/file.csv");
118        assert_eq!(resolved, Path::new("/home/user/tests/data/file.csv"));
119    }
120
121    #[test]
122    #[cfg(not(miri))]
123    fn test_get_file_size() {
124        let dir = std::env::temp_dir().join("gctf_test_size");
125        let _ = fs::remove_dir_all(&dir);
126        fs::create_dir_all(&dir).unwrap();
127        let file = dir.join("test.gctf");
128        fs::write(&file, "hello").unwrap();
129        assert_eq!(FileUtils::get_file_size(&file).unwrap(), 5);
130        let _ = fs::remove_dir_all(&dir);
131    }
132
133    #[test]
134    fn test_is_excluded() {
135        assert!(!is_excluded(Path::new("test.gctf"), &[]));
136        assert!(is_excluded(Path::new("test.gctf"), &["test*".into()]));
137        assert!(!is_excluded(Path::new("other.gctf"), &["test*".into()]));
138        // Glob pattern matching
139        assert!(is_excluded(Path::new("test.gctf"), &["*.gctf".into()]));
140        assert!(!is_excluded(Path::new("test.gctf"), &["*.txt".into()]));
141    }
142
143    #[test]
144    fn test_sort_files_by_name() {
145        let mut files = vec![
146            PathBuf::from("b.gctf"),
147            PathBuf::from("a.gctf"),
148            PathBuf::from("c.gctf"),
149        ];
150        FileUtils::sort_files(&mut files, "name");
151        assert_eq!(files[0].file_name().unwrap(), "a.gctf");
152        assert_eq!(files[1].file_name().unwrap(), "b.gctf");
153        assert_eq!(files[2].file_name().unwrap(), "c.gctf");
154    }
155
156    #[test]
157    fn test_sort_files_unsupported() {
158        let mut files = vec![PathBuf::from("b.gctf"), PathBuf::from("a.gctf")];
159        FileUtils::sort_files(&mut files, "unsupported");
160        // Should stay in original order
161        assert_eq!(files[0].file_name().unwrap(), "b.gctf");
162    }
163
164    #[test]
165    fn test_get_mtime_nonexistent() {
166        let result = FileUtils::get_mtime(Path::new("/nonexistent/path.gctf"));
167        assert!(result.is_err());
168    }
169}