use crate::models::FileAttributes; use std::path::Path; /// Write CAN metadata as OS-level file attributes. /// - Unix/macOS: Extended Attributes (xattr) /// - Windows: NTFS Alternate Data Streams pub fn write_attributes(path: &Path, attrs: &FileAttributes) -> std::io::Result<()> { #[cfg(unix)] { write_xattr(path, attrs) } #[cfg(windows)] { write_ntfs_ads(path, attrs) } } /// Read CAN metadata from OS-level file attributes. pub fn read_attributes(path: &Path) -> std::io::Result { #[cfg(unix)] { read_xattr(path) } #[cfg(windows)] { read_ntfs_ads(path) } } // ── Unix implementation using xattr crate ── #[cfg(unix)] fn write_xattr(path: &Path, attrs: &FileAttributes) -> std::io::Result<()> { use xattr::FileExt; let file = std::fs::File::open(path)?; if let Some(ref v) = attrs.mime_type { file.set_xattr("user.can.mime_type", v.as_bytes())?; } if let Some(ref v) = attrs.application { file.set_xattr("user.can.application", v.as_bytes())?; } if let Some(ref v) = attrs.user { file.set_xattr("user.can.user", v.as_bytes())?; } if let Some(ref v) = attrs.tags { file.set_xattr("user.can.tags", v.as_bytes())?; } if let Some(ref v) = attrs.description { file.set_xattr("user.can.description", v.as_bytes())?; } if let Some(ref v) = attrs.human_filename { file.set_xattr("user.can.human_filename", v.as_bytes())?; } if let Some(ref v) = attrs.human_path { file.set_xattr("user.can.human_path", v.as_bytes())?; } Ok(()) } #[cfg(unix)] fn read_xattr(path: &Path) -> std::io::Result { use xattr::FileExt; let file = std::fs::File::open(path)?; let read_attr = |name: &str| -> Option { file.get_xattr(name) .ok() .flatten() .and_then(|bytes| String::from_utf8(bytes).ok()) }; Ok(FileAttributes { mime_type: read_attr("user.can.mime_type"), application: read_attr("user.can.application"), user: read_attr("user.can.user"), tags: read_attr("user.can.tags"), description: read_attr("user.can.description"), human_filename: read_attr("user.can.human_filename"), human_path: read_attr("user.can.human_path"), }) } // ── Windows implementation using NTFS Alternate Data Streams ── #[cfg(windows)] fn write_ntfs_ads(path: &Path, attrs: &FileAttributes) -> std::io::Result<()> { let base = path.to_string_lossy(); if let Some(ref v) = attrs.mime_type { std::fs::write(format!("{}:can.mime_type", base), v)?; } if let Some(ref v) = attrs.application { std::fs::write(format!("{}:can.application", base), v)?; } if let Some(ref v) = attrs.user { std::fs::write(format!("{}:can.user", base), v)?; } if let Some(ref v) = attrs.tags { std::fs::write(format!("{}:can.tags", base), v)?; } if let Some(ref v) = attrs.description { std::fs::write(format!("{}:can.description", base), v)?; } if let Some(ref v) = attrs.human_filename { std::fs::write(format!("{}:can.human_filename", base), v)?; } if let Some(ref v) = attrs.human_path { std::fs::write(format!("{}:can.human_path", base), v)?; } Ok(()) } #[cfg(windows)] fn read_ntfs_ads(path: &Path) -> std::io::Result { let base = path.to_string_lossy(); let read_stream = |name: &str| -> Option { std::fs::read_to_string(format!("{}:{}", base, name)).ok() }; Ok(FileAttributes { mime_type: read_stream("can.mime_type"), application: read_stream("can.application"), user: read_stream("can.user"), tags: read_stream("can.tags"), description: read_stream("can.description"), human_filename: read_stream("can.human_filename"), human_path: read_stream("can.human_path"), }) } #[cfg(test)] mod tests { use super::*; use tempfile::NamedTempFile; #[test] fn test_write_and_read_attributes() { let file = NamedTempFile::new().unwrap(); std::fs::write(file.path(), b"test content").unwrap(); let attrs = FileAttributes { mime_type: Some("image/jpeg".to_string()), application: Some("TestApp".to_string()), user: Some("jason".to_string()), tags: Some("photo,vacation,2024".to_string()), description: Some("A test file".to_string()), human_filename: Some("my_photo.jpg".to_string()), human_path: Some("/photos/trip/".to_string()), }; write_attributes(file.path(), &attrs).unwrap(); let read_back = read_attributes(file.path()).unwrap(); assert_eq!(read_back.mime_type, Some("image/jpeg".to_string())); assert_eq!(read_back.application, Some("TestApp".to_string())); assert_eq!(read_back.user, Some("jason".to_string())); assert_eq!(read_back.tags, Some("photo,vacation,2024".to_string())); assert_eq!(read_back.description, Some("A test file".to_string())); assert_eq!(read_back.human_filename, Some("my_photo.jpg".to_string())); assert_eq!(read_back.human_path, Some("/photos/trip/".to_string())); } #[test] fn test_partial_attributes() { let file = NamedTempFile::new().unwrap(); std::fs::write(file.path(), b"data").unwrap(); let attrs = FileAttributes { mime_type: None, application: Some("App".to_string()), user: None, tags: None, description: None, human_filename: None, human_path: None, }; write_attributes(file.path(), &attrs).unwrap(); let read_back = read_attributes(file.path()).unwrap(); assert_eq!(read_back.application, Some("App".to_string())); assert_eq!(read_back.user, None); assert_eq!(read_back.tags, None); } }