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

317 lines
10 KiB
Rust

use std::os::raw::c_void;
use std::sync::Arc;
use parking_lot::{Mutex, RwLock};
use tracing::{debug, warn};
use widestring::U16CStr;
use winfsp::filesystem::{
DirBuffer, DirInfo, DirMarker, FileInfo, FileSecurity, FileSystemContext, OpenFileInfo,
VolumeInfo, WideNameInfo,
};
use winfsp::FspError;
use crate::api::{AssetMeta, CanClient};
use crate::tree::{NodeId, NodeKind, VirtualTree};
use crate::util;
// NTSTATUS constants (raw i32 values to avoid windows crate version conflicts)
const STATUS_OBJECT_NAME_NOT_FOUND: i32 = 0xC0000034_u32 as i32;
const STATUS_NOT_A_DIRECTORY: i32 = 0xC0000103_u32 as i32;
const STATUS_UNEXPECTED_NETWORK_ERROR: i32 = 0xC00000C4_u32 as i32;
const STATUS_INVALID_DEVICE_REQUEST: i32 = 0xC0000010_u32 as i32;
// File attribute constants
const FILE_ATTRIBUTE_DIRECTORY: u32 = 0x10;
const FILE_ATTRIBUTE_READONLY: u32 = 0x01;
const FILE_ATTRIBUTE_ARCHIVE: u32 = 0x20;
fn ntstatus(code: i32) -> FspError {
FspError::NTSTATUS(code)
}
/// Shared cache state: asset list + virtual tree.
pub struct CacheState {
pub assets: Vec<AssetMeta>,
pub tree: VirtualTree,
}
/// The WinFSP filesystem context for CAN service.
pub struct CanFs {
pub cache: Arc<RwLock<CacheState>>,
pub client: Arc<CanClient>,
}
/// Per-open-handle context.
pub struct CanFileContext {
node_id: NodeId,
/// Lazily fetched file bytes.
content: Mutex<Option<Vec<u8>>>,
/// Directory enumeration buffer.
dir_buffer: DirBuffer,
}
impl FileSystemContext for CanFs {
type FileContext = CanFileContext;
fn get_security_by_name(
&self,
file_name: &U16CStr,
_security_descriptor: Option<&mut [c_void]>,
_resolve_reparse_points: impl FnOnce(&U16CStr) -> Option<FileSecurity>,
) -> winfsp::Result<FileSecurity> {
let path = util::normalize_path(file_name);
debug!("get_security_by_name: {}", path);
let cache = self.cache.read();
let node_id = cache
.tree
.lookup(&path)
.ok_or(ntstatus(STATUS_OBJECT_NAME_NOT_FOUND))?;
let node = cache.tree.get(node_id);
let attributes = if node.is_directory() {
FILE_ATTRIBUTE_DIRECTORY
} else {
FILE_ATTRIBUTE_READONLY | FILE_ATTRIBUTE_ARCHIVE
};
Ok(FileSecurity {
reparse: false,
sz_security_descriptor: 0,
attributes,
})
}
fn open(
&self,
file_name: &U16CStr,
_create_options: u32,
_granted_access: u32,
file_info: &mut OpenFileInfo,
) -> winfsp::Result<Self::FileContext> {
let path = util::normalize_path(file_name);
debug!("open: {}", path);
let cache = self.cache.read();
let node_id = cache
.tree
.lookup(&path)
.ok_or(ntstatus(STATUS_OBJECT_NAME_NOT_FOUND))?;
let node = cache.tree.get(node_id);
let fi = file_info.as_mut();
if node.is_directory() {
fi.file_attributes = FILE_ATTRIBUTE_DIRECTORY;
fi.file_size = 0;
fi.allocation_size = 0;
} else {
fi.file_attributes = FILE_ATTRIBUTE_READONLY | FILE_ATTRIBUTE_ARCHIVE;
if let NodeKind::File { asset_index } = &node.kind {
let sz = cache.assets[*asset_index].size as u64;
fi.file_size = sz;
fi.allocation_size = sz;
} else {
fi.file_size = 0;
fi.allocation_size = 0;
}
}
if let NodeKind::File { asset_index } = &node.kind {
let ts = util::epoch_ms_to_filetime(cache.assets[*asset_index].timestamp);
fi.creation_time = ts;
fi.last_access_time = ts;
fi.last_write_time = ts;
fi.change_time = ts;
} else {
fi.creation_time = 0;
fi.last_access_time = 0;
fi.last_write_time = 0;
fi.change_time = 0;
}
fi.index_number = 0;
fi.hard_links = 0;
fi.ea_size = 0;
fi.reparse_tag = 0;
Ok(CanFileContext {
node_id,
content: Mutex::new(None),
dir_buffer: DirBuffer::new(),
})
}
fn close(&self, _context: Self::FileContext) {}
fn get_file_info(
&self,
context: &Self::FileContext,
file_info: &mut FileInfo,
) -> winfsp::Result<()> {
let cache = self.cache.read();
let node = cache.tree.get(context.node_id);
if node.is_directory() {
file_info.file_attributes = FILE_ATTRIBUTE_DIRECTORY;
file_info.file_size = 0;
file_info.allocation_size = 0;
file_info.creation_time = 0;
file_info.last_access_time = 0;
file_info.last_write_time = 0;
file_info.change_time = 0;
} else {
file_info.file_attributes = FILE_ATTRIBUTE_READONLY | FILE_ATTRIBUTE_ARCHIVE;
// Use actual downloaded size if available, otherwise metadata size
let content = context.content.lock();
if let Some(ref bytes) = *content {
let sz = bytes.len() as u64;
file_info.file_size = sz;
file_info.allocation_size = sz;
} else if let NodeKind::File { asset_index } = &node.kind {
let sz = cache.assets[*asset_index].size as u64;
file_info.file_size = sz;
file_info.allocation_size = sz;
} else {
file_info.file_size = 0;
file_info.allocation_size = 0;
}
if let NodeKind::File { asset_index } = &node.kind {
let ts = util::epoch_ms_to_filetime(cache.assets[*asset_index].timestamp);
file_info.creation_time = ts;
file_info.last_access_time = ts;
file_info.last_write_time = ts;
file_info.change_time = ts;
}
}
file_info.index_number = 0;
file_info.hard_links = 0;
file_info.ea_size = 0;
file_info.reparse_tag = 0;
Ok(())
}
fn read(
&self,
context: &Self::FileContext,
buffer: &mut [u8],
offset: u64,
) -> winfsp::Result<u32> {
let mut content = context.content.lock();
if content.is_none() {
let cache = self.cache.read();
let node = cache.tree.get(context.node_id);
if let NodeKind::File { asset_index } = &node.kind {
let hash = &cache.assets[*asset_index].hash;
debug!("fetching bytes for {}", hash);
match self.client.fetch_bytes(hash) {
Ok(bytes) => {
*content = Some(bytes);
}
Err(e) => {
warn!("failed to fetch asset: {}", e);
return Err(ntstatus(STATUS_UNEXPECTED_NETWORK_ERROR));
}
}
} else {
return Err(ntstatus(STATUS_INVALID_DEVICE_REQUEST));
}
}
let bytes = content.as_ref().unwrap();
let offset = offset as usize;
if offset >= bytes.len() {
return Ok(0);
}
let end = (offset + buffer.len()).min(bytes.len());
let count = end - offset;
buffer[..count].copy_from_slice(&bytes[offset..end]);
Ok(count as u32)
}
fn read_directory(
&self,
context: &Self::FileContext,
_pattern: Option<&U16CStr>,
marker: DirMarker,
buffer: &mut [u8],
) -> winfsp::Result<u32> {
let cache = self.cache.read();
let node = cache.tree.get(context.node_id);
if !node.is_directory() {
return Err(ntstatus(STATUS_NOT_A_DIRECTORY));
}
if let Ok(dir_buffer_lock) = context.dir_buffer.acquire(marker.is_none(), None) {
// "." entry
{
let mut di: DirInfo = DirInfo::new();
let _ = di.set_name(std::ffi::OsStr::new("."));
di.file_info_mut().file_attributes = FILE_ATTRIBUTE_DIRECTORY;
let _ = dir_buffer_lock.write(&mut di);
}
// ".." entry
{
let mut di: DirInfo = DirInfo::new();
let _ = di.set_name(std::ffi::OsStr::new(".."));
di.file_info_mut().file_attributes = FILE_ATTRIBUTE_DIRECTORY;
let _ = dir_buffer_lock.write(&mut di);
}
for &child_id in &node.children {
let child = cache.tree.get(child_id);
let mut di: DirInfo = DirInfo::new();
if di.set_name(std::ffi::OsStr::new(&child.name)).is_err() {
continue;
}
let fi = di.file_info_mut();
if child.is_directory() {
fi.file_attributes = FILE_ATTRIBUTE_DIRECTORY;
fi.file_size = 0;
fi.allocation_size = 0;
} else {
fi.file_attributes = FILE_ATTRIBUTE_READONLY | FILE_ATTRIBUTE_ARCHIVE;
if let NodeKind::File { asset_index } = &child.kind {
let sz = cache.assets[*asset_index].size as u64;
fi.file_size = sz;
fi.allocation_size = sz;
} else {
fi.file_size = 0;
fi.allocation_size = 0;
}
}
if let NodeKind::File { asset_index } = &child.kind {
let ts = util::epoch_ms_to_filetime(cache.assets[*asset_index].timestamp);
fi.creation_time = ts;
fi.last_access_time = ts;
fi.last_write_time = ts;
fi.change_time = ts;
}
fi.index_number = 0;
fi.hard_links = 0;
fi.ea_size = 0;
fi.reparse_tag = 0;
let _ = dir_buffer_lock.write(&mut di);
}
}
Ok(context.dir_buffer.read(marker, buffer))
}
fn get_volume_info(&self, out_volume_info: &mut VolumeInfo) -> winfsp::Result<()> {
out_volume_info.total_size = 1024 * 1024 * 1024; // 1 GB
out_volume_info.free_size = 0;
Ok(())
}
}