use std::{ hash::Hasher, path::PathBuf, time::{SystemTime, UNIX_EPOCH}, }; use std::os::unix::ffi::OsStrExt; use fuser::INodeNo; use twox_hash::XxHash64; use crate::music::metadata::MusicMetadata; use crate::origins::attrs::FileAttrs; #[derive(PartialEq, Eq, Copy, Clone, Debug)] pub enum FileType { Directory, File, } #[derive(Debug, Clone)] pub struct Item { pub inode: INodeNo, pub parent_inode: INodeNo, pub name: String, pub original_path: PathBuf, pub local_path: PathBuf, pub file_type: FileType, pub attrs: FileAttrs, pub music_metadata: Option, pub hash: u64, } fn secs_since_epoch(time: SystemTime) -> u64 { return time .duration_since(UNIX_EPOCH) .map(|d| d.as_secs()) .unwrap_or(0); } /// Hash inputs shared by every Item regardless of origin. Server manifests /// and client Items both feed these same five values through xxhash, so a /// file's hash is identical on both sides — that's what makes `Reconcile`'s /// diff correct. pub fn compute_item_hash( inode: u64, original_path: &[u8], ctime_secs: u64, mtime_secs: u64, crtime_secs: u64, ) -> u64 { let seed = 1234; let mut hasher = XxHash64::with_seed(seed); hasher.write_u64(inode); hasher.write(original_path); hasher.write_u64(ctime_secs); hasher.write_u64(mtime_secs); hasher.write_u64(crtime_secs); return hasher.finish(); } impl Item { #[allow(clippy::too_many_arguments)] pub fn new( inode: INodeNo, parent_inode: INodeNo, name: String, original_path: PathBuf, local_path: PathBuf, file_type: FileType, attrs: FileAttrs, music_metadata: Option, ) -> Item { let mut item = Item { inode, parent_inode, name, original_path, local_path, file_type, attrs, music_metadata, hash: 0, }; item.hash = item.compute_hash(); return item; } pub fn compute_hash(&self) -> u64 { return compute_item_hash( self.inode.0, self.original_path.as_os_str().as_bytes(), secs_since_epoch(self.attrs.ctime), secs_since_epoch(self.attrs.mtime), secs_since_epoch(self.attrs.crtime), ); } } #[cfg(test)] mod tests { use super::*; use crate::origins::attrs::FileAttrs; #[test] fn compute_hash_deterministic() { let attrs = file_attrs_for_tempdir(); let item1 = Item::new( INodeNo(42), INodeNo::ROOT, "test".to_string(), PathBuf::from("/some/path"), PathBuf::from("test"), FileType::File, attrs.clone(), None, ); let item2 = Item::new( INodeNo(42), INodeNo::ROOT, "test".to_string(), PathBuf::from("/some/path"), PathBuf::from("test"), FileType::File, attrs, None, ); assert_eq!(item1.hash, item2.hash); } #[test] fn compute_hash_changes_on_path_change() { let attrs = file_attrs_for_tempdir(); let item1 = Item::new( INodeNo(42), INodeNo::ROOT, "test".to_string(), PathBuf::from("/some/path"), PathBuf::from("test"), FileType::File, attrs.clone(), None, ); let item2 = Item::new( INodeNo(42), INodeNo::ROOT, "test".to_string(), PathBuf::from("/different/path"), PathBuf::from("test"), FileType::File, attrs, None, ); assert_ne!(item1.hash, item2.hash); } fn file_attrs_for_tempdir() -> FileAttrs { let tmp = tempfile::tempdir().unwrap(); let metadata = std::fs::metadata(tmp.path()).unwrap(); return FileAttrs::from(&metadata); } }