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