srusty-files 0.2.0

A high-performance, cross-platform file search engine library with REST API
Documentation
use crate::core::error::Result;
use crate::core::types::FileEntry;
use crate::utils::mime::detect_mime_type;
use crate::utils::path::is_hidden;
use chrono::{DateTime, TimeZone, Utc};
use std::fs;
use std::path::Path;

pub struct MetadataExtractor;

impl MetadataExtractor {
    pub fn extract<P: AsRef<Path>>(path: P) -> Result<FileEntry> {
        let path = path.as_ref();
        let metadata = fs::metadata(path)?;

        let mut entry = FileEntry::new(path.to_path_buf());

        entry.size = metadata.len();
        entry.is_directory = metadata.is_dir();
        entry.is_hidden = is_hidden(path);

        #[cfg(unix)]
        {
            entry.is_symlink = metadata.file_type().is_symlink();
        }

        #[cfg(windows)]
        {
            entry.is_symlink = metadata.file_type().is_symlink();
        }

        if let Ok(created) = metadata.created() {
            entry.created_at = Self::system_time_to_datetime(created);
        }

        if let Ok(modified) = metadata.modified() {
            entry.modified_at = Self::system_time_to_datetime(modified);
        }

        if let Ok(accessed) = metadata.accessed() {
            entry.accessed_at = Self::system_time_to_datetime(accessed);
        }

        if !entry.is_directory {
            entry.mime_type = detect_mime_type(path);
        }

        let now = Utc::now();
        entry.indexed_at = now;
        entry.last_verified = now;

        Ok(entry)
    }

    pub fn extract_batch<P: AsRef<Path> + Sync>(paths: &[P]) -> Vec<Result<FileEntry>> {
        use rayon::prelude::*;

        paths
            .par_iter()
            .map(|path| Self::extract(path.as_ref()))
            .collect()
    }

    fn system_time_to_datetime(time: std::time::SystemTime) -> Option<DateTime<Utc>> {
        time.duration_since(std::time::UNIX_EPOCH)
            .ok()
            .and_then(|duration| {
                Utc.timestamp_opt(duration.as_secs() as i64, duration.subsec_nanos())
                    .single()
            })
    }

    pub fn is_modified_since<P: AsRef<Path>>(
        path: P,
        since: DateTime<Utc>,
    ) -> Result<bool> {
        let metadata = fs::metadata(path)?;
        if let Ok(modified) = metadata.modified() {
            if let Some(modified_dt) = Self::system_time_to_datetime(modified) {
                return Ok(modified_dt > since);
            }
        }
        Ok(false)
    }

    pub fn get_file_size<P: AsRef<Path>>(path: P) -> Result<u64> {
        let metadata = fs::metadata(path)?;
        Ok(metadata.len())
    }

    pub fn is_readable<P: AsRef<Path>>(path: P) -> bool {
        fs::metadata(path).is_ok()
    }
}

#[cfg(test)]
mod tests {
    use super::*;
    use std::fs;
    use tempfile::TempDir;

    #[test]
    fn test_extract_file_metadata() {
        let temp_dir = TempDir::new().unwrap();
        let file_path = temp_dir.path().join("test.txt");
        fs::write(&file_path, "Hello, world!").unwrap();

        let entry = MetadataExtractor::extract(&file_path).unwrap();

        assert_eq!(entry.name, "test.txt");
        assert_eq!(entry.extension, Some("txt".to_string()));
        assert_eq!(entry.size, 13);
        assert!(!entry.is_directory);
    }

    #[test]
    fn test_extract_directory_metadata() {
        let temp_dir = TempDir::new().unwrap();
        let dir_path = temp_dir.path().join("subdir");
        fs::create_dir(&dir_path).unwrap();

        let entry = MetadataExtractor::extract(&dir_path).unwrap();

        assert_eq!(entry.name, "subdir");
        assert!(entry.is_directory);
    }

    #[test]
    fn test_extract_batch() {
        let temp_dir = TempDir::new().unwrap();
        let file1 = temp_dir.path().join("file1.txt");
        let file2 = temp_dir.path().join("file2.txt");

        fs::write(&file1, "content1").unwrap();
        fs::write(&file2, "content2").unwrap();

        let paths = vec![file1, file2];
        let results = MetadataExtractor::extract_batch(&paths);

        assert_eq!(results.len(), 2);
        assert!(results.iter().all(|r| r.is_ok()));
    }
}