use std::{ collections::BTreeMap, fs, hash::Hasher, io::{self, Read, Seek, SeekFrom}, os::unix::{ ffi::OsStrExt, fs::{MetadataExt, PermissionsExt}, }, path::{Path, PathBuf}, sync::{Arc, Mutex, mpsc}, thread, time::{Duration, SystemTime}, }; use fuser::{Errno, FileAttr, Filesystem, Generation, INodeNo, Request}; use notify::{Event, EventKind, RecursiveMode, Watcher}; use sea_orm::entity::prelude::*; use twox_hash::XxHash64; use crate::file_watcher::FileWatcher; use crate::music_metadata::MusicMetadata; use crate::music_metadata::db::{ artists, music_metadata as music_metadata_entity, other_tags, pictures, }; #[derive(PartialEq, Eq, Copy, Clone, Debug)] enum FileType { Directory, File, } #[derive(Debug)] struct LocalItem { inode: INodeNo, parent_inode: INodeNo, name: String, original_path: PathBuf, local_path: PathBuf, file_type: FileType, metadata: fs::Metadata, music_metadata: Option, hash: u64, } impl LocalItem { fn compute_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.original_path.as_os_str().as_bytes()); hasher.write_u64(metadata.ctime() as u64); hasher.write_u64(mtime); hasher.write_u64(crtime); return hasher.finish(); } } pub struct LocalOrigin { source: PathBuf, destination: PathBuf, files: Arc>>, client: sea_orm::DatabaseConnection, } impl std::fmt::Debug for LocalOrigin { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { f.debug_struct("LocalOrigin") .field("source", &self.source) .field("destination", &self.destination) .field("files", &self.files) .finish() } } impl LocalOrigin { pub async fn new( source: String, destination: String, client: sea_orm::DatabaseConnection, ) -> Result { println!("Initializing the LocalOrigin"); let mut snapshot = LocalOrigin::build_snapshot(Path::new(&source), Path::new(&destination))?; let db_items: std::collections::HashMap = Entity::find() .all(&client) .await .unwrap() .into_iter() .map(|e| (e.inode, e)) .collect(); let mut to_insert: Vec = vec![]; let mut to_update: Vec = vec![]; let mut to_delete: Vec = vec![]; let mut to_save_music: Vec = vec![]; for (ino, item) in &snapshot { let ino_i64 = ino.0 as i64; match db_items.get(&ino_i64) { None => { to_insert.push(ActiveModel::from(item)); if item.music_metadata.is_some() { to_save_music.push(ino_i64); } } Some(db_item) if db_item.hash != item.hash as i64 => { to_update.push(ActiveModel::from(item)); if item.music_metadata.is_some() { to_save_music.push(ino_i64); } } _ => {} } } let fresh_inodes: std::collections::HashSet = snapshot.keys().map(|i| i.0 as i64).collect(); for ino in db_items.keys().filter(|i| !fresh_inodes.contains(i)) { to_delete.push(*ino); } if !to_insert.is_empty() { Entity::insert_many(to_insert).exec(&client).await.unwrap(); } for model in to_update { model.update(&client).await.unwrap(); } for ino in to_delete { Entity::delete_by_id(ino).exec(&client).await.unwrap(); } for ino_i64 in &to_save_music { let ino = INodeNo(*ino_i64 as u64); if let Some(music_metadata) = snapshot .get(&ino) .and_then(|item| item.music_metadata.as_ref()) { LocalOrigin::save_music_metadata(*ino_i64, music_metadata, &client) .await .unwrap(); } } // For unchanged FLAC items, restore music_metadata from DB // (overrides the fresh FLAC parse so that writes made in a previous session survive restarts) let unchanged_music_inodes: Vec = db_items .values() .filter_map(|db_item| { let ino = INodeNo(db_item.inode as u64); snapshot .get(&ino) .filter(|item| { item.hash as i64 == db_item.hash && item.music_metadata.is_some() }) .map(|_| db_item.inode) }) .collect(); if !unchanged_music_inodes.is_empty() { use artists::Column as ArtCol; use music_metadata_entity::Column as MmCol; use other_tags::Column as OtCol; use pictures::Column as PicCol; let mm_rows: std::collections::HashMap = music_metadata_entity::Entity::find() .filter(MmCol::Inode.is_in(unchanged_music_inodes.clone())) .all(&client) .await .unwrap() .into_iter() .map(|m| (m.inode, m)) .collect(); let mut artists_by_inode: std::collections::HashMap> = std::collections::HashMap::new(); for row in artists::Entity::find() .filter(ArtCol::Inode.is_in(unchanged_music_inodes.clone())) .all(&client) .await .unwrap() { artists_by_inode .entry(row.inode) .or_default() .push(row.artist); } let mut other_tags_by_inode: std::collections::HashMap> = std::collections::HashMap::new(); for row in other_tags::Entity::find() .filter(OtCol::Inode.is_in(unchanged_music_inodes.clone())) .all(&client) .await .unwrap() { other_tags_by_inode .entry(row.inode) .or_default() .push((row.position, row.tag)); } let mut pictures_by_inode: std::collections::HashMap> = std::collections::HashMap::new(); for row in pictures::Entity::find() .filter(PicCol::Inode.is_in(unchanged_music_inodes)) .all(&client) .await .unwrap() { pictures_by_inode.entry(row.inode).or_default().push(row); } for (inode, mm_row) in mm_rows { let ino = INodeNo(inode as u64); let Some(item) = snapshot.get_mut(&ino) else { continue; }; let mut sorted_tags = other_tags_by_inode.remove(&inode).unwrap_or_default(); sorted_tags.sort_by_key(|(pos, _)| *pos); let mut sorted_pics = pictures_by_inode.remove(&inode).unwrap_or_default(); sorted_pics.sort_by_key(|p| p.position); let picture_block_headers: Vec<[u8; 4]> = sorted_pics .iter() .map(|p| { let mut hdr = [0u8; 4]; let len = p.block_header.len().min(4); hdr[..len].copy_from_slice(&p.block_header[..len]); hdr }) .collect(); let picture_data_ranges: Vec<(u64, u64)> = sorted_pics .iter() .map(|p| (p.data_offset as u64, p.data_length as u64)) .collect(); let mut music_metadata = MusicMetadata { artist: artists_by_inode.remove(&inode).unwrap_or_default(), album: mm_row.album, track_number: mm_row.track_number, track_title: mm_row.track_title, other_tags: sorted_tags.into_iter().map(|(_, tag)| tag).collect(), header: mm_row.header, picture_block_headers, picture_data_ranges, real_audio_start: mm_row.real_audio_start as u64, vorbis_comment_offset: 0, vorbis_comment_length: 0, }; music_metadata.find_vorbis_offsets(); item.music_metadata = Some(music_metadata); } } // For unchanged items (hash matches), restore virtual paths from DB // so that renames performed in a previous session are preserved. let restorations: Vec<(INodeNo, String, PathBuf)> = db_items .values() .filter_map(|db_item| { let ino = INodeNo(db_item.inode as u64); snapshot .get(&ino) .filter(|item| item.hash as i64 == db_item.hash) .map(|_| { ( ino, db_item.name.clone(), PathBuf::from(&db_item.local_path), ) }) }) .collect(); for (_, _, local_path) in &restorations { LocalOrigin::ensure_virtual_dirs(local_path, Path::new(&source), &mut snapshot); } for (ino, name, local_path) in restorations { if let Some(item) = snapshot.get_mut(&ino) { item.name = name; item.parent_inode = LocalOrigin::parent_inode_from_path(&local_path); item.local_path = local_path; } } let local_origin = LocalOrigin { source: source.into(), destination: destination.into(), files: Arc::new(Mutex::new(snapshot)), client, }; //dbg!(&local_origin); Ok(local_origin) } async fn save_music_metadata( inode: i64, music_metadata: &MusicMetadata, client: &sea_orm::DatabaseConnection, ) -> Result<(), sea_orm::DbErr> { use sea_orm::ActiveValue::Set; music_metadata_entity::Entity::delete_by_id(inode) .exec(client) .await?; music_metadata_entity::Entity::insert(music_metadata_entity::ActiveModel { inode: Set(inode), track_title: Set(music_metadata.track_title.clone()), album: Set(music_metadata.album.clone()), track_number: Set(music_metadata.track_number), header: Set(music_metadata.header.clone()), real_audio_start: Set(music_metadata.real_audio_start as i64), }) .exec(client) .await?; if !music_metadata.artist.is_empty() { artists::Entity::insert_many(music_metadata.artist.iter().map(|a| { artists::ActiveModel { inode: Set(inode), artist: Set(a.clone()), } })) .exec(client) .await?; } if !music_metadata.other_tags.is_empty() { other_tags::Entity::insert_many(music_metadata.other_tags.iter().enumerate().map( |(pos, tag)| other_tags::ActiveModel { inode: Set(inode), position: Set(pos as i32), tag: Set(tag.clone()), }, )) .exec(client) .await?; } if !music_metadata.picture_block_headers.is_empty() { pictures::Entity::insert_many( music_metadata .picture_block_headers .iter() .zip(music_metadata.picture_data_ranges.iter()) .enumerate() .map(|(pos, (hdr, (offset, len)))| pictures::ActiveModel { inode: Set(inode), position: Set(pos as i32), block_header: Set(hdr.to_vec()), data_offset: Set(*offset as i64), data_length: Set(*len as i64), }), ) .exec(client) .await?; } Ok(()) } fn fill_fileset( map: &Arc>>, 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, parent_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)?, music_metadata: None, hash: 0, }; let local_root = LocalItem { hash: local_root.compute_hash(), ..local_root }; map.insert(INodeNo::ROOT, local_root); LocalOrigin::read_into_map(source, destination, &mut map)?; Ok(map) } fn read_into_map( source: &Path, destination: &Path, map: &mut BTreeMap, ) -> Result<(), io::Error> { 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 music_metadata = if item_path.extension().and_then(std::ffi::OsStr::to_str) == Some("flac") { MusicMetadata::parse_music_metadata(&item_path) } else { None }; let mut local_path = PathBuf::new(); if music_metadata.is_some() { let music_metadata = music_metadata.clone().unwrap(); local_path.push(music_metadata.artist.join("-")); local_path.push(music_metadata.album); } local_path.push(name.clone()); let inode = INodeNo(metadata.ino()); let parent_inode = LocalOrigin::ensure_virtual_dirs(&local_path, source, map); let local_item = LocalItem { inode, parent_inode, name, original_path: item_path.clone(), local_path, file_type, metadata, music_metadata, hash: 0, }; let local_item = LocalItem { hash: local_item.compute_hash(), ..local_item }; map.insert(inode, local_item); if file_type == FileType::Directory { LocalOrigin::read_into_map(&item_path, destination, map)?; } } Ok(()) } fn local_file_to_file_attr(local_file: &LocalItem) -> FileAttr { let metadata = &local_file.metadata; let ctime = std::time::UNIX_EPOCH + Duration::from_secs(metadata.ctime() as u64); let atime = metadata.accessed().unwrap_or(SystemTime::UNIX_EPOCH); let mtime = metadata.modified().unwrap_or(SystemTime::UNIX_EPOCH); let crtime = metadata.created().unwrap_or(SystemTime::UNIX_EPOCH); let kind = LocalOrigin::file_type(local_file.file_type); let size = match &local_file.music_metadata { Some(mm) if mm.real_audio_start > 0 => mm.virtual_size(metadata.size()), _ => metadata.size(), }; return FileAttr { ino: local_file.inode, size, blocks: metadata.blocks(), atime: atime, mtime: mtime, ctime: ctime, crtime: crtime, kind: kind, perm: metadata.permissions().mode() as u16, nlink: metadata.nlink() as u32, uid: metadata.uid(), gid: metadata.gid(), rdev: metadata.rdev() as u32, blksize: metadata.blksize() as u32, flags: 0, // TODO this is for macos only but perhaps we could get it from metadata still? }; } fn file_type(file_type: FileType) -> fuser::FileType { return match file_type { FileType::Directory => fuser::FileType::Directory, FileType::File => fuser::FileType::RegularFile, }; } fn read_bytes_at(path: &Path, offset: u64, len: usize) -> io::Result> { let mut f = fs::File::open(path)?; f.seek(SeekFrom::Start(offset))?; let mut buf = vec![0u8; len]; let n = f.read(&mut buf)?; buf.truncate(n); Ok(buf) } fn parent_inode_from_path(local_path: &Path) -> INodeNo { let components: Vec<_> = local_path.components().collect(); if components.len() <= 1 { return INodeNo::ROOT; } let mut parent_path = String::new(); for (i, comp) in components[..components.len() - 1].iter().enumerate() { if i > 0 { parent_path.push('/'); } parent_path.push_str(comp.as_os_str().to_str().unwrap_or_default()); } LocalOrigin::virtual_inode(&parent_path) } fn virtual_inode(path: &str) -> INodeNo { let mut hasher = XxHash64::with_seed(5678); hasher.write(path.as_bytes()); INodeNo(hasher.finish() | (1u64 << 63)) } fn ensure_virtual_dirs( local_path: &Path, source: &Path, map: &mut BTreeMap, ) -> INodeNo { let components: Vec<_> = local_path.components().collect(); if components.len() <= 1 { return INodeNo::ROOT; } let mut current_parent = INodeNo::ROOT; let mut current_path = String::new(); for component in &components[..components.len() - 1] { let comp_str = component.as_os_str().to_str().unwrap_or_default(); if !current_path.is_empty() { current_path.push('/'); } current_path.push_str(comp_str); let virt_ino = LocalOrigin::virtual_inode(¤t_path); if !map.contains_key(&virt_ino) { let metadata = fs::metadata(source).unwrap(); let virt_item = LocalItem { inode: virt_ino, parent_inode: current_parent, name: comp_str.to_string(), original_path: source.to_path_buf(), local_path: PathBuf::from(¤t_path), file_type: FileType::Directory, metadata, music_metadata: None, hash: 0, }; let virt_item = LocalItem { hash: virt_item.compute_hash(), ..virt_item }; map.insert(virt_ino, virt_item); } current_parent = virt_ino; } return current_parent; } } impl Filesystem for LocalOrigin { fn open( &self, _req: &Request, ino: INodeNo, _flags: fuser::OpenFlags, reply: fuser::ReplyOpen, ) { if self.files.lock().unwrap().contains_key(&ino) { reply.opened(fuser::FileHandle(ino.0), fuser::FopenFlags::empty()); } else { reply.error(Errno::ENOENT); } } fn setattr( &self, _req: &Request, ino: INodeNo, _mode: Option, _uid: Option, _gid: Option, _size: Option, _atime: Option, _mtime: Option, _ctime: Option, _fh: Option, _crtime: Option, _chgtime: Option, _bkuptime: Option, _flags: Option, reply: fuser::ReplyAttr, ) { match self.files.lock().unwrap().get(&ino) { Some(file) => reply.attr( &Duration::new(1, 0), &LocalOrigin::local_file_to_file_attr(file), ), None => reply.error(Errno::ENOENT), } } fn write( &self, _req: &Request, ino: INodeNo, _fh: fuser::FileHandle, offset: u64, data: &[u8], _write_flags: fuser::WriteFlags, _flags: fuser::OpenFlags, _lock_owner: Option, reply: fuser::ReplyWrite, ) { let written = data.len() as u32; let write_start = offset; let write_end = write_start + data.len() as u64; let updated_music_metadata = { let mut files = self.files.lock().unwrap(); let item = match files.get_mut(&ino) { Some(item) => item, None => { reply.written(written); return; } }; let music_metadata = match &mut item.music_metadata { Some(mm) if mm.vorbis_comment_length > 0 => mm, _ => { reply.written(written); return; } }; let vc_data_offset = music_metadata.vorbis_comment_offset; let vc_hdr_offset = vc_data_offset - 4; // Must cover the 4-byte block header to read the (possibly new) data length if write_start <= vc_hdr_offset && write_end >= vc_data_offset { let hdr_from = (vc_hdr_offset - write_start) as usize; let new_length = u32::from_be_bytes([ 0, data[hdr_from + 1], data[hdr_from + 2], data[hdr_from + 3], ]) as u64; let vc_data_end = vc_data_offset + new_length; if write_end >= vc_data_end { let from = (vc_data_offset - write_start) as usize; let to = (vc_data_end - write_start) as usize; music_metadata.update_from_vorbis_comment_data(&data[from..to]); Some(music_metadata.clone()) } else { None } } else { None } }; if let Some(music_metadata) = updated_music_metadata { let client = self.client.clone(); let ino_i64 = ino.0 as i64; tokio::runtime::Builder::new_current_thread() .enable_io() .enable_time() .build() .unwrap() .block_on(async move { LocalOrigin::save_music_metadata(ino_i64, &music_metadata, &client) .await .unwrap(); }); } reply.written(written); } fn getattr( &self, _req: &fuser::Request, ino: INodeNo, _fh: Option, reply: fuser::ReplyAttr, ) { println!("getattr(ino={})", ino); // TODO fix unwrap match self.files.lock().unwrap().get(&ino) { Some(file) => { let ttl = Duration::new(1, 0); let attr = LocalOrigin::local_file_to_file_attr(file); reply.attr(&ttl, &attr); } None => reply.error(Errno::ENOENT), } } fn readdir( &self, _req: &fuser::Request, ino: INodeNo, fh: fuser::FileHandle, offset: u64, mut reply: fuser::ReplyDirectory, ) { println!("readdir(ino={}, fh={}, offset={})", ino, fh, offset); let files = self.files.lock().unwrap(); let parent_inode = match files.get(&ino) { Some(dir) => dir.parent_inode, None => { reply.error(Errno::ENOENT); return; } }; if offset == 0 { let _ = reply.add(ino, 0, fuser::FileType::Directory, &Path::new(".")); let _ = reply.add( parent_inode, 1, fuser::FileType::Directory, &Path::new(".."), ); } for (i, (key, value)) in files .iter() .filter(|(_, v)| v.parent_inode == ino && v.inode != ino) .skip(offset as usize) .enumerate() { let entry_offset: u64 = 2 + i as u64; let file_type: fuser::FileType = LocalOrigin::file_type(value.file_type); let buffer_full: bool = reply.add(*key, entry_offset, file_type, &value.name); if buffer_full { break; } } reply.ok(); } fn rename( &self, _req: &Request, parent: INodeNo, name: &std::ffi::OsStr, newparent: INodeNo, newname: &std::ffi::OsStr, _flags: fuser::RenameFlags, reply: fuser::ReplyEmpty, ) { let name_str = match name.to_str() { Some(s) => s, None => { reply.error(Errno::EINVAL); return; } }; let newname_str = match newname.to_str() { Some(s) => s, None => { reply.error(Errno::EINVAL); return; } }; let mut files = self.files.lock().unwrap(); let item_ino = match files .iter() .find(|(_, v)| v.parent_inode == parent && v.name == name_str) { Some((ino, _)) => *ino, None => { reply.error(Errno::ENOENT); return; } }; let new_local_path = if newparent == INodeNo::ROOT { PathBuf::from(newname_str) } else { match files.get(&newparent) { Some(dir) => dir.local_path.join(newname_str), None => { reply.error(Errno::ENOENT); return; } } }; let new_parent_inode = LocalOrigin::parent_inode_from_path(&new_local_path); let item = files.get_mut(&item_ino).unwrap(); item.name = newname_str.to_string(); item.local_path = new_local_path.clone(); item.parent_inode = new_parent_inode; let inode_i64 = item_ino.0 as i64; let new_name_owned = newname_str.to_string(); let new_local_path_str = new_local_path.to_string_lossy().into_owned(); let client = self.client.clone(); drop(files); tokio::runtime::Builder::new_current_thread() .enable_time() .enable_io() .build() .unwrap() .block_on(async move { use sea_orm::ActiveValue::Set; ActiveModel { inode: Set(inode_i64), name: Set(new_name_owned), local_path: Set(new_local_path_str), ..Default::default() } .update(&client) .await .unwrap(); }); reply.ok(); } fn read( &self, _req: &Request, ino: INodeNo, _fh: fuser::FileHandle, offset: u64, size: u32, _flags: fuser::OpenFlags, _lock_owner: Option, reply: fuser::ReplyData, ) { let files = self.files.lock().unwrap(); let item = match files.get(&ino) { Some(f) => f, None => { reply.error(Errno::ENOENT); return; } }; let original_path = item.original_path.clone(); let mm = item .music_metadata .as_ref() .filter(|mm| mm.real_audio_start > 0); let is_flac = mm.is_some(); let header = mm.map(|mm| mm.header.clone()).unwrap_or_default(); let pic_hdrs = mm .map(|mm| mm.picture_block_headers.clone()) .unwrap_or_default(); let pic_ranges = mm .map(|mm| mm.picture_data_ranges.clone()) .unwrap_or_default(); let real_audio_start = mm.map(|mm| mm.real_audio_start).unwrap_or(0); drop(files); if !is_flac { match LocalOrigin::read_bytes_at(&original_path, offset, size as usize) { Ok(bytes) => reply.data(&bytes), Err(_) => reply.error(Errno::EIO), } return; } let end = offset + size as u64; let header_end = header.len() as u64; // Pure header read — most common for tag readers if end <= header_end { reply.data(&header[offset as usize..end as usize]); return; } // Multi-region read: walk [header][pic_hdr+pic_data]...[audio] and collect bytes let mut buf = Vec::with_capacity(size as usize); if offset < header_end { buf.extend_from_slice(&header[offset as usize..]); } let mut virt_pos = header_end; for (pic_hdr, (data_real_offset, data_len)) in pic_hdrs.iter().zip(pic_ranges.iter()) { let pic_hdr_end = virt_pos + 4; let pic_end = pic_hdr_end + data_len; if end <= virt_pos { break; } if offset >= pic_end { virt_pos = pic_end; continue; } let hdr_from = (offset.max(virt_pos) - virt_pos) as usize; let hdr_to = ((end.min(pic_hdr_end)) - virt_pos) as usize; if hdr_from < hdr_to { buf.extend_from_slice(&pic_hdr[hdr_from..hdr_to.min(4)]); } let data_start = offset.max(pic_hdr_end); let data_end = end.min(pic_end); if data_start < data_end { let real_off = data_real_offset + (data_start - pic_hdr_end); match LocalOrigin::read_bytes_at( &original_path, real_off, (data_end - data_start) as usize, ) { Ok(bytes) => buf.extend_from_slice(&bytes), Err(_) => { reply.error(Errno::EIO); return; } } } virt_pos = pic_end; } let audio_start = offset.max(virt_pos); if audio_start < end { let real_off = real_audio_start + (audio_start - virt_pos); match LocalOrigin::read_bytes_at(&original_path, real_off, (end - audio_start) as usize) { Ok(bytes) => buf.extend_from_slice(&bytes), Err(_) => { reply.error(Errno::EIO); return; } } } reply.data(&buf); } fn lookup( &self, _req: &Request, parent: INodeNo, name: &std::ffi::OsStr, reply: fuser::ReplyEntry, ) { println!("lookup(parent={}, name={})", parent, name.display()); return match self .files .lock() .unwrap() .iter() .find(|item| item.1.parent_inode == parent && item.1.name == name.to_str().unwrap()) { Some(item) => { let ttl = Duration::new(1, 0); let attr = LocalOrigin::local_file_to_file_attr(item.1); reply.entry(&ttl, &attr, Generation(0)); } None => reply.error(Errno::ENOENT), }; } } impl FileWatcher for LocalOrigin { fn watch(&self) { let files = self.files.clone(); let source = self.source.clone(); let destination = self.destination.clone(); println!("Starting to watch the source files in another thread"); thread::spawn(move || { let (tx, rx): ( mpsc::Sender>, mpsc::Receiver>, ) = mpsc::channel(); // Use recommended_watcher() to automatically select the best implementation // for your platform. The `EventHandler` passed to this constructor can be a // closure, a `std::sync::mpsc::Sender`, a `crossbeam_channel::Sender`, or // another type the trait is implemented for. let mut watcher: notify::INotifyWatcher = notify::recommended_watcher(tx).unwrap(); // Add a path to be watched. All files and directories at that path and // below will be monitored for changes. watcher.watch(&source, RecursiveMode::Recursive).unwrap(); // Block forever, printing out events as they come in for res in rx { match res { Ok(event) => { println!("event: {:?}", event); match event.kind { EventKind::Any => { println!("Something happened to item, ignoring"); } EventKind::Access(_access_kind) => { println!("Item was read"); } EventKind::Create(_) | EventKind::Modify(_) | EventKind::Remove(_) => { println!("Item was removed"); LocalOrigin::fill_fileset(&files, &source, &destination); } EventKind::Other => { println!("Some other action happened to item, ignoring"); } } } Err(e) => println!("watch error: {:?}", e), } } }); } } #[derive(Clone, Debug, PartialEq, Eq, sea_orm::DeriveEntityModel)] #[sea_orm(table_name = "items")] pub struct Model { #[sea_orm(primary_key, auto_increment = false)] pub inode: i64, pub name: String, pub original_path: String, pub local_path: String, pub file_type: String, pub hash: i64, } pub type LocalItemEntity = Model; #[derive(Copy, Clone, Debug, sea_orm::EnumIter, sea_orm::DeriveRelation)] pub enum Relation {} impl sea_orm::ActiveModelBehavior for ActiveModel {} impl From<&LocalItem> for ActiveModel { fn from(item: &LocalItem) -> Self { use sea_orm::ActiveValue::Set; ActiveModel { inode: Set(item.inode.0 as i64), name: Set(item.name.clone()), original_path: Set(item.original_path.to_string_lossy().into_owned()), local_path: Set(item.local_path.to_string_lossy().into_owned()), file_type: Set(match item.file_type { FileType::Directory => "directory".to_string(), FileType::File => "file".to_string(), }), hash: Set(item.hash as i64), } } } impl From<(LocalItemEntity, fs::Metadata)> for LocalItem { fn from((entity, metadata): (LocalItemEntity, fs::Metadata)) -> Self { let local_path = PathBuf::from(&entity.local_path); let parent_inode = LocalOrigin::parent_inode_from_path(&local_path); let local_item = LocalItem { inode: INodeNo(entity.inode as u64), parent_inode, name: entity.name, original_path: PathBuf::from(entity.original_path), local_path, file_type: match entity.file_type.as_str() { "directory" => FileType::Directory, _ => FileType::File, }, metadata, music_metadata: None, hash: 0, }; LocalItem { hash: local_item.compute_hash(), ..local_item } } }