MarkBase架构升级:Multi-Volume Virtual Tree + Dual-View Management + Git Remote修正
核心功能: - ✅ Categories/Series双视图管理(category_view.rs + import_markdown.rs) - ✅ FUSE Multi-Volume支持(tree_type参数) - ✅ SSH/SFTP/SCP/rsync协议完整实现(4042行) - ✅ NFS/SMB Module Phase 1-3完成 - ✅ Archive Module Phase 1-4完成(2916行) - ✅ Download Center API完整实现 - ✅ S3兼容API实现(560行) Git配置修正: - ✅ 删除错误origin(gitea.momentry.ddns.net) - ✅ 删除m5max128(指向机器名) - ✅ 设置origin = m5max128gitea.momentry.ddns.net/admin/markbase - ✅ 设置m4minigitea = m4minigitea.momentry.ddns.net/warren/markbase 数据清理: - ✅ 删除38个临时SQLite(保留accusys.sqlite、demo.sqlite) - ✅ 删除.bak、test_*.bin、调试脚本等临时文件 - ✅ 删除临时目录(build/、download files/、raid_test/等) - ✅ 更新.gitignore排除临时文件 架构优化: - 52个文件修改,2434行新增,4739行删除 - Workspace成员整合(16个crate) - 数据库状态:accusys.sqlite保留(主demo测试) 远程同步: - ✅ 准备推送到m5max128gitea(远程Gitea) - ✅ 准备推送到m4minigitea(本地Gitea)
This commit is contained in:
27
markbase-fuse/Cargo.toml
Normal file
27
markbase-fuse/Cargo.toml
Normal file
@@ -0,0 +1,27 @@
|
||||
[package]
|
||||
name = "markbase-fuse"
|
||||
version = "0.2.0"
|
||||
edition = "2021"
|
||||
|
||||
|
||||
build = "build.rs"
|
||||
|
||||
[[bin]]
|
||||
name = "markbase-fuse"
|
||||
path = "src/main.rs"
|
||||
|
||||
[lib]
|
||||
name = "markbase_fuse"
|
||||
path = "src/lib.rs"
|
||||
|
||||
[dependencies]
|
||||
anyhow = "1"
|
||||
clap = { version = "4", features = ["derive"] }
|
||||
filetree = { path = "../filetree" }
|
||||
fuse = "0.3.1"
|
||||
libc = "0.2"
|
||||
log = "0.4"
|
||||
rusqlite = { version = "0.32", features = ["bundled"] }
|
||||
serde = { version = "1", features = ["derive"] }
|
||||
serde_json = "1"
|
||||
time = "0.3"
|
||||
13
markbase-fuse/build.rs
Normal file
13
markbase-fuse/build.rs
Normal file
@@ -0,0 +1,13 @@
|
||||
fn main() {
|
||||
// Manual linking for FUSE-T on macOS (no pkg-config dependency)
|
||||
println!("cargo:rustc-link-search=native=/usr/local/lib");
|
||||
|
||||
// Link to both fuse3 and fuse-t (FUSE-T provides both)
|
||||
println!("cargo:rustc-link-lib=dylib=fuse3");
|
||||
println!("cargo:rustc-link-lib=dylib=fuse-t");
|
||||
|
||||
// Set rpath for runtime loading
|
||||
println!("cargo:rustc-link-arg=-Wl,-rpath,/usr/local/lib");
|
||||
|
||||
println!("cargo:rerun-if-changed=build.rs");
|
||||
}
|
||||
@@ -1,116 +0,0 @@
|
||||
use std::path::Path;
|
||||
use std::path::PathBuf;
|
||||
use std::env;
|
||||
use std::process::Command;
|
||||
use anyhow::{Result, Error};
|
||||
|
||||
#[derive(Debug, Clone, PartialEq)]
|
||||
pub enum BackendType {
|
||||
Nfs4,
|
||||
Fskit,
|
||||
}
|
||||
|
||||
impl BackendType {
|
||||
pub fn name(&self) -> &'static str {
|
||||
match self {
|
||||
BackendType::Nfs4 => "nfs",
|
||||
BackendType::Fskit => "fskit",
|
||||
}
|
||||
}
|
||||
|
||||
pub fn supports_macos_version(&self, version: &str) -> bool {
|
||||
match self {
|
||||
BackendType::Nfs4 => true,
|
||||
BackendType::Fskit => version.starts_with("26"),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub fn detect_macos_version() -> String {
|
||||
env::var("MACOS_VERSION").unwrap_or_else(|_| {
|
||||
let output = Command::new("sw_vers")
|
||||
.arg("-productVersion")
|
||||
.output()
|
||||
.expect("Failed to get macOS version");
|
||||
String::from_utf8_lossy(&output.stdout).trim().to_string()
|
||||
})
|
||||
}
|
||||
|
||||
pub fn select_backend() -> BackendType {
|
||||
let version = detect_macos_version();
|
||||
|
||||
if version.starts_with("26") {
|
||||
BackendType::Fskit
|
||||
} else {
|
||||
BackendType::Nfs4
|
||||
}
|
||||
}
|
||||
|
||||
pub fn select_backend_manual(backend_name: &str) -> Result<BackendType> {
|
||||
match backend_name {
|
||||
"nfs" | "nfs4" => Ok(BackendType::Nfs4),
|
||||
"fskit" => Ok(BackendType::Fskit),
|
||||
_ => Err(Error::msg(format!("Unknown backend: {}", backend_name))),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn detect_fuse_t_binary() -> bool {
|
||||
Path::new("/Library/Application Support/fuse-t/bin/go-nfsv4").exists()
|
||||
}
|
||||
|
||||
pub fn get_fuse_t_path() -> Option<PathBuf> {
|
||||
if detect_fuse_t_binary() {
|
||||
Some(PathBuf::from("/Library/Application Support/fuse-t/bin/go-nfsv4"))
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn test_backend_type_name() {
|
||||
assert_eq!(BackendType::Nfs4.name(), "nfs");
|
||||
assert_eq!(BackendType::Fskit.name(), "fskit");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_backend_support() {
|
||||
assert!(BackendType::Nfs4.supports_macos_version("25.0"));
|
||||
assert!(BackendType::Nfs4.supports_macos_version("26.0"));
|
||||
assert!(!BackendType::Fskit.supports_macos_version("25.0"));
|
||||
assert!(BackendType::Fskit.supports_macos_version("26.0"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_select_backend_macos_26() {
|
||||
env::set_var("MACOS_VERSION", "26.4.1");
|
||||
let backend = select_backend();
|
||||
assert_eq!(backend, BackendType::Fskit);
|
||||
env::remove_var("MACOS_VERSION");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_select_backend_macos_25() {
|
||||
env::set_var("MACOS_VERSION", "25.0.0");
|
||||
let backend = select_backend();
|
||||
assert_eq!(backend, BackendType::Nfs4);
|
||||
env::remove_var("MACOS_VERSION");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_manual_backend_selection() {
|
||||
assert!(select_backend_manual("nfs").is_ok());
|
||||
assert!(select_backend_manual("fskit").is_ok());
|
||||
assert!(select_backend_manual("invalid").is_err());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_fuse_t_binary_detection() {
|
||||
// Should detect FUSE-T binary after installation
|
||||
let detected = detect_fuse_t_binary();
|
||||
assert!(detected); // Expected to be true after installation
|
||||
}
|
||||
}
|
||||
36
markbase-fuse/src/fuse/cache.rs
Normal file
36
markbase-fuse/src/fuse/cache.rs
Normal file
@@ -0,0 +1,36 @@
|
||||
use std::collections::HashMap;
|
||||
use std::sync::Mutex;
|
||||
|
||||
pub struct ThreadSafeCache {
|
||||
file_cache: Mutex<HashMap<String, (String, u64)>>, // node_id -> (file_path, file_size)
|
||||
path_cache: Mutex<HashMap<String, String>>, // path -> node_id
|
||||
}
|
||||
|
||||
impl ThreadSafeCache {
|
||||
pub fn new() -> Self {
|
||||
Self {
|
||||
file_cache: Mutex::new(HashMap::new()),
|
||||
path_cache: Mutex::new(HashMap::new()),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn insert_file(&self, node_id: &str, file_path: &str, file_size: u64) {
|
||||
let mut cache = self.file_cache.lock().unwrap();
|
||||
cache.insert(node_id.to_string(), (file_path.to_string(), file_size));
|
||||
}
|
||||
|
||||
pub fn lookup_file(&self, node_id: &str) -> Option<(String, u64)> {
|
||||
let cache = self.file_cache.lock().unwrap();
|
||||
cache.get(node_id).cloned()
|
||||
}
|
||||
|
||||
pub fn insert_path(&self, path: &str, node_id: &str) {
|
||||
let mut cache = self.path_cache.lock().unwrap();
|
||||
cache.insert(path.to_string(), node_id.to_string());
|
||||
}
|
||||
|
||||
pub fn lookup_path(&self, path: &str) -> Option<String> {
|
||||
let cache = self.path_cache.lock().unwrap();
|
||||
cache.get(path).cloned()
|
||||
}
|
||||
}
|
||||
140
markbase-fuse/src/fuse/db.rs
Normal file
140
markbase-fuse/src/fuse/db.rs
Normal file
@@ -0,0 +1,140 @@
|
||||
use anyhow::Result;
|
||||
use rusqlite::{params, Connection, OpenFlags};
|
||||
use std::sync::Mutex;
|
||||
|
||||
pub struct DbManager {
|
||||
conn: Mutex<Connection>,
|
||||
tree_type: String,
|
||||
}
|
||||
|
||||
impl DbManager {
|
||||
pub fn open(db_path: &str, tree_type: &str) -> Result<Self> {
|
||||
let conn = Connection::open_with_flags(
|
||||
db_path,
|
||||
OpenFlags::SQLITE_OPEN_READ_WRITE | OpenFlags::SQLITE_OPEN_CREATE,
|
||||
)?;
|
||||
|
||||
Ok(Self {
|
||||
conn: Mutex::new(conn),
|
||||
tree_type: tree_type.to_string(),
|
||||
})
|
||||
}
|
||||
|
||||
pub fn find_node_id(&self, path: &str) -> Result<Option<String>> {
|
||||
if path == "/" {
|
||||
return Ok(None);
|
||||
}
|
||||
|
||||
let components: Vec<&str> = path.split('/').filter(|s| !s.is_empty()).collect();
|
||||
|
||||
if components.is_empty() {
|
||||
return Ok(None);
|
||||
}
|
||||
|
||||
let conn = self.conn.lock().unwrap();
|
||||
let mut current_parent: Option<String> = None;
|
||||
|
||||
for (level, component) in components.iter().enumerate() {
|
||||
let sql = if level == 0 {
|
||||
"SELECT node_id FROM file_nodes WHERE label = ?1 AND tree_type = ?2 AND (parent_id IS NULL OR parent_id = '') LIMIT 1"
|
||||
} else {
|
||||
"SELECT node_id FROM file_nodes WHERE label = ?1 AND tree_type = ?2 AND parent_id = ?3 LIMIT 1"
|
||||
};
|
||||
|
||||
let mut stmt = conn.prepare(sql)?;
|
||||
|
||||
let result = if level == 0 {
|
||||
stmt.query_row(params![component, &self.tree_type], |row| row.get::<_, String>(0))
|
||||
} else {
|
||||
stmt.query_row(
|
||||
params![component, &self.tree_type, current_parent.as_ref().unwrap()],
|
||||
|row| row.get::<_, String>(0),
|
||||
)
|
||||
};
|
||||
|
||||
match result {
|
||||
Ok(node_id) => current_parent = Some(node_id),
|
||||
Err(_) => return Ok(None),
|
||||
}
|
||||
}
|
||||
|
||||
Ok(current_parent)
|
||||
}
|
||||
|
||||
pub fn get_file_path(&self, node_id: &str) -> Result<Option<String>> {
|
||||
let conn = self.conn.lock().unwrap();
|
||||
|
||||
let sql = "SELECT location FROM file_locations WHERE file_uuid = ?1 LIMIT 1";
|
||||
let mut stmt = conn.prepare(sql)?;
|
||||
|
||||
let result = stmt.query_row(params![node_id], |row| row.get::<_, String>(0));
|
||||
|
||||
match result {
|
||||
Ok(path) => Ok(Some(path)),
|
||||
Err(rusqlite::Error::QueryReturnedNoRows) => Ok(None),
|
||||
Err(e) => Err(e.into()),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn get_node_info(&self, node_id: &str) -> Result<Option<(String, u64)>> {
|
||||
let conn = self.conn.lock().unwrap();
|
||||
|
||||
let sql = "SELECT node_type, file_size FROM file_nodes WHERE node_id = ?1 AND tree_type = ?2";
|
||||
let mut stmt = conn.prepare(sql)?;
|
||||
|
||||
let result = stmt.query_row(params![node_id, &self.tree_type], |row| {
|
||||
Ok((row.get::<_, String>(0)?, row.get::<_, u64>(1)?))
|
||||
});
|
||||
|
||||
match result {
|
||||
Ok(info) => Ok(Some(info)),
|
||||
Err(rusqlite::Error::QueryReturnedNoRows) => Ok(None),
|
||||
Err(e) => Err(e.into()),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn list_children(&self, parent_id: &str) -> Result<Vec<String>> {
|
||||
let conn = self.conn.lock().unwrap();
|
||||
|
||||
let sql = "SELECT label FROM file_nodes WHERE parent_id = ?1 AND tree_type = ?2";
|
||||
let mut stmt = conn.prepare(sql)?;
|
||||
|
||||
let labels = stmt
|
||||
.query_map(params![parent_id, &self.tree_type], |row| row.get::<_, String>(0))?
|
||||
.collect::<Result<Vec<_>, _>>()?;
|
||||
|
||||
Ok(labels)
|
||||
}
|
||||
|
||||
pub fn preload_files(
|
||||
&self,
|
||||
cache: &super::cache::ThreadSafeCache,
|
||||
limit: usize,
|
||||
) -> Result<usize> {
|
||||
let conn = self.conn.lock().unwrap();
|
||||
|
||||
let sql = "SELECT f.file_uuid, l.location, f.file_size
|
||||
FROM file_nodes f
|
||||
JOIN file_locations l ON f.file_uuid = l.file_uuid
|
||||
WHERE f.tree_type = ?2
|
||||
ORDER BY f.file_size DESC LIMIT ?1";
|
||||
|
||||
let mut stmt = conn.prepare(sql)?;
|
||||
let rows = stmt.query_map(params![limit, &self.tree_type], |row| {
|
||||
Ok((
|
||||
row.get::<_, String>(0)?,
|
||||
row.get::<_, String>(1)?,
|
||||
row.get::<_, u64>(2)?,
|
||||
))
|
||||
})?;
|
||||
|
||||
let mut count = 0;
|
||||
for row in rows {
|
||||
let (node_id, location, file_size) = row?;
|
||||
cache.insert_file(&node_id, &location, file_size);
|
||||
count += 1;
|
||||
}
|
||||
|
||||
Ok(count)
|
||||
}
|
||||
}
|
||||
242
markbase-fuse/src/fuse/filesystem.rs
Normal file
242
markbase-fuse/src/fuse/filesystem.rs
Normal file
@@ -0,0 +1,242 @@
|
||||
use anyhow::Result;
|
||||
use fuse::{
|
||||
FileAttr, FileType, Filesystem, ReplyAttr, ReplyData, ReplyDirectory, ReplyEntry, Request,
|
||||
};
|
||||
use libc::{EIO, ENOENT};
|
||||
use std::collections::HashMap;
|
||||
use std::fs::File;
|
||||
use std::io::{Read, SeekFrom};
|
||||
use std::path::Path;
|
||||
use std::sync::Arc;
|
||||
use std::time::SystemTime;
|
||||
|
||||
use std::ffi::CString;
|
||||
|
||||
const TTL: std::time::Duration = std::time::Duration::from_secs(1);
|
||||
const READ_CHUNK_SIZE: usize = 524288; // 512KB for maximum throughput
|
||||
|
||||
const CACHE_SIZE: usize = 1000;
|
||||
|
||||
pub struct MarkBaseFs {
|
||||
db: Arc<DbManager>,
|
||||
cache: Arc<ThreadSafeCache>,
|
||||
inode_map: HashMap<u64, String>,
|
||||
path_cache: HashMap<String, u64>,
|
||||
next_inode: u64,
|
||||
}
|
||||
|
||||
impl MarkBaseFs {
|
||||
pub fn new(db_path: &str, tree_type: &str) -> Result<Self> {
|
||||
let db = DbManager::open(db_path, tree_type)?;
|
||||
let cache = ThreadSafeCache::new();
|
||||
|
||||
let count = db.preload_files(&cache, CACHE_SIZE)?;
|
||||
println!("Pre-cached {} files for tree_type: {}", count, tree_type);
|
||||
|
||||
Ok(Self {
|
||||
db: Arc::new(db),
|
||||
cache: Arc::new(cache),
|
||||
inode_map: HashMap::new(),
|
||||
path_cache: HashMap::new(),
|
||||
next_inode: 2,
|
||||
})
|
||||
}
|
||||
|
||||
fn find_node_id_by_inode(&self, ino: u64) -> Option<String> {
|
||||
self.inode_map.get(&ino).cloned()
|
||||
}
|
||||
|
||||
fn get_or_create_inode(&mut self, node_id: &str) -> u64 {
|
||||
// Find existing inode
|
||||
for (ino, id) in &self.inode_map {
|
||||
if id == node_id {
|
||||
return *ino;
|
||||
}
|
||||
}
|
||||
|
||||
// Create new inode
|
||||
let ino = self.next_inode;
|
||||
self.next_inode += 1;
|
||||
self.inode_map.insert(ino, node_id.to_string());
|
||||
ino
|
||||
}
|
||||
|
||||
fn find_node_id_by_path(&self, path: &str) -> Option<String> {
|
||||
// Check path cache first
|
||||
if let Some(node_id) = self.cache.lookup_path(path) {
|
||||
return Some(node_id);
|
||||
}
|
||||
|
||||
// Query from database
|
||||
match self.db.find_node_id(path) {
|
||||
Ok(Some(node_id)) => {
|
||||
self.cache.insert_path(path, &node_id);
|
||||
Some(node_id)
|
||||
}
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
|
||||
fn get_file_path(&self, node_id: &str) -> Option<String> {
|
||||
// Check cache first
|
||||
if let Some((path, _)) = self.cache.lookup_file(node_id) {
|
||||
return Some(path);
|
||||
}
|
||||
|
||||
// Query from database
|
||||
match self.db.get_file_path(node_id) {
|
||||
Ok(Some(path)) => {
|
||||
self.cache.insert_file(node_id, &path, 0);
|
||||
Some(path)
|
||||
}
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
|
||||
fn make_file_attr(ino: u64, node_type: &str, file_size: u64) -> FileAttr {
|
||||
FileAttr {
|
||||
ino,
|
||||
size: file_size,
|
||||
blocks: (file_size + 511) / 512,
|
||||
atime: SystemTime::now(),
|
||||
mtime: SystemTime::now(),
|
||||
ctime: SystemTime::now(),
|
||||
crtime: SystemTime::now(),
|
||||
kind: if node_type == "folder" {
|
||||
FileType::Directory
|
||||
} else {
|
||||
FileType::RegularFile
|
||||
},
|
||||
perm: if node_type == "folder" { 0o755 } else { 0o644 },
|
||||
nlink: if node_type == "folder" { 2 } else { 1 },
|
||||
uid: 501, // default user
|
||||
gid: 20, // default group
|
||||
rdev: 0,
|
||||
flags: 0,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Filesystem for MarkBaseFs {
|
||||
fn lookup(&mut self, _req: &Request, parent: u64, name: &Path, reply: ReplyEntry) {
|
||||
let parent_path = if parent == 1 { "/" } else { "" };
|
||||
let full_path = format!("{}/{}", parent_path, name.to_string_lossy());
|
||||
|
||||
let node_id = self.find_node_id_by_path(&full_path);
|
||||
|
||||
match node_id {
|
||||
Some(id) => {
|
||||
let info = self.db.get_node_info(&id).ok();
|
||||
|
||||
match info {
|
||||
Some((node_type, file_size)) => {
|
||||
let ino = self.get_or_create_inode(&id);
|
||||
let attr = self.make_file_attr(ino, &node_type, file_size);
|
||||
|
||||
reply.entry(&TTL, &attr, 0);
|
||||
}
|
||||
_ => reply.error(ENOENT),
|
||||
}
|
||||
}
|
||||
None => reply.error(ENOENT),
|
||||
}
|
||||
}
|
||||
|
||||
fn getattr(&mut self, _req: &Request, ino: u64, reply: ReplyAttr) {
|
||||
if ino == 1 {
|
||||
let attr = self.make_file_attr(1, "folder", 0);
|
||||
reply.attr(&TTL, &attr);
|
||||
return;
|
||||
}
|
||||
|
||||
let node_id = self.find_node_id_by_inode(ino);
|
||||
|
||||
match node_id {
|
||||
Some(id) => {
|
||||
let info = self.db.get_node_info(&id).ok();
|
||||
|
||||
match info {
|
||||
Some((node_type, file_size)) => {
|
||||
let attr = self.make_file_attr(ino, &node_type, file_size);
|
||||
reply.attr(&TTL, &attr);
|
||||
}
|
||||
_ => reply.error(ENOENT),
|
||||
}
|
||||
}
|
||||
None => reply.error(ENOENT),
|
||||
}
|
||||
}
|
||||
|
||||
fn read(
|
||||
&mut self,
|
||||
_req: &Request,
|
||||
ino: u64,
|
||||
fh: u64,
|
||||
offset: i64,
|
||||
size: u32,
|
||||
reply: ReplyData,
|
||||
) {
|
||||
let node_id = self.find_node_id_by_inode(ino);
|
||||
|
||||
match node_id {
|
||||
Some(id) => {
|
||||
let file_path = self.get_file_path(&id);
|
||||
|
||||
match file_path {
|
||||
Some(fp) => {
|
||||
let mut file = File::open(&fp)?;
|
||||
file.seek(SeekFrom::Start(offset as u64)).ok();
|
||||
|
||||
let mut buffer = vec![0u8; size as usize];
|
||||
let bytes_read = file.read(&mut buffer).ok();
|
||||
buffer.truncate(bytes_read);
|
||||
reply.data(&buffer);
|
||||
}
|
||||
None => reply.error(ENOENT),
|
||||
}
|
||||
}
|
||||
None => reply.error(ENOENT),
|
||||
}
|
||||
}
|
||||
|
||||
fn readdir(
|
||||
&mut self,
|
||||
_req: &Request,
|
||||
ino: u64,
|
||||
fh: u64,
|
||||
offset: i64,
|
||||
mut reply: ReplyDirectory,
|
||||
) {
|
||||
if offset == 0 {
|
||||
reply.add(ino, 1, FileType::Directory, ".");
|
||||
reply.add(ino, 2, FileType::Directory, "..");
|
||||
|
||||
if ino == 1 {
|
||||
// Root - show Home folder
|
||||
reply.add(2, 3, FileType::Directory, "Home");
|
||||
} else {
|
||||
let node_id = self.find_node_id_by_inode(ino);
|
||||
|
||||
match node_id {
|
||||
Some(id) => {
|
||||
let children = self.db.list_children(&id).ok();
|
||||
let mut child_offset = 3;
|
||||
|
||||
for child_name in children {
|
||||
reply.add(
|
||||
child_offset,
|
||||
child_offset + 1,
|
||||
FileType::RegularFile,
|
||||
&child_name,
|
||||
);
|
||||
child_offset += 1;
|
||||
}
|
||||
}
|
||||
None => {}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
reply.ok();
|
||||
}
|
||||
}
|
||||
@@ -1,193 +0,0 @@
|
||||
use std::path::PathBuf;
|
||||
use std::fs::File;
|
||||
use std::io::{Read, Seek, SeekFrom};
|
||||
use anyhow::{Result, Error};
|
||||
|
||||
pub struct FuseOperations<'a> {
|
||||
fs: &'a MarkBaseFs,
|
||||
}
|
||||
|
||||
struct QueryNodeResult {
|
||||
node_id: String,
|
||||
label: String,
|
||||
node_type: String,
|
||||
file_size: Option<i64>,
|
||||
parent_id: Option<String>,
|
||||
created_at: Option<i64>,
|
||||
updated_at: Option<i64>,
|
||||
}
|
||||
|
||||
impl<'a> FuseOperations<'a> {
|
||||
pub fn new(fs: &'a MarkBaseFs) -> Self {
|
||||
FuseOperations { fs }
|
||||
}
|
||||
|
||||
pub fn getattr(&self, ino: u64) -> Result<FileAttr> {
|
||||
let uuid = MarkBaseFs::ino_to_uuid(ino);
|
||||
|
||||
let node = self.query_node(&uuid)?;
|
||||
|
||||
let kind = match node.node_type.as_str() {
|
||||
"folder" => FileKind::Directory,
|
||||
"file" => FileKind::RegularFile,
|
||||
_ => FileKind::RegularFile,
|
||||
};
|
||||
|
||||
let size = if kind == FileKind::RegularFile {
|
||||
node.file_size.unwrap_or(0) as u64
|
||||
} else {
|
||||
0
|
||||
};
|
||||
|
||||
Ok(FileAttr {
|
||||
ino,
|
||||
size,
|
||||
mode: if kind == FileKind::Directory { 0o755 } else { 0o644 },
|
||||
nlink: if kind == FileKind::Directory { 2 } else { 1 },
|
||||
uid: 0,
|
||||
gid: 0,
|
||||
atime: node.updated_at.unwrap_or(0) as u64,
|
||||
mtime: node.updated_at.unwrap_or(0) as u64,
|
||||
ctime: node.created_at.unwrap_or(0) as u64,
|
||||
kind,
|
||||
})
|
||||
}
|
||||
|
||||
pub fn readdir(&self, ino: u64) -> Result<Vec<(u64, String, FileKind)>> {
|
||||
let uuid = MarkBaseFs::ino_to_uuid(ino);
|
||||
|
||||
let children = self.query_children(&uuid)?;
|
||||
|
||||
let entries: Vec<(u64, String, FileKind)> = children
|
||||
.into_iter()
|
||||
.map(|node| {
|
||||
let child_ino = MarkBaseFs::uuid_to_ino(&node.node_id);
|
||||
let kind = match node.node_type.as_str() {
|
||||
"folder" => FileKind::Directory,
|
||||
"file" => FileKind::RegularFile,
|
||||
_ => FileKind::RegularFile,
|
||||
};
|
||||
(child_ino, node.label, kind)
|
||||
})
|
||||
.collect();
|
||||
|
||||
Ok(entries)
|
||||
}
|
||||
|
||||
pub fn read(&self, ino: u64, offset: u64, size: u32) -> Result<Vec<u8>> {
|
||||
let uuid = MarkBaseFs::ino_to_uuid(ino);
|
||||
|
||||
let path = self.get_file_path(&uuid)?;
|
||||
|
||||
if !path.exists() {
|
||||
return Err(Error::msg("File not found"));
|
||||
}
|
||||
|
||||
let mut file = File::open(&path)?;
|
||||
file.seek(SeekFrom::Start(offset))?;
|
||||
|
||||
let mut buffer = vec![0u8; size as usize];
|
||||
let bytes_read = file.read(&mut buffer)?;
|
||||
buffer.truncate(bytes_read);
|
||||
|
||||
Ok(buffer)
|
||||
}
|
||||
|
||||
fn query_node(&self, uuid: &str) -> Result<QueryNodeResult> {
|
||||
use rusqlite::Connection;
|
||||
|
||||
let db_path = self.fs.get_db_path();
|
||||
let conn = Connection::open(db_path)?;
|
||||
|
||||
let node = conn.query_row(
|
||||
"SELECT node_id, label, node_type, file_size, parent_id, created_at, updated_at
|
||||
FROM file_nodes
|
||||
WHERE node_id = ?",
|
||||
[uuid],
|
||||
|row| {
|
||||
Ok(QueryNodeResult {
|
||||
node_id: row.get::<_, String>(0)?,
|
||||
label: row.get::<_, String>(1)?,
|
||||
node_type: row.get::<_, String>(2)?,
|
||||
file_size: row.get::<_, Option<i64>>(3)?,
|
||||
parent_id: row.get::<_, Option<String>>(4)?,
|
||||
created_at: row.get::<_, Option<i64>>(5)?,
|
||||
updated_at: row.get::<_, Option<i64>>(6)?,
|
||||
})
|
||||
}
|
||||
)?;
|
||||
|
||||
Ok(node)
|
||||
}
|
||||
|
||||
fn query_children(&self, parent_uuid: &str) -> Result<Vec<QueryNodeResult>> {
|
||||
use rusqlite::Connection;
|
||||
|
||||
let db_path = self.fs.get_db_path();
|
||||
let conn = Connection::open(db_path)?;
|
||||
|
||||
let mut stmt = conn.prepare(
|
||||
"SELECT node_id, label, node_type, file_size, parent_id, created_at, updated_at
|
||||
FROM file_nodes
|
||||
WHERE parent_id = ?
|
||||
ORDER BY sort_order, label"
|
||||
)?;
|
||||
|
||||
let children = stmt.query_map([parent_uuid], |row| {
|
||||
Ok(QueryNodeResult {
|
||||
node_id: row.get::<_, String>(0)?,
|
||||
label: row.get::<_, String>(1)?,
|
||||
node_type: row.get::<_, String>(2)?,
|
||||
file_size: row.get::<_, Option<i64>>(3)?,
|
||||
parent_id: row.get::<_, Option<String>>(4)?,
|
||||
created_at: row.get::<_, Option<i64>>(5)?,
|
||||
updated_at: row.get::<_, Option<i64>>(6)?,
|
||||
})
|
||||
})?.collect::<Result<Vec<_>, _>>()?;
|
||||
|
||||
Ok(children)
|
||||
}
|
||||
|
||||
fn get_file_path(&self, uuid: &str) -> Result<PathBuf> {
|
||||
use rusqlite::Connection;
|
||||
|
||||
let db_path = self.fs.get_db_path();
|
||||
let conn = Connection::open(db_path)?;
|
||||
|
||||
let path_str = conn.query_row(
|
||||
"SELECT location FROM file_locations WHERE file_uuid = ?",
|
||||
[uuid],
|
||||
|row| row.get::<_, String>(0)
|
||||
)?;
|
||||
|
||||
Ok(PathBuf::from(path_str))
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use crate::fuse::backend::BackendType;
|
||||
|
||||
#[test]
|
||||
fn test_fuse_operations_creation() {
|
||||
let db_path = PathBuf::from("data/users/warren.sqlite");
|
||||
let fs = MarkBaseFs::new("warren".to_string(), db_path, BackendType::Fskit);
|
||||
let ops = FuseOperations::new(&fs);
|
||||
|
||||
assert!(true);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_uuid_roundtrip() {
|
||||
let uuid = "8b1ede3cd6970f02fa85b8e34b682caf";
|
||||
let ino = MarkBaseFs::uuid_to_ino(uuid);
|
||||
|
||||
// Just verify the conversion produces a valid inode number
|
||||
assert!(ino > 0);
|
||||
|
||||
// And that we can convert back
|
||||
let recovered = MarkBaseFs::ino_to_uuid(ino);
|
||||
assert!(!recovered.is_empty());
|
||||
}
|
||||
}
|
||||
@@ -1,399 +0,0 @@
|
||||
use std::path::{Path, PathBuf};
|
||||
use std::ffi::CStr;
|
||||
use std::io;
|
||||
use std::time::Duration;
|
||||
use std::fs::File;
|
||||
use std::io::{Read, Seek, SeekFrom, Write};
|
||||
use anyhow::Result;
|
||||
use fuse_backend_rs::api::filesystem::{FileSystem, Entry, DirEntry, Context};
|
||||
use fuse_backend_rs::abi::fuse_abi::{FsOptions, OpenOptions, statvfs64};
|
||||
use libc::{stat as stat64, DT_DIR, DT_REG};
|
||||
|
||||
use crate::fuse::backend::BackendType;
|
||||
|
||||
pub struct MarkBaseFs {
|
||||
user_id: String,
|
||||
db_path: PathBuf,
|
||||
backend: BackendType,
|
||||
}
|
||||
|
||||
struct QueryNodeResult {
|
||||
node_id: String,
|
||||
label: String,
|
||||
node_type: String,
|
||||
file_size: Option<i64>,
|
||||
parent_id: Option<String>,
|
||||
created_at: Option<i64>,
|
||||
updated_at: Option<i64>,
|
||||
}
|
||||
|
||||
impl MarkBaseFs {
|
||||
pub fn new(user_id: String, db_path: PathBuf, backend: BackendType) -> Self {
|
||||
MarkBaseFs {
|
||||
user_id,
|
||||
db_path,
|
||||
backend,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn get_user_id(&self) -> &str {
|
||||
&self.user_id
|
||||
}
|
||||
|
||||
pub fn get_backend(&self) -> &BackendType {
|
||||
&self.backend
|
||||
}
|
||||
|
||||
pub fn get_db_path(&self) -> &Path {
|
||||
&self.db_path
|
||||
}
|
||||
|
||||
pub fn mount(&self, mount_path: &Path) -> Result<()> {
|
||||
println!("=== Mounting MarkBase FUSE ===");
|
||||
println!("User: {}", self.user_id);
|
||||
println!("Database: {}", self.db_path.display());
|
||||
println!("Backend: {}", self.backend.name());
|
||||
println!("Mount path: {}", mount_path.display());
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn uuid_to_ino(uuid: &str) -> u64 {
|
||||
let bytes = uuid.as_bytes();
|
||||
if bytes.len() >= 8 {
|
||||
u64::from_be_bytes([
|
||||
bytes[0], bytes[1], bytes[2], bytes[3],
|
||||
bytes[4], bytes[5], bytes[6], bytes[7],
|
||||
])
|
||||
} else {
|
||||
0
|
||||
}
|
||||
}
|
||||
|
||||
pub fn ino_to_uuid(ino: u64) -> String {
|
||||
let bytes = ino.to_be_bytes();
|
||||
format!("{:02x}{:02x}{:02x}{:02x}{:02x}{:02x}{:02x}{:02x}",
|
||||
bytes[0], bytes[1], bytes[2], bytes[3],
|
||||
bytes[4], bytes[5], bytes[6], bytes[7])
|
||||
}
|
||||
|
||||
fn query_node(&self, uuid: &str) -> io::Result<QueryNodeResult> {
|
||||
use rusqlite::Connection;
|
||||
|
||||
let conn = Connection::open(&self.db_path)
|
||||
.map_err(|e| io::Error::new(io::ErrorKind::Other, e.to_string()))?;
|
||||
|
||||
conn.query_row(
|
||||
"SELECT node_id, label, node_type, file_size, parent_id, created_at, updated_at
|
||||
FROM file_nodes
|
||||
WHERE node_id = ?",
|
||||
[uuid],
|
||||
|row| {
|
||||
Ok(QueryNodeResult {
|
||||
node_id: row.get::<_, String>(0)?,
|
||||
label: row.get::<_, String>(1)?,
|
||||
node_type: row.get::<_, String>(2)?,
|
||||
file_size: row.get::<_, Option<i64>>(3)?,
|
||||
parent_id: row.get::<_, Option<String>>(4)?,
|
||||
created_at: row.get::<_, Option<i64>>(5)?,
|
||||
updated_at: row.get::<_, Option<i64>>(6)?,
|
||||
})
|
||||
}
|
||||
).map_err(|e| io::Error::new(io::ErrorKind::NotFound, e.to_string()))
|
||||
}
|
||||
|
||||
fn query_children(&self, parent_uuid: &str) -> io::Result<Vec<QueryNodeResult>> {
|
||||
use rusqlite::Connection;
|
||||
|
||||
let conn = Connection::open(&self.db_path)
|
||||
.map_err(|e| io::Error::new(io::ErrorKind::Other, e.to_string()))?;
|
||||
|
||||
let mut stmt = conn.prepare(
|
||||
"SELECT node_id, label, node_type, file_size, parent_id, created_at, updated_at
|
||||
FROM file_nodes
|
||||
WHERE parent_id = ?
|
||||
ORDER BY sort_order, label"
|
||||
).map_err(|e| io::Error::new(io::ErrorKind::Other, e.to_string()))?;
|
||||
|
||||
let rows = stmt.query_map([parent_uuid], |row| {
|
||||
Ok(QueryNodeResult {
|
||||
node_id: row.get::<_, String>(0)?,
|
||||
label: row.get::<_, String>(1)?,
|
||||
node_type: row.get::<_, String>(2)?,
|
||||
file_size: row.get::<_, Option<i64>>(3)?,
|
||||
parent_id: row.get::<_, Option<String>>(4)?,
|
||||
created_at: row.get::<_, Option<i64>>(5)?,
|
||||
updated_at: row.get::<_, Option<i64>>(6)?,
|
||||
})
|
||||
}).map_err(|e| io::Error::new(io::ErrorKind::Other, e.to_string()))?;
|
||||
|
||||
let mut children = Vec::new();
|
||||
for row in rows {
|
||||
children.push(row.map_err(|e| io::Error::new(io::ErrorKind::Other, e.to_string()))?);
|
||||
}
|
||||
|
||||
Ok(children)
|
||||
}
|
||||
|
||||
fn get_file_path(&self, uuid: &str) -> io::Result<PathBuf> {
|
||||
use rusqlite::Connection;
|
||||
|
||||
let conn = Connection::open(&self.db_path)
|
||||
.map_err(|e| io::Error::new(io::ErrorKind::Other, e.to_string()))?;
|
||||
|
||||
conn.query_row(
|
||||
"SELECT location FROM file_locations WHERE file_uuid = ?",
|
||||
[uuid],
|
||||
|row| row.get::<_, String>(0)
|
||||
).map(PathBuf::from).map_err(|e| io::Error::new(io::ErrorKind::NotFound, e.to_string()))
|
||||
}
|
||||
}
|
||||
|
||||
impl FileSystem for MarkBaseFs {
|
||||
type Inode = u64;
|
||||
type Handle = u64;
|
||||
|
||||
fn init(&self, _capable: FsOptions) -> io::Result<FsOptions> {
|
||||
println!("MarkBaseFs::init() called - filesystem ready");
|
||||
println!("Database: {}", self.db_path.display());
|
||||
println!("User: {}", self.user_id);
|
||||
println!("Backend: {}", self.backend.name());
|
||||
Ok(FsOptions::empty())
|
||||
}
|
||||
|
||||
fn lookup(&self, _ctx: &Context, parent: Self::Inode, name: &CStr) -> io::Result<Entry> {
|
||||
let parent_uuid = Self::ino_to_uuid(parent);
|
||||
let name_str = name.to_string_lossy();
|
||||
|
||||
let children = self.query_children(&parent_uuid)?;
|
||||
|
||||
for child in children {
|
||||
if child.label == name_str {
|
||||
let child_ino = Self::uuid_to_ino(&child.node_id);
|
||||
let is_dir = child.node_type == "folder";
|
||||
|
||||
let mut stat: stat64 = unsafe { std::mem::zeroed() };
|
||||
stat.st_ino = child_ino;
|
||||
stat.st_mode = if is_dir { 0o755 | libc::S_IFDIR } else { 0o644 | libc::S_IFREG };
|
||||
stat.st_nlink = if is_dir { 2 } else { 1 };
|
||||
stat.st_size = child.file_size.unwrap_or(0) as i64;
|
||||
stat.st_mtime = child.updated_at.unwrap_or(0);
|
||||
stat.st_ctime = child.created_at.unwrap_or(0);
|
||||
|
||||
return Ok(Entry {
|
||||
inode: child_ino,
|
||||
generation: 0,
|
||||
attr: stat,
|
||||
attr_flags: 0,
|
||||
attr_timeout: Duration::from_secs(60),
|
||||
entry_timeout: Duration::from_secs(60),
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
Err(io::Error::from_raw_os_error(libc::ENOENT))
|
||||
}
|
||||
|
||||
fn getattr(
|
||||
&self,
|
||||
_ctx: &Context,
|
||||
inode: Self::Inode,
|
||||
_handle: Option<Self::Handle>,
|
||||
) -> io::Result<(stat64, Duration)> {
|
||||
let uuid = Self::ino_to_uuid(inode);
|
||||
let node = self.query_node(&uuid)?;
|
||||
|
||||
let is_dir = node.node_type == "folder";
|
||||
|
||||
let mut stat: stat64 = unsafe { std::mem::zeroed() };
|
||||
stat.st_ino = inode;
|
||||
stat.st_mode = if is_dir { 0o755 | libc::S_IFDIR } else { 0o644 | libc::S_IFREG };
|
||||
stat.st_nlink = if is_dir { 2 } else { 1 };
|
||||
stat.st_size = node.file_size.unwrap_or(0) as i64;
|
||||
stat.st_mtime = node.updated_at.unwrap_or(0);
|
||||
stat.st_ctime = node.created_at.unwrap_or(0);
|
||||
|
||||
Ok((stat, Duration::from_secs(60)))
|
||||
}
|
||||
|
||||
fn opendir(
|
||||
&self,
|
||||
_ctx: &Context,
|
||||
inode: Self::Inode,
|
||||
_flags: u32,
|
||||
) -> io::Result<(Option<Self::Handle>, OpenOptions)> {
|
||||
Ok((Some(inode), OpenOptions::empty()))
|
||||
}
|
||||
|
||||
fn readdir(
|
||||
&self,
|
||||
_ctx: &Context,
|
||||
inode: Self::Inode,
|
||||
_handle: Self::Handle,
|
||||
_size: u32,
|
||||
offset: u64,
|
||||
add_entry: &mut dyn FnMut(DirEntry) -> io::Result<usize>,
|
||||
) -> io::Result<()> {
|
||||
let uuid = Self::ino_to_uuid(inode);
|
||||
let children = self.query_children(&uuid)?;
|
||||
|
||||
for (idx, child) in children.iter().enumerate().skip(offset as usize) {
|
||||
let child_ino = Self::uuid_to_ino(&child.node_id);
|
||||
let type_ = if child.node_type == "folder" { DT_DIR } else { DT_REG };
|
||||
let name_bytes = child.label.as_bytes();
|
||||
|
||||
let entry = DirEntry {
|
||||
ino: child_ino,
|
||||
offset: (idx + 1) as u64,
|
||||
type_: type_ as u32,
|
||||
name: name_bytes,
|
||||
};
|
||||
|
||||
match add_entry(entry) {
|
||||
Ok(0) => break,
|
||||
Ok(_) => continue,
|
||||
Err(e) => return Err(e),
|
||||
}
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn releasedir(
|
||||
&self,
|
||||
_ctx: &Context,
|
||||
_inode: Self::Inode,
|
||||
_flags: u32,
|
||||
_handle: Self::Handle,
|
||||
) -> io::Result<()> {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn open(
|
||||
&self,
|
||||
_ctx: &Context,
|
||||
inode: Self::Inode,
|
||||
_flags: u32,
|
||||
_fuse_flags: u32,
|
||||
) -> io::Result<(Option<Self::Handle>, OpenOptions, Option<u32>)> {
|
||||
Ok((Some(inode), OpenOptions::empty(), None))
|
||||
}
|
||||
|
||||
fn read(
|
||||
&self,
|
||||
_ctx: &Context,
|
||||
inode: Self::Inode,
|
||||
_handle: Self::Handle,
|
||||
w: &mut dyn fuse_backend_rs::api::filesystem::ZeroCopyWriter,
|
||||
size: u32,
|
||||
offset: u64,
|
||||
_lock_owner: Option<u64>,
|
||||
_flags: u32,
|
||||
) -> io::Result<usize> {
|
||||
let uuid = Self::ino_to_uuid(inode);
|
||||
let path = self.get_file_path(&uuid)?;
|
||||
|
||||
if !path.exists() {
|
||||
return Err(io::Error::from_raw_os_error(libc::ENOENT));
|
||||
}
|
||||
|
||||
let mut file = File::open(&path)?;
|
||||
file.seek(SeekFrom::Start(offset))?;
|
||||
|
||||
let mut buffer = vec![0u8; size as usize];
|
||||
let bytes_read = file.read(&mut buffer)?;
|
||||
|
||||
w.write_all(&buffer[..bytes_read])?;
|
||||
|
||||
Ok(bytes_read)
|
||||
}
|
||||
|
||||
fn write(
|
||||
&self,
|
||||
_ctx: &Context,
|
||||
inode: Self::Inode,
|
||||
_handle: Self::Handle,
|
||||
r: &mut dyn fuse_backend_rs::api::filesystem::ZeroCopyReader,
|
||||
size: u32,
|
||||
offset: u64,
|
||||
_lock_owner: Option<u64>,
|
||||
_delayed_write: bool,
|
||||
_flags: u32,
|
||||
_fuse_flags: u32,
|
||||
) -> io::Result<usize> {
|
||||
let uuid = Self::ino_to_uuid(inode);
|
||||
let path = self.get_file_path(&uuid)?;
|
||||
|
||||
let mut file = File::create(&path)?;
|
||||
file.seek(SeekFrom::Start(offset))?;
|
||||
|
||||
let mut buffer = vec![0u8; size as usize];
|
||||
let bytes_read = r.read(&mut buffer)?;
|
||||
|
||||
file.write_all(&buffer[..bytes_read])?;
|
||||
|
||||
Ok(bytes_read)
|
||||
}
|
||||
|
||||
fn release(
|
||||
&self,
|
||||
_ctx: &Context,
|
||||
_inode: Self::Inode,
|
||||
_flags: u32,
|
||||
_handle: Self::Handle,
|
||||
_flush: bool,
|
||||
_flock_release: bool,
|
||||
_lock_owner: Option<u64>,
|
||||
) -> io::Result<()> {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn statfs(&self, _ctx: &Context, _inode: Self::Inode) -> io::Result<statvfs64> {
|
||||
let mut stat: statvfs64 = unsafe { std::mem::zeroed() };
|
||||
stat.f_bsize = 4096;
|
||||
stat.f_frsize = 4096;
|
||||
stat.f_blocks = 1000000;
|
||||
stat.f_bfree = 500000;
|
||||
stat.f_bavail = 500000;
|
||||
stat.f_files = 12659;
|
||||
stat.f_ffree = 50000;
|
||||
stat.f_favail = 50000;
|
||||
Ok(stat)
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn test_markbase_fs_creation() {
|
||||
let db_path = PathBuf::from("/tmp/test.sqlite");
|
||||
let fs = MarkBaseFs::new("test_user".to_string(), db_path, BackendType::Fskit);
|
||||
|
||||
assert_eq!(fs.get_user_id(), "test_user");
|
||||
assert_eq!(fs.get_backend(), &BackendType::Fskit);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_uuid_to_ino_conversion() {
|
||||
let uuid = "8b1ede3cd6970f02fa85b8e34b682caf";
|
||||
let ino = MarkBaseFs::uuid_to_ino(uuid);
|
||||
|
||||
let ino2 = MarkBaseFs::uuid_to_ino(uuid);
|
||||
assert_eq!(ino, ino2);
|
||||
|
||||
assert!(ino > 0);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_mount_placeholder() {
|
||||
let db_path = PathBuf::from("/tmp/test.sqlite");
|
||||
let fs = MarkBaseFs::new("test_user".to_string(), db_path, BackendType::Nfs4);
|
||||
|
||||
let mount_path = Path::new("/tmp/mount_test");
|
||||
let result = fs.mount(mount_path);
|
||||
|
||||
assert!(result.is_ok());
|
||||
}
|
||||
}
|
||||
@@ -1,9 +1,5 @@
|
||||
pub mod poc_hello;
|
||||
pub mod backend;
|
||||
pub mod markbase_fs;
|
||||
pub mod mount_manager;
|
||||
pub mod cache;
|
||||
pub mod db;
|
||||
pub mod filesystem;
|
||||
|
||||
pub use backend::{BackendType, select_backend, select_backend_manual, detect_macos_version};
|
||||
pub use poc_hello::{HelloFs, mount_hello_fs};
|
||||
pub use markbase_fs::MarkBaseFs;
|
||||
pub use mount_manager::{MountHandle, mount_user_fs};
|
||||
pub use filesystem::MarkBaseFs;
|
||||
|
||||
@@ -1,160 +0,0 @@
|
||||
use std::path::PathBuf;
|
||||
use std::sync::Arc;
|
||||
use std::thread;
|
||||
use anyhow::{Result, Error};
|
||||
use log::info;
|
||||
|
||||
use fuse_backend_rs::api::server::Server;
|
||||
use fuse_backend_rs::transport::FuseSession;
|
||||
|
||||
use crate::fuse::markbase_fs::MarkBaseFs;
|
||||
|
||||
pub struct MountHandle {
|
||||
session: FuseSession,
|
||||
mount_path: PathBuf,
|
||||
user_id: String,
|
||||
}
|
||||
|
||||
impl MountHandle {
|
||||
pub fn new(
|
||||
user_id: String,
|
||||
mount_path: PathBuf,
|
||||
_db_path: PathBuf,
|
||||
readonly: bool,
|
||||
) -> Result<Self> {
|
||||
let fsname = "MarkBase";
|
||||
let subtype = &user_id;
|
||||
|
||||
let session = FuseSession::new(&mount_path, fsname, subtype, readonly)
|
||||
.map_err(|e| Error::msg(format!("Failed to create FUSE session: {:?}", e)))?;
|
||||
|
||||
Ok(MountHandle {
|
||||
session,
|
||||
mount_path,
|
||||
user_id,
|
||||
})
|
||||
}
|
||||
|
||||
pub fn mount(&mut self, db_path: PathBuf) -> Result<()> {
|
||||
info!("Mounting MarkBase FUSE for user: {}", self.user_id);
|
||||
info!("Mount path: {}", self.mount_path.display());
|
||||
info!("Database: {}", db_path.display());
|
||||
|
||||
self.session.mount()
|
||||
.map_err(|e| Error::msg(format!("Failed to mount: {:?}", e)))?;
|
||||
|
||||
info!("FUSE session mounted successfully");
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn unmount(&mut self) -> Result<()> {
|
||||
info!("Unmounting MarkBase FUSE for user: {}", self.user_id);
|
||||
|
||||
self.session.umount()
|
||||
.map_err(|e| Error::msg(format!("Failed to unmount: {:?}", e)))?;
|
||||
|
||||
self.session.wake()
|
||||
.map_err(|e| Error::msg(format!("Failed to wake session: {:?}", e)))?;
|
||||
|
||||
info!("FUSE session unmounted successfully");
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
pub fn mount_user_fs(
|
||||
user_id: String,
|
||||
mount_path: PathBuf,
|
||||
db_path: PathBuf,
|
||||
readonly: bool,
|
||||
) -> Result<()> {
|
||||
println!("[DEBUG] Creating mount handle...");
|
||||
let mut handle = MountHandle::new(user_id.clone(), mount_path.clone(), db_path.clone(), readonly)?;
|
||||
|
||||
println!("[DEBUG] Calling session.mount()...");
|
||||
handle.mount(db_path.clone())?;
|
||||
|
||||
println!("[DEBUG] Creating filesystem instance...");
|
||||
let backend = crate::fuse::backend::select_backend();
|
||||
let fs = Arc::new(MarkBaseFs::new(user_id.clone(), db_path, backend));
|
||||
|
||||
let server = Arc::new(Server::new(fs));
|
||||
|
||||
println!("[DEBUG] Creating FUSE channel...");
|
||||
let channel = handle.session.new_channel()
|
||||
.map_err(|e| Error::msg(format!("Failed to create channel: {:?}", e)))?;
|
||||
|
||||
println!("[DEBUG] Starting FUSE request handler thread...");
|
||||
|
||||
let user_id_clone = user_id.clone();
|
||||
|
||||
let handler_thread = thread::spawn(move || {
|
||||
println!("[DEBUG] Handler thread started for user: {}", user_id_clone);
|
||||
|
||||
let mut channel = channel;
|
||||
|
||||
loop {
|
||||
match channel.get_request() {
|
||||
Ok(Some((reader, writer))) => {
|
||||
println!("[DEBUG] Received FUSE request");
|
||||
let writer = writer.into();
|
||||
if let Err(e) = server.handle_message(reader, writer, None, None) {
|
||||
println!("[WARN] Error handling FUSE request: {:?}", e);
|
||||
}
|
||||
}
|
||||
Ok(None) => {
|
||||
println!("[DEBUG] FUSE channel received signal to exit");
|
||||
break;
|
||||
}
|
||||
Err(e) => {
|
||||
println!("[WARN] Error getting FUSE request: {:?}", e);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
println!("[DEBUG] Handler thread exited for user: {}", user_id_clone);
|
||||
});
|
||||
|
||||
println!("[DEBUG] Calling session.wait_mount()...");
|
||||
match handle.session.wait_mount() {
|
||||
Ok(_) => {
|
||||
println!("[INFO] wait_mount() returned OK - mount completed successfully");
|
||||
}
|
||||
Err(e) => {
|
||||
println!("[ERROR] wait_mount() failed: {:?}", e);
|
||||
return Err(Error::msg(format!("Failed to wait mount: {:?}", e)));
|
||||
}
|
||||
}
|
||||
|
||||
println!("[INFO] Mount completed for user: {}", user_id);
|
||||
println!("[DEBUG] Handler thread status: {:?}", handler_thread.is_finished());
|
||||
|
||||
println!("[DEBUG] Joining handler thread...");
|
||||
handler_thread.join()
|
||||
.map_err(|e| Error::msg(format!("Handler thread error: {:?}", e)))?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use std::path::PathBuf;
|
||||
|
||||
#[test]
|
||||
fn test_mount_handle_creation() {
|
||||
let mount_path = PathBuf::from("/tmp/test_mount");
|
||||
let db_path = PathBuf::from("/tmp/test.sqlite");
|
||||
|
||||
let result = MountHandle::new(
|
||||
"test_user".to_string(),
|
||||
mount_path,
|
||||
db_path,
|
||||
false,
|
||||
);
|
||||
|
||||
assert!(result.is_ok());
|
||||
}
|
||||
}
|
||||
@@ -1,36 +0,0 @@
|
||||
use std::path::Path;
|
||||
use anyhow::Result;
|
||||
|
||||
pub struct HelloFs;
|
||||
|
||||
impl HelloFs {
|
||||
pub fn new() -> Self {
|
||||
HelloFs
|
||||
}
|
||||
}
|
||||
|
||||
pub fn mount_hello_fs(path: &Path) -> Result<()> {
|
||||
println!("FUSE Hello POC - Mount at: {}", path.display());
|
||||
println!("NOTE: This is a placeholder implementation.");
|
||||
println!("Actual FUSE mount requires fuse library (not yet added).");
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn test_hello_fs_creation() {
|
||||
let fs = HelloFs::new();
|
||||
assert!(true);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_mount_placeholder() {
|
||||
let path = Path::new("/tmp/test_fuse");
|
||||
let result = mount_hello_fs(path);
|
||||
assert!(result.is_ok());
|
||||
}
|
||||
}
|
||||
80
markbase-fuse/src/fuse_rust_main.rs
Normal file
80
markbase-fuse/src/fuse_rust_main.rs
Normal file
@@ -0,0 +1,80 @@
|
||||
use clap::{Parser, Subcommand};
|
||||
use std::path::PathBuf;
|
||||
use anyhow::Result;
|
||||
|
||||
#[derive(Parser)]
|
||||
#[command(name = "markbase-fuse-rust", about = "MarkBase FUSE (Rust libfuse3)")]
|
||||
struct Cli {
|
||||
#[command(subcommand)]
|
||||
command: Commands,
|
||||
}
|
||||
|
||||
#[derive(Subcommand)]
|
||||
enum Commands {
|
||||
/// Mount FUSE filesystem
|
||||
Mount {
|
||||
/// User ID
|
||||
#[arg(short, long)]
|
||||
user: String,
|
||||
/// Mount path
|
||||
#[arg(short, long)]
|
||||
dir: PathBuf,
|
||||
/// Database path
|
||||
#[arg(long)]
|
||||
db: Option<PathBuf>,
|
||||
},
|
||||
}
|
||||
|
||||
fn main() -> Result<()> {
|
||||
let cli = Cli::parse();
|
||||
|
||||
match cli.command {
|
||||
Commands::Mount { user, dir, db } => {
|
||||
mount_user(user, dir, db)?;
|
||||
}
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn mount_user(user: String, dir: PathBuf, db_path: Option<PathBuf>) -> Result<()> {
|
||||
use markbase_fuse::fuse_rust::MarkBaseFs;
|
||||
use std::env::current_dir;
|
||||
|
||||
let db_path = match db_path {
|
||||
Some(p) => p,
|
||||
None => {
|
||||
let mut path = current_dir()?;
|
||||
path.push("data/users");
|
||||
path.push(format!("{}.sqlite", user));
|
||||
path
|
||||
}
|
||||
};
|
||||
|
||||
if !db_path.exists() {
|
||||
return Err(anyhow::anyhow!("Database not found: {}", db_path.display()));
|
||||
}
|
||||
|
||||
if !dir.exists() {
|
||||
std::fs::create_dir_all(&dir)?;
|
||||
}
|
||||
|
||||
println!("=== MarkBase FUSE (Rust v15 equivalent) ===");
|
||||
println!("User: {}", user);
|
||||
println!("Database: {}", db_path.display());
|
||||
println!("Mount: {}", dir.display());
|
||||
println!("Features:");
|
||||
println!(" - 512KB read chunks");
|
||||
println!(" - Hash-based cache (1000 entries)");
|
||||
println!(" - Path cache (2000 entries)");
|
||||
println!(" - Pre-cache 1000 files");
|
||||
println!(" - Thread-safe Mutex");
|
||||
println!("");
|
||||
|
||||
let fs = MarkBaseFs::new(&db_path.to_string_lossy())?;
|
||||
|
||||
// Mount using fuse crate
|
||||
fuse::mount(fs, &dir, &[]);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
3
markbase-fuse/src/lib.rs
Normal file
3
markbase-fuse/src/lib.rs
Normal file
@@ -0,0 +1,3 @@
|
||||
pub mod fuse;
|
||||
|
||||
pub use fuse::MarkBaseFs;
|
||||
107
markbase-fuse/src/main.rs
Normal file
107
markbase-fuse/src/main.rs
Normal file
@@ -0,0 +1,107 @@
|
||||
use anyhow::Result;
|
||||
use clap::{Parser, Subcommand};
|
||||
use std::path::PathBuf;
|
||||
use std::sync::Arc;
|
||||
use std::sync::Mutex;
|
||||
|
||||
#[derive(Parser)]
|
||||
#[command(name = "markbase-fuse", about = "MarkBase FUSE Mount Tool")]
|
||||
struct Cli {
|
||||
#[command(subcommand)]
|
||||
command: Commands,
|
||||
}
|
||||
|
||||
#[derive(Subcommand)]
|
||||
enum Commands {
|
||||
Mount {
|
||||
#[arg(short, long)]
|
||||
user: String,
|
||||
#[arg(short, long)]
|
||||
dir: PathBuf,
|
||||
#[arg(short, long, default_value = "untitled folder")]
|
||||
tree_type: String,
|
||||
},
|
||||
Unmount {
|
||||
#[arg(short, long)]
|
||||
dir: PathBuf,
|
||||
},
|
||||
}
|
||||
|
||||
fn main() -> Result<()> {
|
||||
let cli = Cli::parse();
|
||||
|
||||
match cli.command {
|
||||
Commands::Mount { user, dir, tree_type } => {
|
||||
mount_user(user, tree_type, dir)?;
|
||||
}
|
||||
Commands::Unmount { dir } => {
|
||||
unmount_user(dir)?;
|
||||
}
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn mount_user(user: String, tree_type: String, dir: PathBuf) -> Result<()> {
|
||||
use fuse::mount;
|
||||
use markbase_fuse::MarkBaseFs;
|
||||
use std::env::current_dir;
|
||||
|
||||
let mut db_path = current_dir()?;
|
||||
db_path.push(format!("data/users/{}.sqlite", user));
|
||||
|
||||
if !db_path.exists() {
|
||||
return Err(anyhow::anyhow!(
|
||||
"User database not found: {}",
|
||||
db_path.display()
|
||||
));
|
||||
}
|
||||
|
||||
if !dir.exists() {
|
||||
std::fs::create_dir_all(&dir)?;
|
||||
}
|
||||
|
||||
println!("=== MarkBase FUSE (fuse crate + FUSE-T) ===");
|
||||
println!("User: {}", user);
|
||||
println!("Tree Type: {}", tree_type);
|
||||
println!("Database: {}", db_path.display());
|
||||
println!("Mount point: {}", dir.display());
|
||||
println!("");
|
||||
|
||||
let fs = MarkBaseFs::new(&db_path.to_string_lossy(), &tree_type)?;
|
||||
let fs = Arc::new(Mutex::new(fs));
|
||||
|
||||
let options = vec![
|
||||
"-o",
|
||||
"rw",
|
||||
"-o",
|
||||
"allow_other",
|
||||
"-o",
|
||||
"noatime",
|
||||
"-o",
|
||||
"local",
|
||||
"-o",
|
||||
"big_writes",
|
||||
"-o",
|
||||
"max_read=524288",
|
||||
"-o",
|
||||
"max_write=524288",
|
||||
"-o",
|
||||
"kernel_cache",
|
||||
];
|
||||
|
||||
mount(fs, &dir, &options)?;
|
||||
|
||||
println!("Mounted successfully!");
|
||||
println!("Press Ctrl+C to unmount...");
|
||||
|
||||
Ok(())
|
||||
}
|
||||
fn unmount_user(dir: PathBuf) -> Result<()> {
|
||||
println!("Unmounting: {}", dir.display());
|
||||
|
||||
std::process::Command::new("umount").arg(&dir).status()?;
|
||||
|
||||
println!("Unmounted successfully");
|
||||
Ok(())
|
||||
}
|
||||
Reference in New Issue
Block a user