diff --git a/Cargo.lock b/Cargo.lock index efa0c8f..5fcc754 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -228,6 +228,18 @@ dependencies = [ "zerocopy", ] +[[package]] +name = "getrandom" +version = "0.3.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "899def5c37c4fd7b2664648c28120ecec138e4d395b459e5ca34f9cce2dd77fd" +dependencies = [ + "cfg-if", + "libc", + "r-efi", + "wasip2", +] + [[package]] name = "hashbrown" version = "0.17.1" @@ -361,6 +373,7 @@ dependencies = [ "notify", "symphonia", "time", + "twox-hash", ] [[package]] @@ -509,6 +522,15 @@ version = "0.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "439ee305def115ba05938db6eb1644ff94165c5ab5e9420d1c1bcedbba909391" +[[package]] +name = "ppv-lite86" +version = "0.2.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "85eae3c4ed2f50dcfe72643da4befc30deadb458a9b590d720cde2f2b1e97da9" +dependencies = [ + "zerocopy", +] + [[package]] name = "proc-macro-crate" version = "3.5.0" @@ -536,6 +558,41 @@ dependencies = [ "proc-macro2", ] +[[package]] +name = "r-efi" +version = "5.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "69cdb34c158ceb288df11e18b4bd39de994f6657d83847bdffdbd7f346754b0f" + +[[package]] +name = "rand" +version = "0.9.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "44c5af06bb1b7d3216d91932aed5265164bf384dc89cd6ba05cf59a35f5f76ea" +dependencies = [ + "rand_chacha", + "rand_core", +] + +[[package]] +name = "rand_chacha" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d3022b5f1df60f26e1ffddd6c66e8aa15de382ae63b3a0c1bfc0e4d3e3f325cb" +dependencies = [ + "ppv-lite86", + "rand_core", +] + +[[package]] +name = "rand_core" +version = "0.9.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "76afc826de14238e6e8c374ddcc1fa19e374fd8dd986b0d2af0d02377261d83c" +dependencies = [ + "getrandom", +] + [[package]] name = "redox_syscall" version = "0.5.18" @@ -811,6 +868,15 @@ dependencies = [ "winnow", ] +[[package]] +name = "twox-hash" +version = "2.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9ea3136b675547379c4bd395ca6b938e5ad3c3d20fad76e7fe85f9e0d011419c" +dependencies = [ + "rand", +] + [[package]] name = "unicode-ident" version = "1.0.24" @@ -839,6 +905,15 @@ version = "0.11.1+wasi-snapshot-preview1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ccf3ec651a847eb01de73ccad15eb7d99f80485de043efb2f370cd654f4ea44b" +[[package]] +name = "wasip2" +version = "1.0.4+wasi-0.2.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b67efb37e106e55ce722a510d6b5f9c17f083e5fc79afc2badeb12cc313d9487" +dependencies = [ + "wit-bindgen", +] + [[package]] name = "winapi" version = "0.3.9" @@ -968,6 +1043,12 @@ dependencies = [ "memchr", ] +[[package]] +name = "wit-bindgen" +version = "0.57.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1ebf944e87a7c253233ad6766e082e3cd714b5d03812acc24c318f549614536e" + [[package]] name = "zerocopy" version = "0.8.52" diff --git a/Cargo.toml b/Cargo.toml index b0bcdc5..ba776cc 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -14,3 +14,4 @@ time = "0.3.49" symphonia = { version = "0.5", default-features = false, features = [ "aac", "alac", "flac", "mp3", "ogg", "vorbis", "wav" ] } +twox-hash = "2.1.2" diff --git a/devenv.lock b/devenv.lock index 5e2b2ed..ae1e973 100644 --- a/devenv.lock +++ b/devenv.lock @@ -55,11 +55,11 @@ "devenv": { "locked": { "dir": "src/modules", - "lastModified": 1781800860, - "narHash": "sha256-LrEo0eC5ckMvjpBRCuk5q5/vjItKlxnb4n/clHNRZlk=", + "lastModified": 1782331842, + "narHash": "sha256-7CJ2EqNVPMq0ly39aaP6dGgdO627MqUtM/+Dm+QwNdU=", "owner": "cachix", "repo": "devenv", - "rev": "d59d872d80876d9eeb3e214d3b088bc4a14a9c4f", + "rev": "885e1c9d62cfa12232802de77b36aaded1ca609b", "type": "github" }, "original": { @@ -292,11 +292,11 @@ "nixpkgs-src": { "flake": false, "locked": { - "lastModified": 1781454065, - "narHash": "sha256-d2xfDjnfRuf/xYGdu9VVRHiav/2w5hDL/5cw2TuVAXw=", + "lastModified": 1781607440, + "narHash": "sha256-rxO+uc/KFbSJp+pgyXRuAX6QlG9hJdnt0BXpEQRXY+U=", "owner": "NixOS", "repo": "nixpkgs", - "rev": "9eac87a12312b8f60dd52e1c6e1a265f6fc7f5fc", + "rev": "3e41b24abd260e8f71dbe2f5737d24122f972158", "type": "github" }, "original": { @@ -311,11 +311,11 @@ "nixpkgs-src": "nixpkgs-src" }, "locked": { - "lastModified": 1781620901, - "narHash": "sha256-UF6scQlG+6lRkZBUpn/3KNavhOo5G8kDWhjVHcno8uc=", + "lastModified": 1782132010, + "narHash": "sha256-ZnAVHdVrotp80iIMm5CSR1fdxPlw7Uwmwxb+O/wsgZ8=", "owner": "cachix", "repo": "devenv-nixpkgs", - "rev": "2df109b343d3c68efd752e32a444a1d9b9f89afa", + "rev": "12866ae2dddbc0ab8b329915f8072bb9c75bde89", "type": "github" }, "original": { diff --git a/devenv.nix b/devenv.nix index 47fd17b..86df0c3 100644 --- a/devenv.nix +++ b/devenv.nix @@ -28,8 +28,6 @@ git just - ffprobe - opencode ]; diff --git a/src/local.rs b/src/local.rs index 7eba964..bc92ffc 100644 --- a/src/local.rs +++ b/src/local.rs @@ -1,7 +1,12 @@ use std::{ collections::BTreeMap, - fs, io, - os::unix::fs::{MetadataExt, PermissionsExt}, + fs, + hash::{Hash, Hasher}, + io, + os::unix::{ + ffi::OsStrExt, + fs::{MetadataExt, PermissionsExt}, + }, path::{Path, PathBuf}, sync::{Arc, Mutex, mpsc}, thread, @@ -11,6 +16,7 @@ use std::{ use clap::builder::OsStr; use fuser::{Errno, FileAttr, Filesystem, Generation, INodeNo, Request}; use notify::{Event, EventKind, RecursiveMode, Watcher}; +use twox_hash::XxHash64; use crate::file_watcher::FileWatcher; use crate::music_metadata::MusicMetadata; @@ -32,6 +38,37 @@ struct LocalItem { music_metadata: Option, } +impl LocalItem { + fn hash(&self) -> u64 { + let seed = 1234; + let mut hasher = XxHash64::with_seed(seed); + + let metadata = &self.metadata; + let mtime: u64 = metadata + .modified() + .unwrap_or(SystemTime::UNIX_EPOCH) + .duration_since(SystemTime::UNIX_EPOCH) + .map(|d| d.as_secs()) + .unwrap_or(0); + let crtime: u64 = metadata + .created() + .unwrap_or(SystemTime::UNIX_EPOCH) + .duration_since(SystemTime::UNIX_EPOCH) + .map(|d| d.as_secs()) + .unwrap_or(0); + + hasher.write_u64(self.inode.0); + hasher.write(self.name.as_bytes()); + hasher.write(self.original_path.as_os_str().as_bytes()); + hasher.write(self.local_path.as_os_str().as_bytes()); + hasher.write_u64(metadata.ctime() as u64); + hasher.write_u64(mtime); + hasher.write_u64(crtime); + + return hasher.finish(); + } +} + #[derive(Debug)] pub struct LocalOrigin { source: PathBuf, @@ -43,32 +80,15 @@ impl LocalOrigin { pub fn new(source: String, destination: String) -> Result { println!("Initializing the LocalOrigin"); - let map: Arc>> = Arc::new(Mutex::new(BTreeMap::new())); + let snapshot = LocalOrigin::build_snapshot(Path::new(&source), Path::new(&destination))?; - let local_root = LocalItem { - inode: INodeNo::ROOT, - name: "/".to_string(), - original_path: PathBuf::from(&source), - local_path: PathBuf::from(&destination), - file_type: FileType::Directory, - metadata: fs::metadata(&source).unwrap(), - music_metadata: Option::None, + let local_origin = LocalOrigin { + source: source.into(), + destination: destination.into(), + files: Arc::new(Mutex::new(snapshot)), }; - map.lock().unwrap().insert(INodeNo::ROOT, local_root); - - match LocalOrigin::read_source(Path::new(&source), Path::new(&destination), &map) { - Ok(_) => { - let local_origin = LocalOrigin { - source: source.into(), - destination: destination.into(), - files: map, - }; - dbg!(&local_origin); - - return Ok(local_origin); - } - Err(err) => return Err(err), - } + dbg!(&local_origin); + Ok(local_origin) } fn fill_fileset( @@ -76,84 +96,90 @@ impl LocalOrigin { source: &Path, destination: &Path, ) { + match LocalOrigin::build_snapshot(source, destination) { + Ok(new_snapshot) => { + let mut files = map.lock().unwrap(); + + files.retain(|ino, _| new_snapshot.contains_key(ino)); + + for (ino, new_item) in new_snapshot { + match files.get(&ino) { + Some(existing) if existing.hash() == new_item.hash() => {} + _ => { + files.insert(ino, new_item); + } + } + } + } + Err(e) => eprintln!("Error while reading source: {e}"), + } + } + + fn build_snapshot( + source: &Path, + destination: &Path, + ) -> Result, io::Error> { + let mut map = BTreeMap::new(); + let local_root = LocalItem { inode: INodeNo::ROOT, name: "/".to_string(), original_path: source.to_path_buf(), local_path: destination.to_path_buf(), file_type: FileType::Directory, - metadata: fs::metadata(&source).unwrap(), - music_metadata: Option::None, + metadata: fs::metadata(source)?, + music_metadata: None, }; - map.lock().unwrap().insert(INodeNo::ROOT, local_root); + map.insert(INodeNo::ROOT, local_root); - let _ = LocalOrigin::read_source(&source, &destination, &map) - .inspect_err(|e| eprintln!("Error while reading source: {e}")); + LocalOrigin::read_into_map(source, destination, &mut map)?; + Ok(map) } - fn read_source( + fn read_into_map( source: &Path, destination: &Path, - map: &Arc>>, + map: &mut BTreeMap, ) -> Result<(), io::Error> { - match fs::read_dir(source) { - Ok(dir) => { - for item in dir { - match item { - Ok(entry) => { - let name = entry.file_name().to_string_lossy().into_owned(); - let item_path: PathBuf = entry.path(); - let metadata: fs::Metadata = entry.metadata().unwrap(); - let file_type = if entry.file_type().unwrap().is_dir() { - FileType::Directory - } else { - FileType::File - }; + for item in fs::read_dir(source)? { + let entry = item?; + let name = entry.file_name().to_string_lossy().into_owned(); + let item_path = entry.path(); + let metadata = entry.metadata()?; + let file_type = if entry.file_type()?.is_dir() { + FileType::Directory + } else { + FileType::File + }; - let mut local_path: PathBuf = PathBuf::new(); - // local_path.push(destination); - local_path.push(name.clone()); + let mut local_path = PathBuf::new(); + local_path.push(name.clone()); - let music_metadata: Option = if item_path - .extension() - .and_then(std::ffi::OsStr::to_str) - .unwrap() - == "flac" - { - MusicMetadata::parse_music_metadata(&item_path) - } else { - Option::None - }; + let music_metadata = + if item_path.extension().and_then(std::ffi::OsStr::to_str) == Some("flac") { + MusicMetadata::parse_music_metadata(&item_path) + } else { + None + }; - let local_item = LocalItem { - inode: INodeNo(metadata.ino()), - name: name, - original_path: item_path.clone(), - local_path: local_path, - file_type: file_type, - metadata: metadata, - music_metadata: music_metadata, - }; - let inode = local_item.inode; + let inode = INodeNo(metadata.ino()); + let local_item = LocalItem { + inode, + name, + original_path: item_path.clone(), + local_path, + file_type, + metadata, + music_metadata, + }; - map.lock().unwrap().insert(inode, local_item); + map.insert(inode, local_item); - if file_type == FileType::Directory { - match LocalOrigin::read_source(&item_path, destination, map) { - Ok(_) => {} - Err(err) => return Err(err), - }; - } - } - - Err(err) => return Err(err), - } - } + if file_type == FileType::Directory { + LocalOrigin::read_into_map(&item_path, destination, map)?; } - Err(err) => return Err(err), - }; - - return Ok(()); + } + Ok(()) } fn local_file_to_file_attr(local_file: &LocalItem) -> FileAttr { @@ -243,12 +269,13 @@ impl Filesystem for LocalOrigin { .lock() .unwrap() .iter() + .filter(|(key, _)| **key != INodeNo::ROOT) .skip(offset as usize) .enumerate() { let offset: u64 = 2 + i as u64; let file_type: fuser::FileType = LocalOrigin::file_type(value.file_type); - let path = &value.local_path; + let path = &value.name; let buffer_full: bool = reply.add(*key, offset, file_type, path);