diff --git a/markbase-core/src/server.rs b/markbase-core/src/server.rs index 61ff343..01946b8 100644 --- a/markbase-core/src/server.rs +++ b/markbase-core/src/server.rs @@ -1008,28 +1008,11 @@ fn extract_and_register_archive( let hex = format!("{:x}", hash); let file_uuid = hex[0..32].to_string(); -// Register file (file_registry table) + // Register file conn.execute( - "INSERT INTO file_registry (file_uuid, original_name, file_size, file_type, registered_at) + "INSERT INTO file_registry (file_uuid, sha256, file_size, mime_type, registered_at) VALUES (?1, ?2, ?3, ?4, ?5)", - rusqlite::params![&file_uuid, &filename, file_size, "", now], - )?; - - // Add file location - conn.execute( - "INSERT OR IGNORE INTO file_locations (file_uuid, location, created_at) - VALUES (?1, ?2, ?3)", - rusqlite::params![&file_uuid, &file_path_str, now], - )?; - - // Add file node (with sha256) - let uuid_str = uuid::Uuid::new_v4().to_string().replace('-', ""); - let node_id = format!("node-{}", &uuid_str[0..8]); - - conn.execute( - "INSERT INTO file_nodes (node_id, label, file_uuid, sha256, node_type, file_size, created_at, updated_at) - VALUES (?1, ?2, ?3, ?4, 'file', ?5, ?6, ?7)", - rusqlite::params![&node_id, &filename, &file_uuid, &file_hash, file_size, now, now], + rusqlite::params![&file_uuid, &file_hash, file_size, "", now], )?; // Add file location @@ -1228,48 +1211,24 @@ async fn upload_file( .unwrap() .as_secs() as i64; -conn.execute( - "INSERT INTO file_registry (file_uuid, original_name, file_size, file_type, registered_at) + conn.execute( + "INSERT INTO file_registry (file_uuid, sha256, file_size, mime_type, registered_at) VALUES (?1, ?2, ?3, ?4, ?5)", rusqlite::params![ - &file_uuid_clone, - &filename_clone, - file_size, - "", // file_type (optional) - now - ], - )?; - -// Add file location - conn.execute( - "INSERT OR IGNORE INTO file_locations (file_uuid, location, added_at) - VALUES (?1, ?2, ?3)", - rusqlite::params![&file_uuid, &file_path_str, now], - )?; - - let uuid_str = uuid::Uuid::new_v4().to_string().replace('-', ""); - let node_id = format!("node-{}", &uuid_str[0..8]); - - conn.execute( - "INSERT INTO file_nodes (node_id, label, file_uuid, sha256, node_type, file_size, created_at, updated_at) - VALUES (?1, ?2, ?3, ?4, 'file', ?5, ?6, ?7)", - rusqlite::params![ - &node_id, - &filename_clone, &file_uuid_clone, &file_hash_clone, file_size, - now, + "", // mime_type (optional) now ], )?; -// Add file location (file_locations table) -conn.execute( - "INSERT OR IGNORE INTO file_locations (file_uuid, location, added_at) - VALUES (?1, ?2, ?3)", - rusqlite::params![&file_uuid_clone, &file_path_clone, now], - )?; + // Add file location + conn.execute( + "INSERT OR IGNORE INTO file_locations (file_uuid, location, created_at) + VALUES (?1, ?2, ?3)", + rusqlite::params![&file_uuid_clone, &file_path_clone, now], + )?; let uuid_str = uuid::Uuid::new_v4().to_string().replace('-', ""); let node_id = format!("node-{}", &uuid_str[0..8]); diff --git a/markbase-nfs/Cargo.toml b/markbase-nfs/Cargo.toml new file mode 100644 index 0000000..dd47deb --- /dev/null +++ b/markbase-nfs/Cargo.toml @@ -0,0 +1,17 @@ +[package] +name = "markbase-nfs" +version = "0.1.0" +edition = "2021" + +[dependencies] +anyhow = "1" +async-trait = "0.1" +clap = { version = "4", features = ["derive"] } +filetree = { path = "../filetree" } +rusqlite = { version = "0.32", features = ["bundled"] } +serde = { version = "1", features = ["derive"] } +serde_json = "1" +tokio = { version = "1", features = ["full"] } +vfs = "0.12" +nfsserve = "0.11" + diff --git a/markbase-nfs/src/lib.rs b/markbase-nfs/src/lib.rs new file mode 100644 index 0000000..1afa718 --- /dev/null +++ b/markbase-nfs/src/lib.rs @@ -0,0 +1,4 @@ +pub mod nfs; + +pub use nfs::markbase_fs::MarkBaseFS; +pub use nfs::backend::MarkBaseNFSBackend; diff --git a/markbase-nfs/src/main.rs b/markbase-nfs/src/main.rs new file mode 100644 index 0000000..1590984 --- /dev/null +++ b/markbase-nfs/src/main.rs @@ -0,0 +1,39 @@ +use clap::Parser; +use markbase_nfs::nfs::run_nfs_server; +use std::path::PathBuf; + +#[derive(Parser)] +#[command(name = "markbase-nfs")] +#[command(about = "MarkBase NFS Server", long_about = None)] +struct Cli { + /// User ID (database name) + #[arg(short, long)] + user: String, + + /// Database path + #[arg(short, long, default_value = "data/users")] + data_dir: String, + + /// NFS server port + #[arg(short, long, default_value_t = 11111)] + port: u16, +} + +fn main() -> anyhow::Result<()> { + let cli = Cli::parse(); + + let db_path = PathBuf::from(&cli.data_dir).join(format!("{}.sqlite", cli.user)); + + if !db_path.exists() { + eprintln!("Database not found: {}", db_path.display()); + eprintln!("Please create database first using markbase-core"); + return Err(anyhow::anyhow!("Database not found")); + } + + eprintln!("Starting MarkBase NFS server..."); + eprintln!("User: {}", cli.user); + eprintln!("Database: {}", db_path.display()); + eprintln!("Port: {}", cli.port); + + run_nfs_server(cli.user, db_path, cli.port) +} \ No newline at end of file diff --git a/markbase-nfs/src/nfs/backend.rs b/markbase-nfs/src/nfs/backend.rs new file mode 100644 index 0000000..b15897d --- /dev/null +++ b/markbase-nfs/src/nfs/backend.rs @@ -0,0 +1,343 @@ +use std::collections::HashMap; +use std::sync::Mutex; +use std::time::SystemTime; + +use async_trait::async_trait; +use nfsserve::nfs::*; +use nfsserve::vfs::{DirEntry, NFSFileSystem, ReadDirResult, VFSCapabilities}; +use rusqlite::Connection; + +use crate::nfs::markbase_fs::MarkBaseFS; + +pub struct MarkBaseNFSBackend { + fs: MarkBaseFS, + id_map: Mutex>, // fileid -> node_id + reverse_map: Mutex>, // node_id -> fileid + next_id: Mutex, +} + +impl MarkBaseNFSBackend { + pub fn new(user_id: String, db_path: std::path::PathBuf) -> anyhow::Result { + let fs = MarkBaseFS::new(user_id, db_path)?; + + Ok(MarkBaseNFSBackend { + fs, + id_map: Mutex::new(HashMap::new()), + reverse_map: Mutex::new(HashMap::new()), + next_id: Mutex::new(2), // 1 is root + }) + } + + fn allocate_id(&self, node_id: &str) -> u64 { + let mut reverse_map = self.reverse_map.lock().unwrap(); + + if let Some(id) = reverse_map.get(node_id) { + return *id; + } + + let mut next_id = self.next_id.lock().unwrap(); + let id = *next_id; + *next_id += 1; + + reverse_map.insert(node_id.to_string(), id); + self.id_map.lock().unwrap().insert(id, node_id.to_string()); + + id + } + + fn get_node_id(&self, fileid: u64) -> Option { + self.id_map.lock().unwrap().get(&fileid).cloned() + } + + fn get_fileid_from_node(&self, node_id: &str) -> u64 { + self.reverse_map.lock().unwrap().get(node_id).copied().unwrap_or_else(|| { + self.allocate_id(node_id) + }) + } +} + +#[async_trait] +impl NFSFileSystem for MarkBaseNFSBackend { + fn capabilities(&self) -> VFSCapabilities { + VFSCapabilities::ReadOnly + } + + fn root_dir(&self) -> fileid3 { + 1 + } + + async fn lookup(&self, dirid: fileid3, filename: &filename3) -> Result { + let dir_node_id = if dirid == 1 { + "root".to_string() + } else { + self.get_node_id(dirid) + .ok_or(nfsstat3::NFS3ERR_STALE)? + }; + + let filename_str = String::from_utf8_lossy(filename).to_string(); + + let conn = self.fs.conn.lock().map_err(|_| nfsstat3::NFS3ERR_SERVERFAULT)?; + + let query = if dir_node_id == "root" { + "SELECT node_id FROM file_nodes WHERE parent_id IS NULL AND label = ?1" + } else { + "SELECT node_id FROM file_nodes WHERE parent_id = ?1 AND label = ?2" + }; + + let node_id: String = if dir_node_id == "root" { + conn.query_row(&query, [&filename_str], |row| row.get(0)) + .map_err(|_| nfsstat3::NFS3ERR_NOENT)? + } else { + conn.query_row(&query, [dir_node_id, filename_str], |row| row.get(0)) + .map_err(|_| nfsstat3::NFS3ERR_NOENT)? + }; + + Ok(self.get_fileid_from_node(&node_id)) + } + + async fn getattr(&self, id: fileid3) -> Result { + if id == 1 { + return Ok(fattr3 { + ftype: ftype3::NF3DIR, + mode: 0o755, + nlink: 1, + uid: 0, + gid: 0, + size: 0, + used: 0, + rdev: specdata3 { specdata1: 0, specdata2: 0 }, + fsid: 0, + fileid: 1, + atime: nfstime3 { seconds: 0, nseconds: 0 }, + mtime: nfstime3 { seconds: 0, nseconds: 0 }, + ctime: nfstime3 { seconds: 0, nseconds: 0 }, + }); + } + + let node_id = self.get_node_id(id).ok_or(nfsstat3::NFS3ERR_STALE)?; + + let conn = self.fs.conn.lock().map_err(|_| nfsstat3::NFS3ERR_SERVERFAULT)?; + + let (node_type, file_size): (String, i64) = conn + .query_row( + "SELECT node_type, file_size FROM file_nodes WHERE node_id = ?1", + [&node_id], + |row| Ok((row.get::<_, String>(0)?, row.get::<_, i64>(1)?)) + ) + .map_err(|_| nfsstat3::NFS3ERR_NOENT)?; + + let type_ = if node_type == "folder" { + ftype3::NF3DIR + } else { + ftype3::NF3REG + }; + + let now = SystemTime::now() + .duration_since(SystemTime::UNIX_EPOCH) + .unwrap() + .as_secs(); + + Ok(fattr3 { + ftype: type_, + mode: if node_type == "folder" { 0o755 } else { 0o644 }, + nlink: 1, + uid: 0, + gid: 0, + size: file_size as u64, + used: file_size as u64, + rdev: specdata3 { specdata1: 0, specdata2: 0 }, + fsid: 0, + fileid: id, + atime: nfstime3 { seconds: now as u32, nseconds: 0 }, + mtime: nfstime3 { seconds: now as u32, nseconds: 0 }, + ctime: nfstime3 { seconds: now as u32, nseconds: 0 }, + }) + } + + async fn setattr(&self, _id: fileid3, _setattr: sattr3) -> Result { + Err(nfsstat3::NFS3ERR_ROFS) + } + + async fn read(&self, id: fileid3, offset: u64, count: u32) -> Result<(Vec, bool), nfsstat3> { + let node_id = self.get_node_id(id).ok_or(nfsstat3::NFS3ERR_STALE)?; + + let conn = self.fs.conn.lock().map_err(|_| nfsstat3::NFS3ERR_SERVERFAULT)?; + + let aliases_json: String = conn + .query_row( + "SELECT aliases_json FROM file_nodes WHERE node_id = ?1", + [&node_id], + |row| row.get(0) + ) + .map_err(|_| nfsstat3::NFS3ERR_NOENT)?; + + let aliases: serde_json::Value = serde_json::from_str(&aliases_json) + .map_err(|_| nfsstat3::NFS3ERR_SERVERFAULT)?; + + let file_path = aliases["path"].as_str().ok_or(nfsstat3::NFS3ERR_NOENT)?; + + let file_data = std::fs::read(file_path).map_err(|_| nfsstat3::NFS3ERR_IO)?; + + let file_size = file_data.len() as u64; + let start = offset.min(file_size) as usize; + let end = (offset + count as u64).min(file_size) as usize; + + let data = file_data[start..end].to_vec(); + let eof = end >= file_size as usize; + + Ok((data, eof)) + } + + async fn write(&self, _id: fileid3, _offset: u64, _data: &[u8]) -> Result { + Err(nfsstat3::NFS3ERR_ROFS) + } + + async fn create(&self, _dirid: fileid3, _filename: &filename3, _attr: sattr3) -> Result<(fileid3, fattr3), nfsstat3> { + Err(nfsstat3::NFS3ERR_ROFS) + } + + async fn create_exclusive(&self, _dirid: fileid3, _filename: &filename3) -> Result { + Err(nfsstat3::NFS3ERR_ROFS) + } + + async fn mkdir(&self, _dirid: fileid3, _dirname: &filename3) -> Result<(fileid3, fattr3), nfsstat3> { + Err(nfsstat3::NFS3ERR_ROFS) + } + + async fn remove(&self, _dirid: fileid3, _filename: &filename3) -> Result<(), nfsstat3> { + Err(nfsstat3::NFS3ERR_ROFS) + } + + async fn rename( + &self, + _from_dirid: fileid3, + _from_filename: &filename3, + _to_dirid: fileid3, + _to_filename: &filename3, + ) -> Result<(), nfsstat3> { + Err(nfsstat3::NFS3ERR_ROFS) + } + + async fn readdir( + &self, + dirid: fileid3, + start_after: fileid3, + max_entries: usize, + ) -> Result { + let dir_node_id = if dirid == 1 { + "root".to_string() + } else { + self.get_node_id(dirid) + .ok_or(nfsstat3::NFS3ERR_STALE)? + }; + + let conn = self.fs.conn.lock().map_err(|_| nfsstat3::NFS3ERR_SERVERFAULT)?; + + let query = if dir_node_id == "root" { + "SELECT node_id, label, node_type, file_size FROM file_nodes WHERE parent_id IS NULL" + } else { + "SELECT node_id, label, node_type, file_size FROM file_nodes WHERE parent_id = ?1" + }; + + let mut stmt = conn.prepare(&query).map_err(|_| nfsstat3::NFS3ERR_SERVERFAULT)?; + + let rows: Vec<(String, String, String, Option)> = if dir_node_id == "root" { + stmt.query_map([], |row| { + row.get::<_, String>(0) + .and_then(|node_id| { + row.get::<_, String>(1) + .and_then(|label| { + row.get::<_, String>(2) + .and_then(|node_type| { + row.get::<_, Option>(3) + .map(|file_size| (node_id, label, node_type, file_size)) + }) + }) + }) + }) + .map_err(|_| nfsstat3::NFS3ERR_SERVERFAULT)? + .collect::, _>>() + .map_err(|_| nfsstat3::NFS3ERR_SERVERFAULT)? + } else { + stmt.query_map([&dir_node_id.as_str()], |row| { + row.get::<_, String>(0) + .and_then(|node_id| { + row.get::<_, String>(1) + .and_then(|label| { + row.get::<_, String>(2) + .and_then(|node_type| { + row.get::<_, Option>(3) + .map(|file_size| (node_id, label, node_type, file_size)) + }) + }) + }) + }) + .map_err(|_| nfsstat3::NFS3ERR_SERVERFAULT)? + .collect::, _>>() + .map_err(|_| nfsstat3::NFS3ERR_SERVERFAULT)? + }; + + let mut entries: Vec = Vec::new(); + let mut started = start_after == 0; + + for row in rows { + let (node_id, label, node_type, file_size_opt) = row; + let file_size = file_size_opt.unwrap_or(0); + let fileid = self.get_fileid_from_node(&node_id); + + if !started { + if fileid == start_after { + started = true; + continue; + } + } + + if started && entries.len() < max_entries { + let attr = fattr3 { + ftype: if node_type == "folder" { + ftype3::NF3DIR + } else { + ftype3::NF3REG + }, + mode: if node_type == "folder" { 0o755 } else { 0o644 }, + nlink: 1, + uid: 0, + gid: 0, + size: file_size as u64, + used: file_size as u64, + rdev: specdata3 { specdata1: 0, specdata2: 0 }, + fsid: 0, + fileid, + atime: nfstime3 { seconds: 0, nseconds: 0 }, + mtime: nfstime3 { seconds: 0, nseconds: 0 }, + ctime: nfstime3 { seconds: 0, nseconds: 0 }, + }; + + entries.push(DirEntry { + fileid, + name: nfsserve::nfs::nfsstring(label.into_bytes()), + attr, + }); + } + } + + Ok(ReadDirResult { + entries, + end: true, + }) + } + + async fn symlink( + &self, + _dirid: fileid3, + _linkname: &filename3, + _symlink: &nfspath3, + _attr: &sattr3, + ) -> Result<(fileid3, fattr3), nfsstat3> { + Err(nfsstat3::NFS3ERR_ROFS) + } + + async fn readlink(&self, _id: fileid3) -> Result { + Err(nfsstat3::NFS3ERR_NOTSUPP) + } +} \ No newline at end of file diff --git a/markbase-nfs/src/nfs/markbase_fs.rs b/markbase-nfs/src/nfs/markbase_fs.rs index 518b09e..ac95300 100644 --- a/markbase-nfs/src/nfs/markbase_fs.rs +++ b/markbase-nfs/src/nfs/markbase_fs.rs @@ -4,8 +4,8 @@ use std::path::PathBuf; use std::sync::Mutex; use rusqlite::Connection; -use vfs::{FileSystem, VfsMetadata, VfsResult, VfsFileType, SeekAndRead, SeekAndWrite}; use vfs::error::VfsErrorKind; +use vfs::{FileSystem, SeekAndRead, SeekAndWrite, VfsFileType, VfsMetadata, VfsResult}; fn rusqlite_to_io_error(e: rusqlite::Error) -> io::Error { io::Error::new(io::ErrorKind::Other, e.to_string()) @@ -15,7 +15,7 @@ fn rusqlite_to_io_error(e: rusqlite::Error) -> io::Error { pub struct MarkBaseFS { user_id: String, db_path: PathBuf, - conn: Mutex, + pub conn: Mutex, path_cache: Mutex>, } @@ -32,7 +32,7 @@ impl MarkBaseFS { pub fn new(user_id: String, db_path: PathBuf) -> VfsResult { let conn = Connection::open(&db_path) .map_err(|e| VfsErrorKind::IoError(rusqlite_to_io_error(e)))?; - + Ok(MarkBaseFS { user_id, db_path, @@ -40,7 +40,7 @@ impl MarkBaseFS { path_cache: Mutex::new(HashMap::new()), }) } - + fn resolve_path(&self, path: &str) -> VfsResult { if path == "" || path == "/" { return Ok(FileNode { @@ -52,15 +52,17 @@ impl MarkBaseFS { file_size: None, }); } - - let conn = self.conn.lock() + + let conn = self + .conn + .lock() .map_err(|_| VfsErrorKind::Other("Failed to lock connection".to_string()))?; - + let parts: Vec<&str> = path.split('/').filter(|s| !s.is_empty()).collect(); - + let mut current_parent: Option = None; let mut current_node: Option = None; - + for part in parts { let query = if current_parent.is_none() { "SELECT node_id, label, node_type, parent_id, aliases_json, file_size @@ -71,10 +73,11 @@ impl MarkBaseFS { FROM file_nodes WHERE parent_id = ?1 AND label = ?2" }; - - let mut stmt = conn.prepare(query) + + let mut stmt = conn + .prepare(query) .map_err(|e| VfsErrorKind::IoError(rusqlite_to_io_error(e)))?; - + let node = if current_parent.is_none() { stmt.query_row([part], |row| { Ok(FileNode { @@ -85,7 +88,8 @@ impl MarkBaseFS { aliases_json: row.get(4)?, file_size: row.get(5)?, }) - }).map_err(|e| rusqlite_to_io_error(e)) + }) + .map_err(|e| rusqlite_to_io_error(e)) } else { let part_str = part.to_string(); stmt.query_row([current_parent.clone().unwrap(), part_str], |row| { @@ -97,9 +101,10 @@ impl MarkBaseFS { aliases_json: row.get(4)?, file_size: row.get(5)?, }) - }).map_err(|e| rusqlite_to_io_error(e)) + }) + .map_err(|e| rusqlite_to_io_error(e)) }; - + match node { Ok(n) => { current_parent = Some(n.node_id.clone()); @@ -108,32 +113,35 @@ impl MarkBaseFS { Err(_) => return Err(VfsErrorKind::FileNotFound.into()), } } - + current_node.ok_or(VfsErrorKind::FileNotFound.into()) } } impl FileSystem for MarkBaseFS { fn read_dir(&self, path: &str) -> VfsResult + Send>> { - let conn = self.conn.lock() + let conn = self + .conn + .lock() .map_err(|_| VfsErrorKind::Other("Failed to lock connection".to_string()))?; - + let parent_id = if path == "" || path == "/" { None } else { let node = self.resolve_path(path)?; Some(node.node_id) }; - + let query = if parent_id.is_none() { "SELECT label FROM file_nodes WHERE parent_id IS NULL" } else { "SELECT label FROM file_nodes WHERE parent_id = ?1" }; - - let mut stmt = conn.prepare(query) + + let mut stmt = conn + .prepare(query) .map_err(|e| VfsErrorKind::IoError(rusqlite_to_io_error(e)))?; - + let children: Vec = if parent_id.is_none() { stmt.query_map([], |row| row.get::<_, String>(0)) .map_err(|e| rusqlite_to_io_error(e))? @@ -145,51 +153,52 @@ impl FileSystem for MarkBaseFS { .collect::, _>>() .map_err(|e| rusqlite_to_io_error(e))? }; - + Ok(Box::new(children.into_iter())) } - + fn create_dir(&self, _path: &str) -> VfsResult<()> { Err(VfsErrorKind::NotSupported.into()) } - + fn open_file(&self, path: &str) -> VfsResult> { let node = self.resolve_path(path)?; - + if node.node_type != "file" { return Err(VfsErrorKind::InvalidPath.into()); } - + let aliases_json = node.aliases_json.ok_or(VfsErrorKind::FileNotFound)?; - let aliases: serde_json::Value = serde_json::from_str(&aliases_json) - .map_err(|e| VfsErrorKind::IoError(io::Error::new(io::ErrorKind::Other, e.to_string())))?; - + let aliases: serde_json::Value = serde_json::from_str(&aliases_json).map_err(|e| { + VfsErrorKind::IoError(io::Error::new(io::ErrorKind::Other, e.to_string())) + })?; + let file_path = aliases["path"].as_str().ok_or(VfsErrorKind::FileNotFound)?; - + let file = std::fs::File::open(file_path)?; - + Ok(Box::new(file)) } - + fn create_file(&self, _path: &str) -> VfsResult> { Err(VfsErrorKind::NotSupported.into()) } - + fn append_file(&self, _path: &str) -> VfsResult> { Err(VfsErrorKind::NotSupported.into()) } - + fn metadata(&self, path: &str) -> VfsResult { let node = self.resolve_path(path)?; - + let file_type = if node.node_type == "folder" { VfsFileType::Directory } else { VfsFileType::File }; - + let len = node.file_size.unwrap_or(0) as u64; - + Ok(VfsMetadata { file_type, len, @@ -198,18 +207,18 @@ impl FileSystem for MarkBaseFS { accessed: None, }) } - + fn exists(&self, path: &str) -> VfsResult { match self.resolve_path(path) { Ok(_) => Ok(true), Err(_) => Ok(false), } } - + fn remove_file(&self, _path: &str) -> VfsResult<()> { Err(VfsErrorKind::NotSupported.into()) } - + fn remove_dir(&self, _path: &str) -> VfsResult<()> { Err(VfsErrorKind::NotSupported.into()) } @@ -219,7 +228,7 @@ impl FileSystem for MarkBaseFS { mod tests { use super::*; use vfs::FileSystem; - + #[test] fn test_markbase_fs_creation() { let fs = MarkBaseFS::new( @@ -228,16 +237,17 @@ mod tests { ); assert!(fs.is_ok()); } - + #[test] fn test_resolve_root() { let fs = MarkBaseFS::new( "warren".to_string(), PathBuf::from("data/users/warren.sqlite"), - ).unwrap(); - + ) + .unwrap(); + let node = fs.resolve_path(""); assert!(node.is_ok()); assert_eq!(node.unwrap().node_type, "folder"); } -} \ No newline at end of file +} diff --git a/markbase-nfs/src/nfs/mod.rs b/markbase-nfs/src/nfs/mod.rs index 9df2a11..167a9de 100644 --- a/markbase-nfs/src/nfs/mod.rs +++ b/markbase-nfs/src/nfs/mod.rs @@ -1,3 +1,7 @@ pub mod markbase_fs; +pub mod backend; +pub mod server; -pub use markbase_fs::MarkBaseFS; \ No newline at end of file +pub use markbase_fs::MarkBaseFS; +pub use backend::MarkBaseNFSBackend; +pub use server::{start_nfs_server, run_nfs_server}; diff --git a/markbase-nfs/src/nfs/server.rs b/markbase-nfs/src/nfs/server.rs new file mode 100644 index 0000000..cd9ffb7 --- /dev/null +++ b/markbase-nfs/src/nfs/server.rs @@ -0,0 +1,44 @@ +use std::path::PathBuf; +use std::sync::Arc; + +use nfsserve::tcp::{NFSTcpListener, NFSTcp}; +use tokio::signal; + +use crate::nfs::backend::MarkBaseNFSBackend; + +pub async fn start_nfs_server(user_id: String, db_path: PathBuf, port: u16) -> anyhow::Result<()> { + let backend = MarkBaseNFSBackend::new(user_id, db_path)?; + + let bind_addr = format!("127.0.0.1:{}", port); + + let mut listener = NFSTcpListener::bind(&bind_addr, backend) + .await + .map_err(|e| anyhow::anyhow!("Failed to bind NFS server: {}", e))?; + + listener.with_export_name("markbase"); + + let listen_port = listener.get_listen_port(); + eprintln!("[NFS] MarkBase NFS server started on port {}", listen_port); + eprintln!("[NFS] Mount command (Mac):"); + eprintln!("[NFS] mkdir /tmp/markbase_mount"); + eprintln!("[NFS] mount_nfs -o nolocks,vers=3,tcp,port={},mountport={} localhost:/markbase /tmp/markbase_mount", listen_port, listen_port); + eprintln!("[NFS] Mount command (Linux):"); + eprintln!("[NFS] mkdir /tmp/markbase_mount"); + eprintln!("[NFS] mount.nfs -o user,noacl,nolock,vers=3,tcp,port={},mountport={} localhost:/markbase /tmp/markbase_mount", listen_port, listen_port); + + tokio::select! { + _ = listener.handle_forever() => { + eprintln!("[NFS] Server stopped"); + } + _ = signal::ctrl_c() => { + eprintln!("[NFS] Received Ctrl+C, shutting down..."); + } + } + + Ok(()) +} + +pub fn run_nfs_server(user_id: String, db_path: PathBuf, port: u16) -> anyhow::Result<()> { + tokio::runtime::Runtime::new()? + .block_on(start_nfs_server(user_id, db_path, port)) +} \ No newline at end of file