CanMan/src/xattr.rs
Jason Tudisco 360ecbdad0 Initial commit: CAN Service + examples (can-sync v1, canfs, filemanager, paste)
CAN Service: content-addressable storage with HTTP API, SQLite metadata,
file-based blob storage, thumbnail generation, and integrity verification.

can-sync v1: P2P sync sidecar using iroh-docs for encrypted peer-to-peer
replication with library/filter-based selective sync. Fully builds but
being superseded by v2 (simplified full-mirror approach).

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-12 10:32:04 -06:00

188 lines
5.9 KiB
Rust

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<FileAttributes> {
#[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<FileAttributes> {
use xattr::FileExt;
let file = std::fs::File::open(path)?;
let read_attr = |name: &str| -> Option<String> {
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<FileAttributes> {
let base = path.to_string_lossy();
let read_stream = |name: &str| -> Option<String> {
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);
}
}