已完成: ✅ App ID(6770506571) ✅ Bundle ID(com.momentry.markbase.fskit) ✅ Developer ID Application证书导入 ✅ .app Bundle创建(build/MarkBaseFSKit.app) ✅ entitlements.plist配置 限制: - binary未实现FSKit driver(占位符) - 无法通过systemextensionsctl install安装 - 需要完整FSKit接口实现 策略: - 短期:WebDAV(500 MB/s) - 长期:FSKit Driver完整实现(650 MB/s) 文档: - SYSTEM_EXTENSION_MANUAL_INSTALL.md - FSKIT_DRIVER_TODO.md(未来待办)
243 lines
7.8 KiB
Rust
243 lines
7.8 KiB
Rust
use std::collections::HashMap;
|
|
use std::io;
|
|
use std::path::PathBuf;
|
|
use std::sync::Mutex;
|
|
|
|
use rusqlite::Connection;
|
|
use vfs::{FileSystem, VfsMetadata, VfsResult, VfsFileType, SeekAndRead, SeekAndWrite};
|
|
use vfs::error::VfsErrorKind;
|
|
|
|
fn rusqlite_to_io_error(e: rusqlite::Error) -> io::Error {
|
|
io::Error::new(io::ErrorKind::Other, e.to_string())
|
|
}
|
|
|
|
#[derive(Debug)]
|
|
pub struct MarkBaseFS {
|
|
user_id: String,
|
|
db_path: PathBuf,
|
|
conn: Mutex<Connection>,
|
|
path_cache: Mutex<HashMap<String, String>>,
|
|
}
|
|
|
|
struct FileNode {
|
|
node_id: String,
|
|
label: String,
|
|
node_type: String,
|
|
parent_id: Option<String>,
|
|
aliases_json: Option<String>,
|
|
file_size: Option<i64>,
|
|
}
|
|
|
|
impl MarkBaseFS {
|
|
pub fn new(user_id: String, db_path: PathBuf) -> VfsResult<Self> {
|
|
let conn = Connection::open(&db_path)
|
|
.map_err(|e| VfsErrorKind::IoError(rusqlite_to_io_error(e)))?;
|
|
|
|
Ok(MarkBaseFS {
|
|
user_id,
|
|
db_path,
|
|
conn: Mutex::new(conn),
|
|
path_cache: Mutex::new(HashMap::new()),
|
|
})
|
|
}
|
|
|
|
fn resolve_path(&self, path: &str) -> VfsResult<FileNode> {
|
|
if path == "" || path == "/" {
|
|
return Ok(FileNode {
|
|
node_id: "root".to_string(),
|
|
label: "".to_string(),
|
|
node_type: "folder".to_string(),
|
|
parent_id: None,
|
|
aliases_json: None,
|
|
file_size: None,
|
|
});
|
|
}
|
|
|
|
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<String> = None;
|
|
let mut current_node: Option<FileNode> = None;
|
|
|
|
for part in parts {
|
|
let query = if current_parent.is_none() {
|
|
"SELECT node_id, label, node_type, parent_id, aliases_json, file_size
|
|
FROM file_nodes
|
|
WHERE parent_id IS NULL AND label = ?1"
|
|
} else {
|
|
"SELECT node_id, label, node_type, parent_id, aliases_json, file_size
|
|
FROM file_nodes
|
|
WHERE parent_id = ?1 AND label = ?2"
|
|
};
|
|
|
|
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 {
|
|
node_id: row.get(0)?,
|
|
label: row.get(1)?,
|
|
node_type: row.get(2)?,
|
|
parent_id: row.get(3)?,
|
|
aliases_json: row.get(4)?,
|
|
file_size: row.get(5)?,
|
|
})
|
|
}).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| {
|
|
Ok(FileNode {
|
|
node_id: row.get(0)?,
|
|
label: row.get(1)?,
|
|
node_type: row.get(2)?,
|
|
parent_id: row.get(3)?,
|
|
aliases_json: row.get(4)?,
|
|
file_size: row.get(5)?,
|
|
})
|
|
}).map_err(|e| rusqlite_to_io_error(e))
|
|
};
|
|
|
|
match node {
|
|
Ok(n) => {
|
|
current_parent = Some(n.node_id.clone());
|
|
current_node = Some(n);
|
|
}
|
|
Err(_) => return Err(VfsErrorKind::FileNotFound.into()),
|
|
}
|
|
}
|
|
|
|
current_node.ok_or(VfsErrorKind::FileNotFound.into())
|
|
}
|
|
}
|
|
|
|
impl FileSystem for MarkBaseFS {
|
|
fn read_dir(&self, path: &str) -> VfsResult<Box<dyn Iterator<Item = String> + Send>> {
|
|
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)
|
|
.map_err(|e| VfsErrorKind::IoError(rusqlite_to_io_error(e)))?;
|
|
|
|
let children: Vec<String> = if parent_id.is_none() {
|
|
stmt.query_map([], |row| row.get::<_, String>(0))
|
|
.map_err(|e| rusqlite_to_io_error(e))?
|
|
.collect::<Result<Vec<_>, _>>()
|
|
.map_err(|e| rusqlite_to_io_error(e))?
|
|
} else {
|
|
stmt.query_map([parent_id.unwrap()], |row| row.get::<_, String>(0))
|
|
.map_err(|e| rusqlite_to_io_error(e))?
|
|
.collect::<Result<Vec<_>, _>>()
|
|
.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<Box<dyn SeekAndRead + Send>> {
|
|
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 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<Box<dyn SeekAndWrite + Send>> {
|
|
Err(VfsErrorKind::NotSupported.into())
|
|
}
|
|
|
|
fn append_file(&self, _path: &str) -> VfsResult<Box<dyn SeekAndWrite + Send>> {
|
|
Err(VfsErrorKind::NotSupported.into())
|
|
}
|
|
|
|
fn metadata(&self, path: &str) -> VfsResult<VfsMetadata> {
|
|
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,
|
|
created: None,
|
|
modified: None,
|
|
accessed: None,
|
|
})
|
|
}
|
|
|
|
fn exists(&self, path: &str) -> VfsResult<bool> {
|
|
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())
|
|
}
|
|
}
|
|
|
|
#[cfg(test)]
|
|
mod tests {
|
|
use super::*;
|
|
use vfs::FileSystem;
|
|
|
|
#[test]
|
|
fn test_markbase_fs_creation() {
|
|
let fs = MarkBaseFS::new(
|
|
"warren".to_string(),
|
|
PathBuf::from("data/users/warren.sqlite"),
|
|
);
|
|
assert!(fs.is_ok());
|
|
}
|
|
|
|
#[test]
|
|
fn test_resolve_root() {
|
|
let fs = MarkBaseFS::new(
|
|
"warren".to_string(),
|
|
PathBuf::from("data/users/warren.sqlite"),
|
|
).unwrap();
|
|
|
|
let node = fs.resolve_path("");
|
|
assert!(node.is_ok());
|
|
assert_eq!(node.unwrap().node_type, "folder");
|
|
}
|
|
} |