From 9349d29200fd34630abbc6fbd4d3a773b9eee85e Mon Sep 17 00:00:00 2001 From: Alexander Date: Sat, 20 Jun 2026 13:36:15 +0200 Subject: [PATCH] Start implementing LocalSource --- Cargo.lock | 127 ++++++++++++++++++++++++++ Cargo.toml | 1 + src/json_filesystem.rs | 175 +++++++++++++++++++++++++++++++++++ src/local.rs | 186 +++++++++++++++++++++++++++++++++++++ src/main.rs | 202 ++++------------------------------------- 5 files changed, 507 insertions(+), 184 deletions(-) create mode 100644 src/json_filesystem.rs create mode 100644 src/local.rs diff --git a/Cargo.lock b/Cargo.lock index 2d3355e..e37e0c9 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2,6 +2,56 @@ # It is not intended for manual editing. version = 4 +[[package]] +name = "anstream" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "824a212faf96e9acacdbd09febd34438f8f711fb84e09a8916013cd7815ca28d" +dependencies = [ + "anstyle", + "anstyle-parse", + "anstyle-query", + "anstyle-wincon", + "colorchoice", + "is_terminal_polyfill", + "utf8parse", +] + +[[package]] +name = "anstyle" +version = "1.0.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "940b3a0ca603d1eade50a4846a2afffd5ef57a9feac2c0e2ec2e14f9ead76000" + +[[package]] +name = "anstyle-parse" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "52ce7f38b242319f7cabaa6813055467063ecdc9d355bbb4ce0c68908cd8130e" +dependencies = [ + "utf8parse", +] + +[[package]] +name = "anstyle-query" +version = "1.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "40c48f72fd53cd289104fc64099abca73db4166ad86ea0b4341abe65af83dadc" +dependencies = [ + "windows-sys", +] + +[[package]] +name = "anstyle-wincon" +version = "3.0.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "291e6a250ff86cd4a820112fb8898808a366d8f9f58ce16d1f538353ad55747d" +dependencies = [ + "anstyle", + "once_cell_polyfill", + "windows-sys", +] + [[package]] name = "autocfg" version = "1.5.1" @@ -35,6 +85,52 @@ version = "0.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "613afe47fcd5fac7ccf1db93babcb082c5994d996f20b8b159f2ad1658eb5724" +[[package]] +name = "clap" +version = "4.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1ddb117e43bbf7dacf0a4190fef4d345b9bad68dfc649cb349e7d17d28428e51" +dependencies = [ + "clap_builder", + "clap_derive", +] + +[[package]] +name = "clap_builder" +version = "4.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "714a53001bf66416adb0e2ef5ac857140e7dc3a0c48fb28b2f10762fc4b5069f" +dependencies = [ + "anstream", + "anstyle", + "clap_lex", + "strsim", +] + +[[package]] +name = "clap_derive" +version = "4.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f2ce8604710f6733aa641a2b3731eaa1e8b3d9973d5e3565da11800813f997a9" +dependencies = [ + "heck", + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "clap_lex" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c8d4a3bb8b1e0c1050499d1815f5ab16d04f0959b233085fb31653fbfc9d98f9" + +[[package]] +name = "colorchoice" +version = "1.0.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1d07550c9036bf2ae0c684c4297d503f838287c83c53686d05370d0e139ae570" + [[package]] name = "ctrlc" version = "3.5.2" @@ -96,6 +192,12 @@ version = "0.17.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ed5909b6e89a2db4456e54cd5f673791d7eca6732202bbf2a9cc504fe2f9b84a" +[[package]] +name = "heck" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea" + [[package]] name = "indexmap" version = "2.14.0" @@ -106,6 +208,12 @@ dependencies = [ "hashbrown", ] +[[package]] +name = "is_terminal_polyfill" +version = "1.70.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a6cb138bb79a146c1bd460005623e142ef0181e3d0219cb493e02f7d08a35695" + [[package]] name = "itoa" version = "1.0.18" @@ -152,6 +260,7 @@ dependencies = [ name = "musicfs" version = "0.1.0" dependencies = [ + "clap", "ctrlc", "fuser", "libc", @@ -227,6 +336,12 @@ version = "4.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ef25abbcd74fb2609453eb695bd2f860d389e457f67dc17cafc8b8cbc89d0c33" +[[package]] +name = "once_cell_polyfill" +version = "1.70.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "384b8ab6d37215f3c5301a95a4accb5d64aa607f1fcb26a11b5303878451b4fe" + [[package]] name = "page_size" version = "0.6.0" @@ -388,6 +503,12 @@ version = "1.15.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8ed6a63f02c8539c91a8685a86f4099661ba3da017932f6ebbea6de3f0fa7c90" +[[package]] +name = "strsim" +version = "0.11.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7da8b5736845d9f2fcb837ea5d9e2628564b3b043a70948a3f0b778838c5fb4f" + [[package]] name = "syn" version = "2.0.118" @@ -454,6 +575,12 @@ version = "1.0.24" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e6e4313cd5fcd3dad5cafa179702e2b244f760991f45397d14d4ebf38247da75" +[[package]] +name = "utf8parse" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "06abde3611657adf66d383f00b093d7faecc7fa57071cce2578660c9f1010821" + [[package]] name = "winapi" version = "0.3.9" diff --git a/Cargo.toml b/Cargo.toml index 85b30fa..cbee114 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -4,6 +4,7 @@ version = "0.1.0" edition = "2024" [dependencies] +clap = { version = "4.6.1", features = ["derive"] } ctrlc = "3.5.2" fuser = "0.17.0" libc = "0.2.186" diff --git a/src/json_filesystem.rs b/src/json_filesystem.rs new file mode 100644 index 0000000..1ff25fe --- /dev/null +++ b/src/json_filesystem.rs @@ -0,0 +1,175 @@ +use fuser::{ + Errno, FileAttr, FileType, Filesystem, Generation, INodeNo, ReplyAttr, ReplyDirectory, Request, +}; +use serde_json::{Map, Value}; +use std::collections::BTreeMap; +use std::path::Path; +use std::time::{Duration, SystemTime}; + +pub struct JsonFilesystem { + tree: Map, + attrs: BTreeMap, + inodes: BTreeMap, +} + +impl JsonFilesystem { + pub fn new(tree: &Map) -> JsonFilesystem { + let mut attrs = BTreeMap::new(); + let mut inodes = BTreeMap::new(); + let ts = SystemTime::now(); + let attr = FileAttr { + ino: INodeNo::ROOT, + size: 0, + blocks: 0, + atime: ts, + mtime: ts, + ctime: ts, + crtime: ts, + kind: FileType::Directory, + perm: 0o755, + nlink: 0, + uid: 0, + gid: 0, + rdev: 0, + blksize: 0, + flags: 0, + }; + + attrs.insert(INodeNo(1), attr); + inodes.insert("/".to_string(), INodeNo(1)); + + for (i, (key, value)) in tree.iter().enumerate() { + let attr = FileAttr { + ino: INodeNo(i as u64 + 2), + size: value.to_string().len() as u64, + blocks: 0, + atime: ts, + mtime: ts, + ctime: ts, + crtime: ts, + kind: FileType::RegularFile, + perm: 0o644, + nlink: 0, + uid: 0, + gid: 0, + rdev: 0, + blksize: 0, + flags: 0, + }; + + attrs.insert(attr.ino, attr); + inodes.insert(key.clone(), attr.ino); + } + + return JsonFilesystem { + tree: tree.clone(), + attrs: attrs, + inodes: inodes, + }; + } +} + +impl Filesystem for JsonFilesystem { + // For stat function like `stat /path/to/fuse` + fn getattr( + &self, + _req: &Request, + ino: fuser::INodeNo, + fh: Option, + reply: ReplyAttr, + ) { + println!("getattr(ino={})", ino); + + match self.attrs.get(&ino) { + Some(attr) => { + let ttl = Duration::new(1, 0); + reply.attr(&ttl, attr); + } + None => reply.error(Errno::ENOENT), + }; + } + + fn readdir( + &self, + _req: &Request, + ino: INodeNo, + fh: fuser::FileHandle, + offset: u64, + mut reply: ReplyDirectory, + ) { + println!("readdir(ino={}, fh={}, offset={})", ino, fh, offset); + + if ino == INodeNo::ROOT { + if offset == 0 { + let _ = reply.add(INodeNo::ROOT, 0, FileType::Directory, &Path::new(".")); + let _ = reply.add(INodeNo::ROOT, 1, FileType::Directory, &Path::new("..")); + for (i, key) in self.tree.keys().enumerate() { + let inode: u64 = 2 + i as u64; + let offset: u64 = 2 + i as u64; + let _ = reply.add( + INodeNo(inode), + offset, + FileType::RegularFile, + &Path::new(key), + ); + } + } + reply.ok(); + } else { + reply.error(Errno::ENOSYS); + } + } + + fn lookup( + &self, + _req: &Request, + parent: INodeNo, + name: &std::ffi::OsStr, + reply: fuser::ReplyEntry, + ) { + println!("lookup(parent={}, name={})", parent, name.display()); + + let inode = match self.inodes.get(name.to_str().unwrap()) { + Some(inode) => inode, + None => { + reply.error(Errno::ENOENT); + return; + } + }; + + match self.attrs.get(inode) { + Some(attr) => { + let ttl = Duration::new(1, 0); + reply.entry(&ttl, attr, Generation(0)); + } + None => reply.error(Errno::ENOENT), + }; + } + + fn read( + &self, + _req: &Request, + ino: INodeNo, + fh: fuser::FileHandle, + offset: u64, + size: u32, + flags: fuser::OpenFlags, + lock_owner: Option, + reply: fuser::ReplyData, + ) { + println!( + "read(ino={}, fh={}, offset={}, size={})", + ino, fh, offset, size + ); + + for (key, &inode) in self.inodes.iter() { + if inode == ino { + let value = self.tree.get(key).unwrap(); + reply.data(value.to_string().as_bytes()); + return; + } + } + + reply.error(Errno::ENOENT); + } +} diff --git a/src/local.rs b/src/local.rs new file mode 100644 index 0000000..a083e78 --- /dev/null +++ b/src/local.rs @@ -0,0 +1,186 @@ +use std::{ + collections::BTreeMap, + fs, io, + os::unix::fs::{MetadataExt, PermissionsExt}, + path::{Path, PathBuf}, + time::{Duration, SystemTime}, +}; + +use fuser::{Errno, FileAttr, Filesystem, INodeNo}; + +#[derive(PartialEq, Eq, Copy, Clone, Debug)] +enum FileType { + Directory, + File, +} + +#[derive(Debug)] +struct LocalItem { + inode: INodeNo, + name: String, + original_path: PathBuf, + local_path: PathBuf, + file_type: FileType, + metadata: fs::Metadata, +} + +#[derive(Debug)] +pub struct LocalOrigin { + source: PathBuf, + destination: PathBuf, + files: BTreeMap, +} + +impl LocalOrigin { + pub fn new(source: String, destination: String) -> Result { + println!("Initializing the LocalOrigin"); + + let mut map: BTreeMap = BTreeMap::new(); + + 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(), + }; + map.insert(INodeNo::ROOT, local_root); + + match LocalOrigin::read_source(PathBuf::from(&source), Path::new(&destination), &mut 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), + } + } + + fn read_source( + source: PathBuf, + destination: &Path, + 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 + }; + + let mut local_path: PathBuf = PathBuf::new(); + local_path.push(destination); + local_path.push(name.clone()); + + let local_item = LocalItem { + inode: INodeNo(metadata.ino()), + name: name, + original_path: item_path.clone(), + local_path: local_path, + file_type: file_type, + metadata, + }; + let inode = local_item.inode; + + 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), + } + } + } + Err(err) => return Err(err), + }; + + return 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() + .map_err(|err| { + println!("Failed to get atime for: {}", &local_file.name); + err + }) + .unwrap(); + let mtime = metadata + .modified() + .map_err(|err| { + println!("Failed to get atime for: {}", &local_file.name); + err + }) + .unwrap(); + + let crtime = metadata + .created() + .map_err(|err| { + println!("Failed to get atime for: {}", &local_file.name); + err + }) + .unwrap(); + let kind = match local_file.file_type { + FileType::Directory => fuser::FileType::Directory, + FileType::File => fuser::FileType::RegularFile, + }; + + return FileAttr { + ino: local_file.inode, + size: metadata.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? + }; + } +} + +impl Filesystem for LocalOrigin { + fn getattr( + &self, + _req: &fuser::Request, + ino: INodeNo, + fh: Option, + reply: fuser::ReplyAttr, + ) { + println!("getattr(ino={})", ino); + match self.files.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), + } + } +} diff --git a/src/main.rs b/src/main.rs index aebeb7c..ed43d98 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,196 +1,26 @@ -use fuser::{ - Errno, FileAttr, FileType, Filesystem, Generation, INodeNo, ReplyAttr, ReplyDirectory, Request, -}; -use serde_json::{Map, Value, json}; -use std::collections::BTreeMap; +use clap::Parser; +use serde_json::json; use std::env; -use std::path::Path; use std::sync::Arc; use std::sync::atomic::{AtomicBool, Ordering}; -use std::time::{Duration, SystemTime}; +use std::time::Duration; -struct JsonFilesystem { - tree: Map, - attrs: BTreeMap, - inodes: BTreeMap, -} +mod json_filesystem; +mod local; -impl JsonFilesystem { - fn new(tree: &Map) -> JsonFilesystem { - let mut attrs = BTreeMap::new(); - let mut inodes = BTreeMap::new(); - let ts = SystemTime::now(); - let attr = FileAttr { - ino: INodeNo::ROOT, - size: 0, - blocks: 0, - atime: ts, - mtime: ts, - ctime: ts, - crtime: ts, - kind: FileType::Directory, - perm: 0o755, - nlink: 0, - uid: 0, - gid: 0, - rdev: 0, - blksize: 0, - flags: 0, - }; +#[derive(Parser, Debug)] +#[command(version, about, long_about = None)] +struct Args { + #[arg(short, long, required = true)] + mountpoint: String, - attrs.insert(INodeNo(1), attr); - inodes.insert("/".to_string(), INodeNo(1)); - - for (i, (key, value)) in tree.iter().enumerate() { - let attr = FileAttr { - ino: INodeNo(i as u64 + 2), - size: value.to_string().len() as u64, - blocks: 0, - atime: ts, - mtime: ts, - ctime: ts, - crtime: ts, - kind: FileType::RegularFile, - perm: 0o644, - nlink: 0, - uid: 0, - gid: 0, - rdev: 0, - blksize: 0, - flags: 0, - }; - - attrs.insert(attr.ino, attr); - inodes.insert(key.clone(), attr.ino); - } - - return JsonFilesystem { - tree: tree.clone(), - attrs: attrs, - inodes: inodes, - }; - } -} - -impl Filesystem for JsonFilesystem { - // For stat function like `stat /path/to/fuse` - fn getattr( - &self, - _req: &Request, - ino: fuser::INodeNo, - fh: Option, - reply: ReplyAttr, - ) { - println!("getattr(ino={})", ino); - - match self.attrs.get(&ino) { - Some(attr) => { - let ttl = Duration::new(1, 0); - reply.attr(&ttl, attr); - } - None => reply.error(Errno::ENOENT), - }; - } - - fn readdir( - &self, - _req: &Request, - ino: INodeNo, - fh: fuser::FileHandle, - offset: u64, - mut reply: ReplyDirectory, - ) { - println!("readdir(ino={}, fh={}, offset={})", ino, fh, offset); - - if ino == INodeNo::ROOT { - if offset == 0 { - let _ = reply.add(INodeNo::ROOT, 0, FileType::Directory, &Path::new(".")); - let _ = reply.add(INodeNo::ROOT, 1, FileType::Directory, &Path::new("..")); - for (i, key) in self.tree.keys().enumerate() { - let inode: u64 = 2 + i as u64; - let offset: u64 = 2 + i as u64; - let _ = reply.add( - INodeNo(inode), - offset, - FileType::RegularFile, - &Path::new(key), - ); - } - } - reply.ok(); - } else { - reply.error(Errno::ENOSYS); - } - } - - fn lookup( - &self, - _req: &Request, - parent: INodeNo, - name: &std::ffi::OsStr, - reply: fuser::ReplyEntry, - ) { - println!("lookup(parent={}, name={})", parent, name.display()); - - let inode = match self.inodes.get(name.to_str().unwrap()) { - Some(inode) => inode, - None => { - reply.error(Errno::ENOENT); - return; - } - }; - - match self.attrs.get(inode) { - Some(attr) => { - let ttl = Duration::new(1, 0); - reply.entry(&ttl, attr, Generation(0)); - } - None => reply.error(Errno::ENOENT), - }; - } - - fn read( - &self, - _req: &Request, - ino: INodeNo, - fh: fuser::FileHandle, - offset: u64, - size: u32, - flags: fuser::OpenFlags, - lock_owner: Option, - reply: fuser::ReplyData, - ) { - println!( - "read(ino={}, fh={}, offset={}, size={})", - ino, fh, offset, size - ); - - for (key, &inode) in self.inodes.iter() { - if inode == ino { - let value = self.tree.get(key).unwrap(); - reply.data(value.to_string().as_bytes()); - return; - } - } - - reply.error(Errno::ENOENT); - } + #[arg(short, long, required = true)] + source: String, } fn main() { - let data = json!({ - "foo": "bar", - "answer": 42, - }); - let tree = data.as_object().unwrap(); - let fs = JsonFilesystem::new(tree); - let mountpoint = match env::args().nth(1) { - Some(path) => path, - None => { - println!("Usage: {} ", env::args().nth(0).unwrap()); - return; - } - }; + let args = Args::parse(); + let mountpoint = args.mountpoint; let running = Arc::new(AtomicBool::new(true)); let r_clone = running.clone(); @@ -201,6 +31,10 @@ fn main() { }) .expect("Error setting Ctrl+C handler"); + // TODO don't think clone is necessary + // TODO unwrap might not be safe and better would be to handle it properly + let fs = local::LocalOrigin::new(args.source, mountpoint.clone()).unwrap(); + let cfg = fuser::Config::default(); let session = fuser::spawn_mount2(fs, &mountpoint, &cfg);