diff --git a/src/db/entities.rs b/src/db/entities.rs index 6e2e0ad..c2c0d3a 100644 --- a/src/db/entities.rs +++ b/src/db/entities.rs @@ -1,10 +1,6 @@ -use std::{fs, path::PathBuf}; - -use fuser::INodeNo; use sea_orm::entity::prelude::*; use crate::item::{FileType, Item}; -use crate::virtual_dirs::parent_inode_from_path; #[derive(Clone, Debug, PartialEq, Eq, DeriveEntityModel)] #[sea_orm(table_name = "items")] @@ -40,36 +36,22 @@ impl From<&Item> for ActiveModel { } } -impl From<(Model, fs::Metadata)> for Item { - fn from((entity, metadata): (Model, fs::Metadata)) -> Self { - let local_path = PathBuf::from(&entity.local_path); - let parent_inode = parent_inode_from_path(&local_path); - let file_type = match entity.file_type.as_str() { - "directory" => FileType::Directory, - _ => FileType::File, - }; - Item::new( - INodeNo(entity.inode as u64), - parent_inode, - entity.name, - PathBuf::from(entity.original_path), - local_path, - file_type, - metadata, - None, - ) - } -} - #[cfg(test)] mod tests { use super::*; use crate::item::{FileType, Item}; + use crate::origins::attrs::FileAttrs; + use fuser::INodeNo; + use std::path::PathBuf; + + fn attrs_for_tempdir() -> FileAttrs { + let tmp = tempfile::tempdir().unwrap(); + let metadata = std::fs::metadata(tmp.path()).unwrap(); + return FileAttrs::from(&metadata); + } #[test] fn item_to_active_model_fields_match() { - let tmp = tempfile::tempdir().unwrap(); - let metadata = std::fs::metadata(tmp.path()).unwrap(); let item = Item::new( INodeNo(42), INodeNo::ROOT, @@ -77,7 +59,7 @@ mod tests { PathBuf::from("/original/path"), PathBuf::from("local/path"), FileType::Directory, - metadata, + attrs_for_tempdir(), None, ); let am = ActiveModel::from(&item); @@ -115,9 +97,6 @@ mod tests { #[test] fn file_type_string_mapping() { - let tmp = tempfile::tempdir().unwrap(); - let metadata = std::fs::metadata(tmp.path()).unwrap(); - let item_dir = Item::new( INodeNo(1), INodeNo::ROOT, @@ -125,7 +104,7 @@ mod tests { PathBuf::from("/original/dir"), PathBuf::from("local/dir"), FileType::Directory, - metadata.clone(), + attrs_for_tempdir(), None, ); let am_dir = ActiveModel::from(&item_dir); @@ -143,7 +122,7 @@ mod tests { PathBuf::from("/original/file"), PathBuf::from("local/file"), FileType::File, - metadata, + attrs_for_tempdir(), None, ); let am_file = ActiveModel::from(&item_file); diff --git a/src/item.rs b/src/item.rs index 2bc6135..030e3da 100644 --- a/src/item.rs +++ b/src/item.rs @@ -1,12 +1,16 @@ -use std::{fs, hash::Hasher, path::PathBuf, time::SystemTime}; +use std::{ + hash::Hasher, + path::PathBuf, + time::{SystemTime, UNIX_EPOCH}, +}; use std::os::unix::ffi::OsStrExt; -use std::os::unix::fs::MetadataExt; 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 { @@ -14,7 +18,7 @@ pub enum FileType { File, } -#[derive(Debug)] +#[derive(Debug, Clone)] pub struct Item { pub inode: INodeNo, pub parent_inode: INodeNo, @@ -22,11 +26,18 @@ pub struct Item { pub original_path: PathBuf, pub local_path: PathBuf, pub file_type: FileType, - pub metadata: fs::Metadata, + 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); +} + impl Item { #[allow(clippy::too_many_arguments)] pub fn new( @@ -36,7 +47,7 @@ impl Item { original_path: PathBuf, local_path: PathBuf, file_type: FileType, - metadata: fs::Metadata, + attrs: FileAttrs, music_metadata: Option, ) -> Item { let mut item = Item { @@ -46,7 +57,7 @@ impl Item { original_path, local_path, file_type, - metadata, + attrs, music_metadata, hash: 0, }; @@ -59,25 +70,11 @@ impl Item { 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); + hasher.write_u64(secs_since_epoch(self.attrs.ctime)); + hasher.write_u64(secs_since_epoch(self.attrs.mtime)); + hasher.write_u64(secs_since_epoch(self.attrs.crtime)); return hasher.finish(); } @@ -86,11 +83,11 @@ impl Item { #[cfg(test)] mod tests { use super::*; + use crate::origins::attrs::FileAttrs; #[test] fn compute_hash_deterministic() { - let tmp = tempfile::tempdir().unwrap(); - let metadata = std::fs::metadata(tmp.path()).unwrap(); + let attrs = file_attrs_for_tempdir(); let item1 = Item::new( INodeNo(42), @@ -99,11 +96,10 @@ mod tests { PathBuf::from("/some/path"), PathBuf::from("test"), FileType::File, - metadata, + attrs.clone(), None, ); - let metadata2 = std::fs::metadata(tmp.path()).unwrap(); let item2 = Item::new( INodeNo(42), INodeNo::ROOT, @@ -111,7 +107,7 @@ mod tests { PathBuf::from("/some/path"), PathBuf::from("test"), FileType::File, - metadata2, + attrs, None, ); @@ -120,8 +116,7 @@ mod tests { #[test] fn compute_hash_changes_on_path_change() { - let tmp = tempfile::tempdir().unwrap(); - let metadata = std::fs::metadata(tmp.path()).unwrap(); + let attrs = file_attrs_for_tempdir(); let item1 = Item::new( INodeNo(42), @@ -130,11 +125,10 @@ mod tests { PathBuf::from("/some/path"), PathBuf::from("test"), FileType::File, - metadata, + attrs.clone(), None, ); - let metadata2 = std::fs::metadata(tmp.path()).unwrap(); let item2 = Item::new( INodeNo(42), INodeNo::ROOT, @@ -142,10 +136,16 @@ mod tests { PathBuf::from("/different/path"), PathBuf::from("test"), FileType::File, - metadata2, + 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); + } } diff --git a/src/main.rs b/src/main.rs index 030f7f6..696901c 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,5 +1,13 @@ +use std::{collections::HashMap, path::Path}; + use clap::Parser; -use musicfs::origins::local::watcher::FileWatcher; +use musicfs::db::entities::{Entity, Model}; +use musicfs::db::sync::sync_items_to_db; +use musicfs::music::db::restore_music_metadata_from_db; +use musicfs::origins::local::LocalOrigin; +use musicfs::origins::{FuseFs, Origin}; +use musicfs::virtual_dirs::restore_virtual_paths; +use sea_orm::entity::prelude::*; use tokio::signal::unix::{SignalKind, signal}; #[derive(Parser, Debug)] @@ -22,11 +30,32 @@ async fn main() { let db = sea_orm::Database::connect(&args.database).await.unwrap(); - let fs = musicfs::origins::local::LocalOrigin::new(args.source.clone(), mountpoint.clone(), db) - .await - .unwrap(); + let origin = LocalOrigin::new(args.source.clone(), mountpoint.clone()); - fs.watch(); + let mut snapshot = origin.snapshot().unwrap(); + + let db_items: HashMap = Entity::find() + .all(&db) + .await + .unwrap() + .into_iter() + .map(|e| (e.inode, e)) + .collect(); + + sync_items_to_db(&snapshot, &db_items, &db).await; + restore_music_metadata_from_db(&mut snapshot, &db_items, &db).await; + restore_virtual_paths(&mut snapshot, &db_items, Path::new(&args.source)); + + let files = std::sync::Arc::new(std::sync::Mutex::new(snapshot)); + let bytes = origin.byte_source(); + let watcher = origin.watcher(); + watcher.watch(files.clone()); + + let fs = FuseFs { + files, + bytes, + client: db, + }; let cfg = fuser::Config::default(); let session = diff --git a/src/origins/attrs.rs b/src/origins/attrs.rs new file mode 100644 index 0000000..38ef588 --- /dev/null +++ b/src/origins/attrs.rs @@ -0,0 +1,88 @@ +use std::{ + fs, + time::{Duration, SystemTime, UNIX_EPOCH}, +}; + +use std::os::unix::fs::{MetadataExt, PermissionsExt}; + +/// Owned, `Clone`-able snapshot of the file attributes that musicfs needs to +/// serve FUSE `getattr` and to compute the per-item identity hash. +/// +/// Decoupled from `std::fs::Metadata` so that non-disk origins (e.g. a future +/// `NetworkOrigin` reading a server manifest) can construct equivalent +/// attributes without a real inode on disk. The on-disk origin builds this +/// via `From<&fs::Metadata>`; other origins build it from their own metadata +/// source using the same field types. +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct FileAttrs { + pub size: u64, + pub blocks: u64, + pub atime: SystemTime, + pub mtime: SystemTime, + pub ctime: SystemTime, + pub crtime: SystemTime, + pub perm: u16, + pub nlink: u32, + pub uid: u32, + pub gid: u32, + pub rdev: u32, + pub blksize: u32, +} + +impl From<&fs::Metadata> for FileAttrs { + fn from(metadata: &fs::Metadata) -> Self { + return FileAttrs { + size: metadata.size(), + blocks: metadata.blocks(), + atime: metadata.accessed().unwrap_or(UNIX_EPOCH), + mtime: metadata.modified().unwrap_or(UNIX_EPOCH), + ctime: UNIX_EPOCH + Duration::from_secs(metadata.ctime() as u64), + crtime: metadata.created().unwrap_or(UNIX_EPOCH), + 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, + }; + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn file_attrs_from_metadata_copies_size_and_block_fields() { + let tmp = tempfile::tempdir().unwrap(); + let metadata = fs::metadata(tmp.path()).unwrap(); + + let attrs = FileAttrs::from(&metadata); + + assert_eq!(attrs.size, metadata.size()); + assert_eq!(attrs.blocks, metadata.blocks()); + assert_eq!(attrs.blksize, metadata.blksize() as u32); + } + + #[test] + fn file_attrs_from_metadata_copies_unix_ownership() { + let tmp = tempfile::tempdir().unwrap(); + let metadata = fs::metadata(tmp.path()).unwrap(); + + let attrs = FileAttrs::from(&metadata); + + assert_eq!(attrs.uid, metadata.uid()); + assert_eq!(attrs.gid, metadata.gid()); + assert_eq!(attrs.perm, metadata.permissions().mode() as u16); + } + + #[test] + fn file_attrs_from_metadata_preserves_mtime() { + let tmp = tempfile::tempdir().unwrap(); + let metadata = fs::metadata(tmp.path()).unwrap(); + + let attrs = FileAttrs::from(&metadata); + + assert_eq!(attrs.mtime, metadata.modified().unwrap_or(UNIX_EPOCH)); + } +} diff --git a/src/origins/local/file_io.rs b/src/origins/local/file_io.rs index 8bc71fb..1d34e54 100644 --- a/src/origins/local/file_io.rs +++ b/src/origins/local/file_io.rs @@ -14,8 +14,16 @@ pub fn read_bytes_at(path: &Path, offset: u64, len: usize) -> io::Result return Ok(buf); } +/// Reassemble a virtualized FLAC byte range by stitching together the cached +/// header, picture block headers, and on-demand audio/picture bytes fetched +/// through `reader`. +/// +/// `reader(offset, len)` is transport-agnostic: for the local origin it wraps +/// `read_bytes_at(&path, ...)`; a network origin will route the same call +/// through its byte cache + HTTP range request. The FLAC assembly logic never +/// touches a `Path` directly. pub fn assemble_flac_read( - original_path: &Path, + reader: &dyn Fn(u64, usize) -> io::Result>, header: &[u8], pic_hdrs: &[Vec], pic_ranges: &[(u64, u64)], @@ -60,7 +68,7 @@ pub fn assemble_flac_read( let data_end = end.min(pic_end); if data_start < data_end { let real_off = data_real_offset + (data_start - pic_hdr_end); - let bytes = read_bytes_at(original_path, real_off, (data_end - data_start) as usize)?; + let bytes = reader(real_off, (data_end - data_start) as usize)?; buf.extend_from_slice(&bytes); } @@ -70,7 +78,7 @@ pub fn assemble_flac_read( let audio_start = offset.max(virt_pos); if audio_start < end { let real_off = real_audio_start + (audio_start - virt_pos); - let bytes = read_bytes_at(original_path, real_off, (end - audio_start) as usize)?; + let bytes = reader(real_off, (end - audio_start) as usize)?; buf.extend_from_slice(&bytes); } @@ -81,6 +89,11 @@ pub fn assemble_flac_read( mod tests { use super::*; use std::io::Write; + use std::path::PathBuf; + + fn file_reader(path: PathBuf) -> impl Fn(u64, usize) -> io::Result> { + return move |offset, len| read_bytes_at(&path, offset, len); + } #[test] fn read_bytes_at_full_file() { @@ -107,7 +120,8 @@ mod tests { #[test] fn assemble_flac_read_header_only() { let header = b"HEADERDATA"; - let result = assemble_flac_read(Path::new(""), header, &[], &[], 0, 2, 4).unwrap(); + let noop = |_, _| Ok(vec![]); + let result = assemble_flac_read(&noop, header, &[], &[], 0, 2, 4).unwrap(); assert_eq!(result, b"ADER"); } @@ -117,10 +131,11 @@ mod tests { let content: Vec = (0..20).collect(); f.write_all(&content).unwrap(); f.flush().unwrap(); - let path = f.path(); + let path = f.path().to_path_buf(); + let reader = file_reader(path); let header = b"HDR"; - let result = assemble_flac_read(path, header, &[], &[], 10, 5, 4).unwrap(); + let result = assemble_flac_read(&reader, header, &[], &[], 10, 5, 4).unwrap(); // offset=5, header_len=3, so we read from header[3..] (0 bytes) + audio at real_off=10+(5-3)=12 // content[12..16] = [12, 13, 14, 15] assert_eq!(result, vec![12, 13, 14, 15]); @@ -132,10 +147,11 @@ mod tests { let content: Vec = (0..110).collect(); f.write_all(&content).unwrap(); f.flush().unwrap(); - let path = f.path(); + let path = f.path().to_path_buf(); + let reader = file_reader(path); let header = b"ABCD"; - let result = assemble_flac_read(path, header, &[], &[], 100, 2, 6).unwrap(); + let result = assemble_flac_read(&reader, header, &[], &[], 100, 2, 6).unwrap(); // offset=2, size=6, header_len=4 // First 2 bytes from header[2..4] = "CD" // Remaining 4 bytes from audio at real_off=100+(2+2-4)=100 diff --git a/src/origins/local/mod.rs b/src/origins/local/mod.rs index d965cd3..42bd43e 100644 --- a/src/origins/local/mod.rs +++ b/src/origins/local/mod.rs @@ -6,26 +6,16 @@ use std::{ collections::BTreeMap, io, path::{Path, PathBuf}, - sync::{Arc, Mutex}, - time::Duration, + sync::Arc, }; -use fuser::{Errno, FileAttr, Filesystem, Generation, INodeNo, Request}; -use sea_orm::entity::prelude::*; - -use crate::db::entities::{ActiveModel, Entity, Model}; -use crate::db::sync::{run_db_blocking, sync_items_to_db}; -use crate::item::{FileType, Item}; -use crate::music::db::{restore_music_metadata_from_db, save_music_metadata}; -use crate::virtual_dirs::{parent_inode_from_path, restore_virtual_paths}; - -use std::os::unix::fs::{MetadataExt, PermissionsExt}; +use fuser::INodeNo; +use crate::item::Item; +use crate::origins::{ByteSource, FileWatcher, Origin}; pub struct LocalOrigin { pub(crate) source: PathBuf, pub(crate) destination: PathBuf, - pub(crate) files: Arc>>, - client: sea_orm::DatabaseConnection, } impl std::fmt::Debug for LocalOrigin { @@ -34,540 +24,44 @@ impl std::fmt::Debug for LocalOrigin { .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 = snapshot::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(); - - sync_items_to_db(&snapshot, &db_items, &client).await; - restore_music_metadata_from_db(&mut snapshot, &db_items, &client).await; - restore_virtual_paths(&mut snapshot, &db_items, Path::new(&source)); - - let local_origin = LocalOrigin { + pub fn new(source: String, destination: String) -> LocalOrigin { + return LocalOrigin { source: source.into(), destination: destination.into(), - files: Arc::new(Mutex::new(snapshot)), - client, }; - return Ok(local_origin); } - fn file_to_attr(item: &Item) -> FileAttr { - let metadata = &item.metadata; - let ctime = std::time::UNIX_EPOCH + Duration::from_secs(metadata.ctime() as u64); - let atime = metadata - .accessed() - .unwrap_or(std::time::SystemTime::UNIX_EPOCH); - let mtime = metadata - .modified() - .unwrap_or(std::time::SystemTime::UNIX_EPOCH); - let crtime = metadata - .created() - .unwrap_or(std::time::SystemTime::UNIX_EPOCH); - let kind = match item.file_type { - FileType::Directory => fuser::FileType::Directory, - FileType::File => fuser::FileType::RegularFile, - }; - - let size = match &item.music_metadata { - Some(mm) if !mm.header.is_empty() => mm.virtual_size(metadata.size()), - _ => metadata.size(), - }; - - return FileAttr { - ino: item.inode, - size, - blocks: metadata.blocks(), - atime, - mtime, - ctime, - crtime, - 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, - }; + pub fn source(&self) -> &Path { + return &self.source; } } -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); - } +impl Origin for LocalOrigin { + fn snapshot(&self) -> io::Result> { + return snapshot::build_snapshot(&self.source, &self.destination); } - 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::file_to_attr(file)), - None => reply.error(Errno::ENOENT), - } + fn byte_source(&self) -> Arc { + return Arc::new(LocalByteSource); } - 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; - } - }; - - match &mut item.music_metadata { - // FLAC: intercept Vorbis comment block writes - Some(mm) if mm.vorbis_comment_length > 0 => { - let vc_data_offset = mm.vorbis_comment_offset; - let vc_hdr_offset = vc_data_offset - 4; - 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; - mm.update_from_vorbis_comment_data(&data[from..to]); - Some(mm.clone()) - } else { - None - } - } else { - None - } - } - // MP3: intercept ID3v2 header writes (must start at offset 0) - Some(mm) - if !mm.header.is_empty() - && write_start == 0 - && data.len() >= 3 - && &data[0..3] == b"ID3" => - { - mm.update_from_id3_data(data); - Some(mm.clone()) - } - // MP3: intercept ID3v1 fallback writes (128-byte "TAG" block) - Some(mm) if !mm.header.is_empty() && data.len() == 128 && &data[0..3] == b"TAG" => { - mm.update_from_id3v1_data(data); - Some(mm.clone()) - } - _ => None, - } - }; - - if let Some(music_metadata) = updated_music_metadata { - let client = self.client.clone(); - let ino_i64 = ino.0 as i64; - run_db_blocking(async move { - 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); - match self.files.lock().unwrap().get(&ino) { - Some(file) => { - let ttl = Duration::new(1, 0); - let attr = LocalOrigin::file_to_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; - } - }; - - // Offsets are 1-based cursors: "." = 1, ".." = 2, real entries = 3+. - // The kernel passes back the offset of the last entry it received; - // we return entries with offset > that value. - if offset < 1 { - if reply.add(ino, 1, fuser::FileType::Directory, ".") { - reply.ok(); - return; - } - } - - if offset < 2 { - if reply.add(parent_inode, 2, fuser::FileType::Directory, "..") { - reply.ok(); - return; - } - } - - let skip_count = if offset <= 2 { - 0 - } else { - (offset - 2) as usize - }; - - for (i, (key, value)) in files - .iter() - .filter(|(_, v)| v.parent_inode == ino && v.inode != ino) - .skip(skip_count) - .enumerate() - { - let entry_offset = (skip_count + i + 3) as u64; - let file_type = match value.file_type { - FileType::Directory => fuser::FileType::Directory, - FileType::File => fuser::FileType::RegularFile, - }; - if reply.add(*key, entry_offset, file_type, &value.name) { - 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 = 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); - - run_db_blocking(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 flac = item - .music_metadata - .as_ref() - .filter(|mm| !mm.header.is_empty()) - .map(|mm| { - ( - mm.header.clone(), - mm.picture_block_headers.clone(), - mm.picture_data_ranges.clone(), - mm.real_audio_start, - ) - }); - drop(files); - - let Some((header, pic_hdrs, pic_ranges, real_audio_start)) = flac else { - match file_io::read_bytes_at(&original_path, offset, size as usize) { - Ok(bytes) => reply.data(&bytes), - Err(_) => reply.error(Errno::EIO), - } - return; - }; - - match file_io::assemble_flac_read( - &original_path, - &header, - &pic_hdrs, - &pic_ranges, - real_audio_start, - offset, - size, - ) { - Ok(bytes) => reply.data(&bytes), - Err(_) => reply.error(Errno::EIO), - } - } - - fn lookup( - &self, - _req: &Request, - parent: INodeNo, - name: &std::ffi::OsStr, - reply: fuser::ReplyEntry, - ) { - println!("lookup(parent={}, name={})", parent, name.display()); - - 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::file_to_attr(item.1); - - reply.entry(&ttl, &attr, Generation(0)); - } - None => reply.error(Errno::ENOENT), - } + fn watcher(&self) -> Box { + return Box::new(watcher::LocalOriginFileWatcher::new( + self.source.clone(), + self.destination.clone(), + )); } } -#[cfg(test)] -mod tests { - use super::*; - use crate::item::Item; - use crate::music::metadata::MusicMetadata; - use std::os::unix::fs::MetadataExt; - use std::path::PathBuf; +pub struct LocalByteSource; - #[test] - fn file_to_attr_uses_real_size_for_non_flac() { - let tmp = tempfile::tempdir().unwrap(); - let metadata = std::fs::metadata(tmp.path()).unwrap(); - let item = Item::new( - INodeNo(42), - INodeNo::ROOT, - "test".to_string(), - tmp.path().to_path_buf(), - PathBuf::from("test"), - FileType::File, - metadata.clone(), - None, - ); - - let attr = LocalOrigin::file_to_attr(&item); - assert_eq!(attr.size, metadata.size()); - } - - #[test] - fn file_to_attr_uses_virtual_size_for_flac() { - let tmp = tempfile::tempdir().unwrap(); - let test_file = tmp.path().join("test.flac"); - std::fs::write(&test_file, vec![0u8; 1000]).unwrap(); - let metadata = std::fs::metadata(&test_file).unwrap(); - let mm = MusicMetadata { - real_audio_start: 500, - header: vec![0u8; 100], - picture_data_ranges: vec![(0, 50)], - ..MusicMetadata::default() - }; - let item = Item::new( - INodeNo(43), - INodeNo::ROOT, - "test_flac".to_string(), - test_file.clone(), - PathBuf::from("test_flac"), - FileType::File, - metadata.clone(), - Some(mm.clone()), - ); - - let attr = LocalOrigin::file_to_attr(&item); - let expected_virtual_size = mm.virtual_size(metadata.size()); - assert_eq!(attr.size, expected_virtual_size); - } - - #[test] - fn file_to_attr_kind_directory() { - let tmp = tempfile::tempdir().unwrap(); - let metadata = std::fs::metadata(tmp.path()).unwrap(); - let item = Item::new( - INodeNo(44), - INodeNo::ROOT, - "test_dir".to_string(), - tmp.path().to_path_buf(), - PathBuf::from("test_dir"), - FileType::Directory, - metadata, - None, - ); - - let attr = LocalOrigin::file_to_attr(&item); - assert_eq!(attr.kind, fuser::FileType::Directory); - } - - #[test] - fn file_to_attr_kind_file() { - let tmp = tempfile::tempdir().unwrap(); - let metadata = std::fs::metadata(tmp.path()).unwrap(); - let item = Item::new( - INodeNo(45), - INodeNo::ROOT, - "test_file".to_string(), - tmp.path().to_path_buf(), - PathBuf::from("test_file"), - FileType::File, - metadata, - None, - ); - - let attr = LocalOrigin::file_to_attr(&item); - assert_eq!(attr.kind, fuser::FileType::RegularFile); +impl ByteSource for LocalByteSource { + fn read_at(&self, locator: &Path, offset: u64, len: usize) -> io::Result> { + return file_io::read_bytes_at(locator, offset, len); } } diff --git a/src/origins/local/snapshot.rs b/src/origins/local/snapshot.rs index d3b7a51..f799e47 100644 --- a/src/origins/local/snapshot.rs +++ b/src/origins/local/snapshot.rs @@ -11,6 +11,7 @@ use std::os::unix::fs::MetadataExt; use crate::item::{FileType, Item}; use crate::music::encoder::MusicMetadataEncoderFactory; use crate::music::parser::MusicMetadataParserFactory; +use crate::origins::attrs::FileAttrs; use crate::virtual_dirs::ensure_virtual_dirs; pub fn fill_fileset(map: &Arc>>, source: &Path, destination: &Path) { @@ -46,7 +47,7 @@ pub fn build_snapshot( source.to_path_buf(), destination.to_path_buf(), FileType::Directory, - fs::metadata(source)?, + FileAttrs::from(&fs::metadata(source)?), None, ); map.insert(INodeNo::ROOT, local_root); @@ -104,7 +105,7 @@ pub fn read_into_map( item_path, local_path, FileType::File, - metadata, + FileAttrs::from(&metadata), music_metadata, ); diff --git a/src/origins/local/watcher.rs b/src/origins/local/watcher.rs index 09d171c..bfe5283 100644 --- a/src/origins/local/watcher.rs +++ b/src/origins/local/watcher.rs @@ -1,16 +1,32 @@ -use std::{sync::mpsc, thread}; +use std::{ + collections::BTreeMap, + path::PathBuf, + sync::{Arc, Mutex, mpsc}, + thread, +}; +use fuser::INodeNo; use notify::{Event, EventKind, RecursiveMode, Watcher}; -pub trait FileWatcher { - fn watch(&self); +use crate::item::Item; +use crate::origins::FileWatcher; + +pub struct LocalOriginFileWatcher { + source: PathBuf, + destination: PathBuf, } -use super::LocalOrigin; +impl LocalOriginFileWatcher { + pub fn new(source: PathBuf, destination: PathBuf) -> Self { + return LocalOriginFileWatcher { + source, + destination, + }; + } +} -impl FileWatcher for LocalOrigin { - fn watch(&self) { - let files = self.files.clone(); +impl FileWatcher for LocalOriginFileWatcher { + fn watch(&self, files: Arc>>) { let source = self.source.clone(); let destination = self.destination.clone(); diff --git a/src/origins/mod.rs b/src/origins/mod.rs index 2709962..897920f 100644 --- a/src/origins/mod.rs +++ b/src/origins/mod.rs @@ -1 +1,534 @@ +pub mod attrs; pub mod local; + +use std::{ + collections::BTreeMap, + io, + path::Path, + sync::{Arc, Mutex}, + time::Duration, +}; + +use fuser::{Errno, FileAttr, Filesystem, Generation, INodeNo, Request}; +use sea_orm::{ActiveModelTrait, DatabaseConnection}; + +use crate::db::sync::run_db_blocking; +use crate::item::{FileType, Item}; +use crate::music::db::save_music_metadata; +use crate::origins::local::file_io; +use crate::virtual_dirs::parent_inode_from_path; + +/// Transport-agnostic byte-range reader. `locator` is whatever string the +/// origin treats as a file key: a filesystem path for `LocalOrigin`, a remote +/// key for the future network origin. Both produce bytes at `(offset, len)`. +pub trait ByteSource: Send + Sync { + fn read_at(&self, locator: &Path, offset: u64, len: usize) -> io::Result>; +} + +pub trait FileWatcher: Send + Sync { + fn watch(&self, files: Arc>>); +} + +pub trait Origin: Send + Sync { + fn snapshot(&self) -> io::Result>; + fn byte_source(&self) -> Arc; + fn watcher(&self) -> Box; +} + +pub struct FuseFs { + pub files: Arc>>, + pub bytes: Arc, + pub client: DatabaseConnection, +} + +pub(crate) fn file_to_attr(item: &Item) -> FileAttr { + let attrs = &item.attrs; + let kind = match item.file_type { + FileType::Directory => fuser::FileType::Directory, + FileType::File => fuser::FileType::RegularFile, + }; + + let size = match &item.music_metadata { + Some(mm) if !mm.header.is_empty() => mm.virtual_size(attrs.size), + _ => attrs.size, + }; + + return FileAttr { + ino: item.inode, + size, + blocks: attrs.blocks, + atime: attrs.atime, + mtime: attrs.mtime, + ctime: attrs.ctime, + crtime: attrs.crtime, + kind, + perm: attrs.perm, + nlink: attrs.nlink, + uid: attrs.uid, + gid: attrs.gid, + rdev: attrs.rdev, + blksize: attrs.blksize, + flags: 0, + }; +} + +impl Filesystem for FuseFs { + 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), &file_to_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; + } + }; + + match &mut item.music_metadata { + Some(mm) if mm.vorbis_comment_length > 0 => { + let vc_data_offset = mm.vorbis_comment_offset; + let vc_hdr_offset = vc_data_offset - 4; + 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; + mm.update_from_vorbis_comment_data(&data[from..to]); + Some(mm.clone()) + } else { + None + } + } else { + None + } + } + Some(mm) + if !mm.header.is_empty() + && write_start == 0 + && data.len() >= 3 + && &data[0..3] == b"ID3" => + { + mm.update_from_id3_data(data); + Some(mm.clone()) + } + Some(mm) if !mm.header.is_empty() && data.len() == 128 && &data[0..3] == b"TAG" => { + mm.update_from_id3v1_data(data); + Some(mm.clone()) + } + _ => None, + } + }; + + if let Some(music_metadata) = updated_music_metadata { + let client = self.client.clone(); + let ino_i64 = ino.0 as i64; + run_db_blocking(async move { + 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); + match self.files.lock().unwrap().get(&ino) { + Some(file) => { + let ttl = Duration::new(1, 0); + let attr = file_to_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 < 1 { + if reply.add(ino, 1, fuser::FileType::Directory, ".") { + reply.ok(); + return; + } + } + + if offset < 2 { + if reply.add(parent_inode, 2, fuser::FileType::Directory, "..") { + reply.ok(); + return; + } + } + + let skip_count = if offset <= 2 { + 0 + } else { + (offset - 2) as usize + }; + + for (i, (key, value)) in files + .iter() + .filter(|(_, v)| v.parent_inode == ino && v.inode != ino) + .skip(skip_count) + .enumerate() + { + let entry_offset = (skip_count + i + 3) as u64; + let file_type = match value.file_type { + FileType::Directory => fuser::FileType::Directory, + FileType::File => fuser::FileType::RegularFile, + }; + if reply.add(*key, entry_offset, file_type, &value.name) { + 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 { + std::path::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 = 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); + + run_db_blocking(async move { + use sea_orm::ActiveValue::Set; + crate::db::entities::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 (locator, flac) = { + let files = self.files.lock().unwrap(); + let item = match files.get(&ino) { + Some(f) => f, + None => { + reply.error(Errno::ENOENT); + return; + } + }; + let locator = item.original_path.clone(); + let flac = item + .music_metadata + .as_ref() + .filter(|mm| !mm.header.is_empty()) + .map(|mm| { + ( + mm.header.clone(), + mm.picture_block_headers.clone(), + mm.picture_data_ranges.clone(), + mm.real_audio_start, + ) + }); + (locator, flac) + }; + + let Some((header, pic_hdrs, pic_ranges, real_audio_start)) = flac else { + match self.bytes.read_at(&locator, offset, size as usize) { + Ok(bytes) => reply.data(&bytes), + Err(_) => reply.error(Errno::EIO), + } + return; + }; + + let bytes = &self.bytes; + let reader = |off: u64, len: usize| bytes.read_at(&locator, off, len); + match file_io::assemble_flac_read( + &reader, + &header, + &pic_hdrs, + &pic_ranges, + real_audio_start, + offset, + size, + ) { + Ok(bytes) => reply.data(&bytes), + Err(_) => reply.error(Errno::EIO), + } + } + + fn lookup( + &self, + _req: &Request, + parent: INodeNo, + name: &std::ffi::OsStr, + reply: fuser::ReplyEntry, + ) { + println!("lookup(parent={}, name={})", parent, name.display()); + + 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 = file_to_attr(item.1); + + reply.entry(&ttl, &attr, Generation(0)); + } + None => reply.error(Errno::ENOENT), + } + } +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::item::FileType; + use crate::music::metadata::MusicMetadata; + use crate::origins::attrs::FileAttrs; + use fuser::INodeNo; + use std::os::unix::fs::MetadataExt; + use std::path::PathBuf; + + #[test] + fn file_to_attr_uses_real_size_for_non_flac() { + let tmp = tempfile::tempdir().unwrap(); + let metadata = std::fs::metadata(tmp.path()).unwrap(); + let attrs = FileAttrs::from(&metadata); + let item = Item::new( + INodeNo(42), + INodeNo::ROOT, + "test".to_string(), + tmp.path().to_path_buf(), + PathBuf::from("test"), + FileType::File, + attrs.clone(), + None, + ); + + let attr = file_to_attr(&item); + assert_eq!(attr.size, metadata.size()); + } + + #[test] + fn file_to_attr_uses_virtual_size_for_flac() { + let tmp = tempfile::tempdir().unwrap(); + let test_file = tmp.path().join("test.flac"); + std::fs::write(&test_file, vec![0u8; 1000]).unwrap(); + let metadata = std::fs::metadata(&test_file).unwrap(); + let attrs = FileAttrs::from(&metadata); + let mm = MusicMetadata { + real_audio_start: 500, + header: vec![0u8; 100], + picture_data_ranges: vec![(0, 50)], + ..MusicMetadata::default() + }; + let item = Item::new( + INodeNo(43), + INodeNo::ROOT, + "test_flac".to_string(), + test_file.clone(), + PathBuf::from("test_flac"), + FileType::File, + attrs.clone(), + Some(mm.clone()), + ); + + let attr = file_to_attr(&item); + let expected_virtual_size = mm.virtual_size(attrs.size); + assert_eq!(attr.size, expected_virtual_size); + } + + #[test] + fn file_to_attr_kind_directory() { + let tmp = tempfile::tempdir().unwrap(); + let metadata = std::fs::metadata(tmp.path()).unwrap(); + let item = Item::new( + INodeNo(44), + INodeNo::ROOT, + "test_dir".to_string(), + tmp.path().to_path_buf(), + PathBuf::from("test_dir"), + FileType::Directory, + FileAttrs::from(&metadata), + None, + ); + + let attr = file_to_attr(&item); + assert_eq!(attr.kind, fuser::FileType::Directory); + } + + #[test] + fn file_to_attr_kind_file() { + let tmp = tempfile::tempdir().unwrap(); + let metadata = std::fs::metadata(tmp.path()).unwrap(); + let item = Item::new( + INodeNo(45), + INodeNo::ROOT, + "test_file".to_string(), + tmp.path().to_path_buf(), + PathBuf::from("test_file"), + FileType::File, + FileAttrs::from(&metadata), + None, + ); + + let attr = file_to_attr(&item); + assert_eq!(attr.kind, fuser::FileType::RegularFile); + } +} diff --git a/src/virtual_dirs.rs b/src/virtual_dirs.rs index db5ce17..aefb1b7 100644 --- a/src/virtual_dirs.rs +++ b/src/virtual_dirs.rs @@ -10,6 +10,7 @@ use twox_hash::XxHash64; use crate::db::entities::Model; use crate::item::{FileType, Item}; +use crate::origins::attrs::FileAttrs; pub fn virtual_inode(path: &str) -> INodeNo { let mut hasher = XxHash64::with_seed(5678); @@ -71,7 +72,7 @@ pub fn ensure_virtual_dirs( source.to_path_buf(), PathBuf::from(¤t_path), FileType::Directory, - fs::metadata(source).unwrap(), + FileAttrs::from(&fs::metadata(source).unwrap()), None, ); map.insert(virt_ino, virt_item); @@ -227,7 +228,7 @@ mod tests { source.to_path_buf(), PathBuf::from("real_file"), FileType::File, - fs::metadata(source).unwrap(), + FileAttrs::from(&fs::metadata(source).unwrap()), None, ); snapshot.insert(real_ino, real_item); diff --git a/tests/file_io_test.rs b/tests/file_io_test.rs index 44ff46d..f86bf0c 100644 --- a/tests/file_io_test.rs +++ b/tests/file_io_test.rs @@ -1,6 +1,11 @@ use std::io::Write; +use std::path::PathBuf; -use musicfs::origins::local::file_io::assemble_flac_read; +use musicfs::origins::local::file_io::{assemble_flac_read, read_bytes_at}; + +fn file_reader(path: PathBuf) -> impl Fn(u64, usize) -> std::io::Result> { + return move |offset, len| read_bytes_at(&path, offset, len); +} #[test] fn assemble_flac_read_picture_header_region() { @@ -8,15 +13,16 @@ fn assemble_flac_read_picture_header_region() { let data: Vec = (0..100).collect(); f.write_all(&data).unwrap(); f.flush().unwrap(); - let path = f.path(); + let path = f.path().to_path_buf(); let header = b"ABCD"; let pic_hdr = vec![0x86u8, 0x00, 0x00, 0x05]; let pic_ranges = [(10u64, 5u64)]; let real_audio_start = 50u64; + let reader = file_reader(path); let result = assemble_flac_read( - path, + &reader, header, &[pic_hdr.clone()], &pic_ranges, @@ -35,15 +41,16 @@ fn assemble_flac_read_picture_data_region() { let data: Vec = (0..100).collect(); f.write_all(&data).unwrap(); f.flush().unwrap(); - let path = f.path(); + let path = f.path().to_path_buf(); let header = b"ABCD"; let pic_hdr = vec![0x86u8, 0x00, 0x00, 0x05]; let pic_ranges = [(10u64, 5u64)]; let real_audio_start = 50u64; + let reader = file_reader(path); let result = assemble_flac_read( - path, + &reader, header, &[pic_hdr.clone()], &pic_ranges, @@ -62,12 +69,13 @@ fn assemble_flac_read_full_virtual_layout() { let data: Vec = (0..200).map(|i| i as u8).collect(); f.write_all(&data).unwrap(); f.flush().unwrap(); - let path = f.path(); + let path = f.path().to_path_buf(); let header = vec![0xAA; 8]; let real_audio_start = 100u64; - let result = assemble_flac_read(path, &header, &[], &[], real_audio_start, 0, 18).unwrap(); + let reader = file_reader(path); + let result = assemble_flac_read(&reader, &header, &[], &[], real_audio_start, 0, 18).unwrap(); let mut expected = vec![0xAA; 8]; expected.extend_from_slice(&(100..110).map(|i| i as u8).collect::>()); diff --git a/tests/snapshot_test.rs b/tests/snapshot_test.rs index 8dcdd93..e8d9f16 100644 --- a/tests/snapshot_test.rs +++ b/tests/snapshot_test.rs @@ -59,25 +59,21 @@ fn build_snapshot_nested_dirs() { let dest = tempfile::tempdir().unwrap(); fs::create_dir(source.path().join("subdir")).unwrap(); - let mut f = fs::File::create(source.path().join("subdir").join("file.txt")).unwrap(); + let nested_path = source.path().join("subdir").join("file.txt"); + let mut f = fs::File::create(&nested_path).unwrap(); f.write_all(b"nested content").unwrap(); let snapshot = build_snapshot(source.path(), dest.path()).unwrap(); assert!(snapshot.contains_key(&INodeNo::ROOT)); - let subdir_entry = snapshot - .values() - .find(|item| item.name == "subdir" && item.file_type == FileType::Directory) - .expect("subdir not found"); - let file_entry = snapshot .values() .find(|item| item.name == "file.txt" && item.file_type == FileType::File) .expect("file.txt not found"); - assert_eq!(subdir_entry.parent_inode, INodeNo::ROOT); assert_eq!(file_entry.parent_inode, INodeNo::ROOT); + assert_eq!(file_entry.original_path, nested_path); } #[test]