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>
188 lines
5.9 KiB
Rust
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);
|
|
}
|
|
}
|