NFS Module完成
This commit is contained in:
@@ -1008,28 +1008,11 @@ fn extract_and_register_archive(
|
|||||||
let hex = format!("{:x}", hash);
|
let hex = format!("{:x}", hash);
|
||||||
let file_uuid = hex[0..32].to_string();
|
let file_uuid = hex[0..32].to_string();
|
||||||
|
|
||||||
// Register file (file_registry table)
|
// Register file
|
||||||
conn.execute(
|
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)",
|
VALUES (?1, ?2, ?3, ?4, ?5)",
|
||||||
rusqlite::params![&file_uuid, &filename, file_size, "", now],
|
rusqlite::params![&file_uuid, &file_hash, 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],
|
|
||||||
)?;
|
)?;
|
||||||
|
|
||||||
// Add file location
|
// Add file location
|
||||||
@@ -1228,48 +1211,24 @@ async fn upload_file(
|
|||||||
.unwrap()
|
.unwrap()
|
||||||
.as_secs() as i64;
|
.as_secs() as i64;
|
||||||
|
|
||||||
conn.execute(
|
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)",
|
VALUES (?1, ?2, ?3, ?4, ?5)",
|
||||||
rusqlite::params![
|
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_uuid_clone,
|
||||||
&file_hash_clone,
|
&file_hash_clone,
|
||||||
file_size,
|
file_size,
|
||||||
now,
|
"", // mime_type (optional)
|
||||||
now
|
now
|
||||||
],
|
],
|
||||||
)?;
|
)?;
|
||||||
|
|
||||||
// Add file location (file_locations table)
|
// Add file location
|
||||||
conn.execute(
|
conn.execute(
|
||||||
"INSERT OR IGNORE INTO file_locations (file_uuid, location, added_at)
|
"INSERT OR IGNORE INTO file_locations (file_uuid, location, created_at)
|
||||||
VALUES (?1, ?2, ?3)",
|
VALUES (?1, ?2, ?3)",
|
||||||
rusqlite::params![&file_uuid_clone, &file_path_clone, now],
|
rusqlite::params![&file_uuid_clone, &file_path_clone, now],
|
||||||
)?;
|
)?;
|
||||||
|
|
||||||
let uuid_str = uuid::Uuid::new_v4().to_string().replace('-', "");
|
let uuid_str = uuid::Uuid::new_v4().to_string().replace('-', "");
|
||||||
let node_id = format!("node-{}", &uuid_str[0..8]);
|
let node_id = format!("node-{}", &uuid_str[0..8]);
|
||||||
|
|||||||
17
markbase-nfs/Cargo.toml
Normal file
17
markbase-nfs/Cargo.toml
Normal file
@@ -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"
|
||||||
|
|
||||||
4
markbase-nfs/src/lib.rs
Normal file
4
markbase-nfs/src/lib.rs
Normal file
@@ -0,0 +1,4 @@
|
|||||||
|
pub mod nfs;
|
||||||
|
|
||||||
|
pub use nfs::markbase_fs::MarkBaseFS;
|
||||||
|
pub use nfs::backend::MarkBaseNFSBackend;
|
||||||
39
markbase-nfs/src/main.rs
Normal file
39
markbase-nfs/src/main.rs
Normal file
@@ -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)
|
||||||
|
}
|
||||||
343
markbase-nfs/src/nfs/backend.rs
Normal file
343
markbase-nfs/src/nfs/backend.rs
Normal file
@@ -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<HashMap<u64, String>>, // fileid -> node_id
|
||||||
|
reverse_map: Mutex<HashMap<String, u64>>, // node_id -> fileid
|
||||||
|
next_id: Mutex<u64>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl MarkBaseNFSBackend {
|
||||||
|
pub fn new(user_id: String, db_path: std::path::PathBuf) -> anyhow::Result<Self> {
|
||||||
|
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<String> {
|
||||||
|
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<fileid3, nfsstat3> {
|
||||||
|
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<fattr3, nfsstat3> {
|
||||||
|
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<fattr3, nfsstat3> {
|
||||||
|
Err(nfsstat3::NFS3ERR_ROFS)
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn read(&self, id: fileid3, offset: u64, count: u32) -> Result<(Vec<u8>, 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<fattr3, nfsstat3> {
|
||||||
|
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<fileid3, nfsstat3> {
|
||||||
|
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<ReadDirResult, nfsstat3> {
|
||||||
|
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<i64>)> = 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<i64>>(3)
|
||||||
|
.map(|file_size| (node_id, label, node_type, file_size))
|
||||||
|
})
|
||||||
|
})
|
||||||
|
})
|
||||||
|
})
|
||||||
|
.map_err(|_| nfsstat3::NFS3ERR_SERVERFAULT)?
|
||||||
|
.collect::<Result<Vec<_>, _>>()
|
||||||
|
.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<i64>>(3)
|
||||||
|
.map(|file_size| (node_id, label, node_type, file_size))
|
||||||
|
})
|
||||||
|
})
|
||||||
|
})
|
||||||
|
})
|
||||||
|
.map_err(|_| nfsstat3::NFS3ERR_SERVERFAULT)?
|
||||||
|
.collect::<Result<Vec<_>, _>>()
|
||||||
|
.map_err(|_| nfsstat3::NFS3ERR_SERVERFAULT)?
|
||||||
|
};
|
||||||
|
|
||||||
|
let mut entries: Vec<DirEntry> = 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<nfspath3, nfsstat3> {
|
||||||
|
Err(nfsstat3::NFS3ERR_NOTSUPP)
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -4,8 +4,8 @@ use std::path::PathBuf;
|
|||||||
use std::sync::Mutex;
|
use std::sync::Mutex;
|
||||||
|
|
||||||
use rusqlite::Connection;
|
use rusqlite::Connection;
|
||||||
use vfs::{FileSystem, VfsMetadata, VfsResult, VfsFileType, SeekAndRead, SeekAndWrite};
|
|
||||||
use vfs::error::VfsErrorKind;
|
use vfs::error::VfsErrorKind;
|
||||||
|
use vfs::{FileSystem, SeekAndRead, SeekAndWrite, VfsFileType, VfsMetadata, VfsResult};
|
||||||
|
|
||||||
fn rusqlite_to_io_error(e: rusqlite::Error) -> io::Error {
|
fn rusqlite_to_io_error(e: rusqlite::Error) -> io::Error {
|
||||||
io::Error::new(io::ErrorKind::Other, e.to_string())
|
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 {
|
pub struct MarkBaseFS {
|
||||||
user_id: String,
|
user_id: String,
|
||||||
db_path: PathBuf,
|
db_path: PathBuf,
|
||||||
conn: Mutex<Connection>,
|
pub conn: Mutex<Connection>,
|
||||||
path_cache: Mutex<HashMap<String, String>>,
|
path_cache: Mutex<HashMap<String, String>>,
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -32,7 +32,7 @@ impl MarkBaseFS {
|
|||||||
pub fn new(user_id: String, db_path: PathBuf) -> VfsResult<Self> {
|
pub fn new(user_id: String, db_path: PathBuf) -> VfsResult<Self> {
|
||||||
let conn = Connection::open(&db_path)
|
let conn = Connection::open(&db_path)
|
||||||
.map_err(|e| VfsErrorKind::IoError(rusqlite_to_io_error(e)))?;
|
.map_err(|e| VfsErrorKind::IoError(rusqlite_to_io_error(e)))?;
|
||||||
|
|
||||||
Ok(MarkBaseFS {
|
Ok(MarkBaseFS {
|
||||||
user_id,
|
user_id,
|
||||||
db_path,
|
db_path,
|
||||||
@@ -40,7 +40,7 @@ impl MarkBaseFS {
|
|||||||
path_cache: Mutex::new(HashMap::new()),
|
path_cache: Mutex::new(HashMap::new()),
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
fn resolve_path(&self, path: &str) -> VfsResult<FileNode> {
|
fn resolve_path(&self, path: &str) -> VfsResult<FileNode> {
|
||||||
if path == "" || path == "/" {
|
if path == "" || path == "/" {
|
||||||
return Ok(FileNode {
|
return Ok(FileNode {
|
||||||
@@ -52,15 +52,17 @@ impl MarkBaseFS {
|
|||||||
file_size: None,
|
file_size: None,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
let conn = self.conn.lock()
|
let conn = self
|
||||||
|
.conn
|
||||||
|
.lock()
|
||||||
.map_err(|_| VfsErrorKind::Other("Failed to lock connection".to_string()))?;
|
.map_err(|_| VfsErrorKind::Other("Failed to lock connection".to_string()))?;
|
||||||
|
|
||||||
let parts: Vec<&str> = path.split('/').filter(|s| !s.is_empty()).collect();
|
let parts: Vec<&str> = path.split('/').filter(|s| !s.is_empty()).collect();
|
||||||
|
|
||||||
let mut current_parent: Option<String> = None;
|
let mut current_parent: Option<String> = None;
|
||||||
let mut current_node: Option<FileNode> = None;
|
let mut current_node: Option<FileNode> = None;
|
||||||
|
|
||||||
for part in parts {
|
for part in parts {
|
||||||
let query = if current_parent.is_none() {
|
let query = if current_parent.is_none() {
|
||||||
"SELECT node_id, label, node_type, parent_id, aliases_json, file_size
|
"SELECT node_id, label, node_type, parent_id, aliases_json, file_size
|
||||||
@@ -71,10 +73,11 @@ impl MarkBaseFS {
|
|||||||
FROM file_nodes
|
FROM file_nodes
|
||||||
WHERE parent_id = ?1 AND label = ?2"
|
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)))?;
|
.map_err(|e| VfsErrorKind::IoError(rusqlite_to_io_error(e)))?;
|
||||||
|
|
||||||
let node = if current_parent.is_none() {
|
let node = if current_parent.is_none() {
|
||||||
stmt.query_row([part], |row| {
|
stmt.query_row([part], |row| {
|
||||||
Ok(FileNode {
|
Ok(FileNode {
|
||||||
@@ -85,7 +88,8 @@ impl MarkBaseFS {
|
|||||||
aliases_json: row.get(4)?,
|
aliases_json: row.get(4)?,
|
||||||
file_size: row.get(5)?,
|
file_size: row.get(5)?,
|
||||||
})
|
})
|
||||||
}).map_err(|e| rusqlite_to_io_error(e))
|
})
|
||||||
|
.map_err(|e| rusqlite_to_io_error(e))
|
||||||
} else {
|
} else {
|
||||||
let part_str = part.to_string();
|
let part_str = part.to_string();
|
||||||
stmt.query_row([current_parent.clone().unwrap(), part_str], |row| {
|
stmt.query_row([current_parent.clone().unwrap(), part_str], |row| {
|
||||||
@@ -97,9 +101,10 @@ impl MarkBaseFS {
|
|||||||
aliases_json: row.get(4)?,
|
aliases_json: row.get(4)?,
|
||||||
file_size: row.get(5)?,
|
file_size: row.get(5)?,
|
||||||
})
|
})
|
||||||
}).map_err(|e| rusqlite_to_io_error(e))
|
})
|
||||||
|
.map_err(|e| rusqlite_to_io_error(e))
|
||||||
};
|
};
|
||||||
|
|
||||||
match node {
|
match node {
|
||||||
Ok(n) => {
|
Ok(n) => {
|
||||||
current_parent = Some(n.node_id.clone());
|
current_parent = Some(n.node_id.clone());
|
||||||
@@ -108,32 +113,35 @@ impl MarkBaseFS {
|
|||||||
Err(_) => return Err(VfsErrorKind::FileNotFound.into()),
|
Err(_) => return Err(VfsErrorKind::FileNotFound.into()),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
current_node.ok_or(VfsErrorKind::FileNotFound.into())
|
current_node.ok_or(VfsErrorKind::FileNotFound.into())
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl FileSystem for MarkBaseFS {
|
impl FileSystem for MarkBaseFS {
|
||||||
fn read_dir(&self, path: &str) -> VfsResult<Box<dyn Iterator<Item = String> + Send>> {
|
fn read_dir(&self, path: &str) -> VfsResult<Box<dyn Iterator<Item = String> + Send>> {
|
||||||
let conn = self.conn.lock()
|
let conn = self
|
||||||
|
.conn
|
||||||
|
.lock()
|
||||||
.map_err(|_| VfsErrorKind::Other("Failed to lock connection".to_string()))?;
|
.map_err(|_| VfsErrorKind::Other("Failed to lock connection".to_string()))?;
|
||||||
|
|
||||||
let parent_id = if path == "" || path == "/" {
|
let parent_id = if path == "" || path == "/" {
|
||||||
None
|
None
|
||||||
} else {
|
} else {
|
||||||
let node = self.resolve_path(path)?;
|
let node = self.resolve_path(path)?;
|
||||||
Some(node.node_id)
|
Some(node.node_id)
|
||||||
};
|
};
|
||||||
|
|
||||||
let query = if parent_id.is_none() {
|
let query = if parent_id.is_none() {
|
||||||
"SELECT label FROM file_nodes WHERE parent_id IS NULL"
|
"SELECT label FROM file_nodes WHERE parent_id IS NULL"
|
||||||
} else {
|
} else {
|
||||||
"SELECT label FROM file_nodes WHERE parent_id = ?1"
|
"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)))?;
|
.map_err(|e| VfsErrorKind::IoError(rusqlite_to_io_error(e)))?;
|
||||||
|
|
||||||
let children: Vec<String> = if parent_id.is_none() {
|
let children: Vec<String> = if parent_id.is_none() {
|
||||||
stmt.query_map([], |row| row.get::<_, String>(0))
|
stmt.query_map([], |row| row.get::<_, String>(0))
|
||||||
.map_err(|e| rusqlite_to_io_error(e))?
|
.map_err(|e| rusqlite_to_io_error(e))?
|
||||||
@@ -145,51 +153,52 @@ impl FileSystem for MarkBaseFS {
|
|||||||
.collect::<Result<Vec<_>, _>>()
|
.collect::<Result<Vec<_>, _>>()
|
||||||
.map_err(|e| rusqlite_to_io_error(e))?
|
.map_err(|e| rusqlite_to_io_error(e))?
|
||||||
};
|
};
|
||||||
|
|
||||||
Ok(Box::new(children.into_iter()))
|
Ok(Box::new(children.into_iter()))
|
||||||
}
|
}
|
||||||
|
|
||||||
fn create_dir(&self, _path: &str) -> VfsResult<()> {
|
fn create_dir(&self, _path: &str) -> VfsResult<()> {
|
||||||
Err(VfsErrorKind::NotSupported.into())
|
Err(VfsErrorKind::NotSupported.into())
|
||||||
}
|
}
|
||||||
|
|
||||||
fn open_file(&self, path: &str) -> VfsResult<Box<dyn SeekAndRead + Send>> {
|
fn open_file(&self, path: &str) -> VfsResult<Box<dyn SeekAndRead + Send>> {
|
||||||
let node = self.resolve_path(path)?;
|
let node = self.resolve_path(path)?;
|
||||||
|
|
||||||
if node.node_type != "file" {
|
if node.node_type != "file" {
|
||||||
return Err(VfsErrorKind::InvalidPath.into());
|
return Err(VfsErrorKind::InvalidPath.into());
|
||||||
}
|
}
|
||||||
|
|
||||||
let aliases_json = node.aliases_json.ok_or(VfsErrorKind::FileNotFound)?;
|
let aliases_json = node.aliases_json.ok_or(VfsErrorKind::FileNotFound)?;
|
||||||
let aliases: serde_json::Value = serde_json::from_str(&aliases_json)
|
let aliases: serde_json::Value = serde_json::from_str(&aliases_json).map_err(|e| {
|
||||||
.map_err(|e| VfsErrorKind::IoError(io::Error::new(io::ErrorKind::Other, e.to_string())))?;
|
VfsErrorKind::IoError(io::Error::new(io::ErrorKind::Other, e.to_string()))
|
||||||
|
})?;
|
||||||
|
|
||||||
let file_path = aliases["path"].as_str().ok_or(VfsErrorKind::FileNotFound)?;
|
let file_path = aliases["path"].as_str().ok_or(VfsErrorKind::FileNotFound)?;
|
||||||
|
|
||||||
let file = std::fs::File::open(file_path)?;
|
let file = std::fs::File::open(file_path)?;
|
||||||
|
|
||||||
Ok(Box::new(file))
|
Ok(Box::new(file))
|
||||||
}
|
}
|
||||||
|
|
||||||
fn create_file(&self, _path: &str) -> VfsResult<Box<dyn SeekAndWrite + Send>> {
|
fn create_file(&self, _path: &str) -> VfsResult<Box<dyn SeekAndWrite + Send>> {
|
||||||
Err(VfsErrorKind::NotSupported.into())
|
Err(VfsErrorKind::NotSupported.into())
|
||||||
}
|
}
|
||||||
|
|
||||||
fn append_file(&self, _path: &str) -> VfsResult<Box<dyn SeekAndWrite + Send>> {
|
fn append_file(&self, _path: &str) -> VfsResult<Box<dyn SeekAndWrite + Send>> {
|
||||||
Err(VfsErrorKind::NotSupported.into())
|
Err(VfsErrorKind::NotSupported.into())
|
||||||
}
|
}
|
||||||
|
|
||||||
fn metadata(&self, path: &str) -> VfsResult<VfsMetadata> {
|
fn metadata(&self, path: &str) -> VfsResult<VfsMetadata> {
|
||||||
let node = self.resolve_path(path)?;
|
let node = self.resolve_path(path)?;
|
||||||
|
|
||||||
let file_type = if node.node_type == "folder" {
|
let file_type = if node.node_type == "folder" {
|
||||||
VfsFileType::Directory
|
VfsFileType::Directory
|
||||||
} else {
|
} else {
|
||||||
VfsFileType::File
|
VfsFileType::File
|
||||||
};
|
};
|
||||||
|
|
||||||
let len = node.file_size.unwrap_or(0) as u64;
|
let len = node.file_size.unwrap_or(0) as u64;
|
||||||
|
|
||||||
Ok(VfsMetadata {
|
Ok(VfsMetadata {
|
||||||
file_type,
|
file_type,
|
||||||
len,
|
len,
|
||||||
@@ -198,18 +207,18 @@ impl FileSystem for MarkBaseFS {
|
|||||||
accessed: None,
|
accessed: None,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
fn exists(&self, path: &str) -> VfsResult<bool> {
|
fn exists(&self, path: &str) -> VfsResult<bool> {
|
||||||
match self.resolve_path(path) {
|
match self.resolve_path(path) {
|
||||||
Ok(_) => Ok(true),
|
Ok(_) => Ok(true),
|
||||||
Err(_) => Ok(false),
|
Err(_) => Ok(false),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn remove_file(&self, _path: &str) -> VfsResult<()> {
|
fn remove_file(&self, _path: &str) -> VfsResult<()> {
|
||||||
Err(VfsErrorKind::NotSupported.into())
|
Err(VfsErrorKind::NotSupported.into())
|
||||||
}
|
}
|
||||||
|
|
||||||
fn remove_dir(&self, _path: &str) -> VfsResult<()> {
|
fn remove_dir(&self, _path: &str) -> VfsResult<()> {
|
||||||
Err(VfsErrorKind::NotSupported.into())
|
Err(VfsErrorKind::NotSupported.into())
|
||||||
}
|
}
|
||||||
@@ -219,7 +228,7 @@ impl FileSystem for MarkBaseFS {
|
|||||||
mod tests {
|
mod tests {
|
||||||
use super::*;
|
use super::*;
|
||||||
use vfs::FileSystem;
|
use vfs::FileSystem;
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn test_markbase_fs_creation() {
|
fn test_markbase_fs_creation() {
|
||||||
let fs = MarkBaseFS::new(
|
let fs = MarkBaseFS::new(
|
||||||
@@ -228,16 +237,17 @@ mod tests {
|
|||||||
);
|
);
|
||||||
assert!(fs.is_ok());
|
assert!(fs.is_ok());
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn test_resolve_root() {
|
fn test_resolve_root() {
|
||||||
let fs = MarkBaseFS::new(
|
let fs = MarkBaseFS::new(
|
||||||
"warren".to_string(),
|
"warren".to_string(),
|
||||||
PathBuf::from("data/users/warren.sqlite"),
|
PathBuf::from("data/users/warren.sqlite"),
|
||||||
).unwrap();
|
)
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
let node = fs.resolve_path("");
|
let node = fs.resolve_path("");
|
||||||
assert!(node.is_ok());
|
assert!(node.is_ok());
|
||||||
assert_eq!(node.unwrap().node_type, "folder");
|
assert_eq!(node.unwrap().node_type, "folder");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,3 +1,7 @@
|
|||||||
pub mod markbase_fs;
|
pub mod markbase_fs;
|
||||||
|
pub mod backend;
|
||||||
|
pub mod server;
|
||||||
|
|
||||||
pub use markbase_fs::MarkBaseFS;
|
pub use markbase_fs::MarkBaseFS;
|
||||||
|
pub use backend::MarkBaseNFSBackend;
|
||||||
|
pub use server::{start_nfs_server, run_nfs_server};
|
||||||
|
|||||||
44
markbase-nfs/src/nfs/server.rs
Normal file
44
markbase-nfs/src/nfs/server.rs
Normal file
@@ -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))
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user