MarkBase架构升级:Multi-Volume Virtual Tree + Dual-View Management + Git Remote修正
Some checks failed
Test / test (push) Has been cancelled
Test / build (push) Has been cancelled

核心功能:
-  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:
Warren
2026-06-12 12:59:54 +08:00
parent 4cb7e80568
commit 1300a4e223
4559 changed files with 195840 additions and 4244 deletions

52
markbase-fskit/src/bin.rs Normal file
View File

@@ -0,0 +1,52 @@
use markbase_fskit::fskit::filesystem::MarkBaseFSFileSystem;
use objc2::rc::Retained;
use objc2_foundation::NSString;
use std::env;
use std::path::Path;
fn main() {
println!("MarkBaseFS Binary Tool");
println!("==================");
println!();
// Parse command line arguments
let args: Vec<String> = env::args().collect();
if args.len() < 2 {
println!("Usage: markbase_fs <user_id> <database_path>");
println!();
println!("This tool creates a MarkBaseFS filesystem instance");
println!();
println!("Arguments:");
println!(" <user_id> - User ID for the filesystem");
println!(" <database_path> - Path to SQLite database file");
println!();
println!("Example:");
println!(" markbase_fs warren /Users/accusys/markbase/data/users/warren.sqlite");
return;
}
let user_id = args[1].clone();
let db_path = Path::new(&args[2]);
println!("Creating MarkBaseFS filesystem...");
println!(" User ID: {}", user_id);
println!(" Database: {}", db_path.display());
// Check if database exists
if !db_path.exists() {
println!(" ✓ Database file exists");
} else {
println!(" ✗ Database file does not exist: {}", db_path.display());
return;
}
// Create filesystem instance
let fs = MarkBaseFSFileSystem::new(user_id);
println!(" ✓ Filesystem instance created");
println!(" ✓ User ID: {}", fs.get_user_id());
println!();
println!("MarkBaseFS filesystem initialized successfully!");
}

View File

@@ -1,247 +1,147 @@
use objc2_foundation::NSString;
use block2::DynBlock;
use objc2::rc::Retained;
use objc2::{define_class, AnyThread, DefinedClass};
use objc2_foundation::{NSError, NSObjectProtocol, NSString};
use objc2_fs_kit::{
FSFileSystem, FSVolume, FSItem,
FSVolumeOperations, FSVolumeReadWriteOperations,
FSContainerIdentifier, FSPathURLResource, FSProbeResult, FSResource, FSTaskOptions,
FSUnaryFileSystem, FSUnaryFileSystemOperations, FSVolume,
};
use rusqlite::Connection;
use std::sync::Mutex;
use std::ptr::null_mut;
pub struct MarkBaseFS {
sqlite: Mutex<Connection>,
use crate::fskit::volume::MarkBaseVolume;
pub struct MarkBaseFSFileSystemIvars {
user_id: String,
}
impl MarkBaseFS {
pub fn new(user_id: &str, db_path: &str) -> Self {
let conn = Connection::open(db_path)
.expect("Failed to open SQLite database");
Self {
sqlite: Mutex::new(conn),
user_id: user_id.to_string(),
}
}
pub fn query_node(&self, node_id: &str) -> Option<FileNodeData> {
let conn = self.sqlite.lock().unwrap();
conn.query_row(
"SELECT node_id, label, node_type, file_size, aliases_json
FROM file_nodes WHERE node_id = ?",
[node_id],
|row| {
Ok(FileNodeData {
node_id: row.get::<_, String>(0)?,
label: row.get::<_, String>(1)?,
node_type: row.get::<_, String>(2)?,
file_size: row.get::<_, Option<i64>>(3)?,
aliases_json: row.get::<_, String>(4)?,
})
},
).ok()
}
pub fn query_children(&self, parent_id: &str) -> Vec<FileNodeData> {
let conn = self.sqlite.lock().unwrap();
let mut stmt = conn.prepare(
"SELECT node_id, label, node_type, file_size, aliases_json
FROM file_nodes WHERE parent_id = ?
ORDER BY sort_order, label"
).unwrap();
stmt.query_map([parent_id], |row| {
Ok(FileNodeData {
node_id: row.get::<_, String>(0)?,
label: row.get::<_, String>(1)?,
node_type: row.get::<_, String>(2)?,
file_size: row.get::<_, Option<i64>>(3)?,
aliases_json: row.get::<_, String>(4)?,
})
}).unwrap()
.filter_map(|r| r.ok())
.collect()
}
pub fn read_file(&self, node_id: &str) -> Option<Vec<u8>> {
let conn = self.sqlite.lock().unwrap();
let aliases_json: String = conn.query_row(
"SELECT aliases_json FROM file_nodes WHERE node_id = ?",
[node_id],
|row| row.get(0),
).unwrap_or_default();
let aliases: serde_json::Value = serde_json::from_str(&aliases_json)
.unwrap_or(serde_json::json!({}));
let file_path = aliases["path"].as_str().unwrap_or_default();
if file_path.is_empty() {
None
} else {
std::fs::read(file_path).ok()
}
}
}
define_class!(
#[unsafe(super(FSUnaryFileSystem))]
#[thread_kind = AnyThread]
#[ivars = MarkBaseFSFileSystemIvars]
#[name = "MarkBaseFS"]
pub struct MarkBaseFSFileSystem;
#[derive(Debug)]
pub struct FileNodeData {
pub node_id: String,
pub label: String,
pub node_type: String,
pub file_size: Option<i64>,
pub aliases_json: String,
}
unsafe impl NSObjectProtocol for MarkBaseFSFileSystem {}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_markbase_fs_creation() {
let fs = MarkBaseFS::new("test", "data/users/test.sqlite");
assert_eq!(fs.user_id, "test");
}
#[test]
fn test_file_node_data() {
let node = FileNodeData {
node_id: "test123".to_string(),
label: "test.txt".to_string(),
node_type: "file".to_string(),
file_size: Some(1024),
aliases_json: "{}".to_string(),
};
assert_eq!(node.node_id, "test123");
assert_eq!(node.label, "test.txt");
}
}
unsafe impl FSUnaryFileSystemOperations for MarkBaseFSFileSystem {
#[unsafe(method(probeResource:replyHandler:))]
unsafe fn probe_resource_reply_handler(
&self,
resource: &FSResource,
reply: &DynBlock<dyn Fn(*mut FSProbeResult, *mut NSError)>,
) {
let path_resource = resource as *const FSResource as *const FSPathURLResource;
let path_resource_ref = &*path_resource;
#[cfg(test)]
mod warren_tests {
use super::*;
#[test]
fn test_warren_database_connection() {
let fs = MarkBaseFS::new("warren", "data/users/warren.sqlite");
assert_eq!(fs.user_id, "warren");
let conn = fs.sqlite.lock().unwrap();
let count: i64 = conn.query_row(
"SELECT COUNT(*) FROM file_nodes",
[],
|row| row.get(0)
).unwrap();
assert_eq!(count, 12659);
}
#[test]
fn test_warren_query_root() {
let fs = MarkBaseFS::new("warren", "data/users/warren.sqlite");
let root_id: String = {
let conn = fs.sqlite.lock().unwrap();
conn.query_row(
"SELECT node_id FROM file_nodes WHERE parent_id IS NULL LIMIT 1",
[],
|row| row.get(0)
).unwrap()
};
let root = fs.query_node(&root_id);
assert!(root.is_some());
let root_node = root.unwrap();
assert_eq!(root_node.node_type, "folder");
println!("Root node: {} - {}", root_node.node_id, root_node.label);
}
#[test]
fn test_warren_query_children() {
let fs = MarkBaseFS::new("warren", "data/users/warren.sqlite");
let root_id: String = {
let conn = fs.sqlite.lock().unwrap();
conn.query_row(
"SELECT node_id FROM file_nodes WHERE parent_id IS NULL LIMIT 1",
[],
|row| row.get(0)
).unwrap()
};
let children = fs.query_children(&root_id);
assert!(children.len() > 0);
let folders = children.iter().filter(|c| c.node_type == "folder").count();
let files = children.iter().filter(|c| c.node_type == "file").count();
println!("Root children: {} folders, {} files", folders, files);
assert!(folders > 0);
}
#[test]
fn test_warren_read_text_file() {
let fs = MarkBaseFS::new("warren", "data/users/warren.sqlite");
let result = {
let conn = fs.sqlite.lock().unwrap();
conn.query_row(
"SELECT node_id, aliases_json FROM file_nodes
WHERE node_type = 'file'
AND aliases_json IS NOT NULL
AND file_size < 1000
LIMIT 1",
[],
|row| Ok((row.get::<_, String>(0)?, row.get::<_, String>(1)?))
).ok()
};
if let Some((node_id, aliases_json)) = result {
let aliases: serde_json::Value = serde_json::from_str(&aliases_json).unwrap();
let path = aliases["path"].as_str().unwrap_or_default();
if !path.is_empty() && std::path::Path::new(path).exists() {
let content = fs.read_file(&node_id);
assert!(content.is_some());
let data = content.unwrap();
assert!(data.len() > 0);
if let Ok(text) = String::from_utf8(data.clone()) {
println!("File content preview: {}", text.chars().take(100).collect::<String>());
let url = path_resource_ref.url();
let path_str = url.path().map(|s| s.to_string()).unwrap_or_default();
let user_id = Self::parse_user_id_from_path(&path_str);
if let Some(user) = user_id {
let db_path = Self::get_db_path_for_user(&user);
if std::path::Path::new(&db_path).exists() {
let probe_result = FSProbeResult::recognizedProbeResultWithName_containerID(
&NSString::from_str(&user),
&FSContainerIdentifier::new(),
);
let result_ptr = Retained::into_raw(probe_result);
reply.call((result_ptr, null_mut()));
} else {
let probe_result = FSProbeResult::usableButLimitedProbeResult();
let result_ptr = Retained::into_raw(probe_result);
reply.call((result_ptr, null_mut()));
}
} else {
let probe_result = FSProbeResult::notRecognizedProbeResult();
let result_ptr = Retained::into_raw(probe_result);
reply.call((result_ptr, null_mut()));
}
}
#[unsafe(method(loadResource:options:replyHandler:))]
unsafe fn load_resource_options_reply_handler(
&self,
resource: &FSResource,
_options: &FSTaskOptions,
reply: &DynBlock<dyn Fn(*mut FSVolume, *mut NSError)>,
) {
let path_resource = resource as *const FSResource as *const FSPathURLResource;
let path_resource_ref = &*path_resource;
let url = path_resource_ref.url();
let path_str = url.path().map(|s| s.to_string()).unwrap_or_default();
let user_id = Self::parse_user_id_from_path(&path_str);
if let Some(user) = user_id {
let db_path = Self::get_db_path_for_user(&user);
if let Ok(conn) = Connection::open(&db_path) {
let volume = MarkBaseVolume::new(conn, user.clone());
let volume_ptr = Retained::into_raw(volume) as *mut FSVolume;
reply.call((volume_ptr, null_mut()));
} else {
reply.call((null_mut(), null_mut()));
}
} else {
reply.call((null_mut(), null_mut()));
}
}
#[unsafe(method(unloadResource:options:replyHandler:))]
unsafe fn unload_resource_options_reply_handler(
&self,
_resource: &FSResource,
_options: &FSTaskOptions,
reply: &DynBlock<dyn Fn(*mut NSError)>,
) {
reply.call((null_mut(),));
}
#[unsafe(method(didFinishLoading))]
unsafe fn did_finish_loading(&self) {}
}
#[test]
fn test_warren_statfs() {
let conn = Connection::open("data/users/warren.sqlite").unwrap();
let total_nodes: i64 = conn.query_row(
"SELECT COUNT(*) FROM file_nodes",
[],
|row| row.get(0)
).unwrap();
let total_size: i64 = conn.query_row(
"SELECT SUM(file_size) FROM file_nodes WHERE file_size IS NOT NULL",
[],
|row| row.get(0)
).unwrap_or(0);
println!("Total nodes: {}", total_nodes);
println!("Total size: {} bytes ({:.2} GB)",
total_size,
total_size as f64 / 1_073_741_824.0
);
assert_eq!(total_nodes, 12659);
assert!(total_size > 0);
);
impl MarkBaseFSFileSystem {
pub fn new(user_id: String) -> Retained<Self> {
let ivars = MarkBaseFSFileSystemIvars { user_id };
let this = Self::alloc().set_ivars(ivars);
unsafe { objc2::msg_send![super(this), init] }
}
}
pub fn get_user_id(&self) -> &str {
&self.ivars().user_id
}
fn parse_user_id_from_path(path: &str) -> Option<String> {
let path = std::path::Path::new(path);
let filename = path.file_name()?.to_str()?;
if filename.ends_with(".sqlite") {
let user_id = filename.trim_end_matches(".sqlite").to_string();
if !user_id.is_empty() {
return Some(user_id);
}
}
None
}
fn get_db_path_for_user(user_id: &str) -> String {
let db_path = format!("data/users/{}.sqlite", user_id);
if std::path::Path::new(&db_path).exists() {
db_path
} else if let Ok(exe_dir) = std::env::current_exe() {
if let Some(parent) = exe_dir.parent() {
let abs_path = parent.join(&db_path);
if abs_path.exists() {
return abs_path.to_string_lossy().to_string();
}
}
db_path
} else {
db_path
}
}
}

View File

@@ -0,0 +1,161 @@
use std::path::Path;
pub struct FrameAlignment {
codec: CodecType,
frame_size: usize,
frame_boundary: usize,
}
#[derive(Debug, Clone, Copy, PartialEq)]
pub enum CodecType {
ProRes,
H264,
HEVC,
DNxHD,
DNxHR,
XAVC,
AVCIntra,
Unknown,
}
impl FrameAlignment {
pub fn detect(file_path: &Path) -> Option<Self> {
let ext = file_path
.extension()
.and_then(|e| e.to_str())
.map(|e| e.to_lowercase());
let codec = match ext.as_deref() {
Some("mov") => CodecType::ProRes,
Some("mp4") => CodecType::H264,
Some("m4v") => CodecType::H264,
Some("mxf") => CodecType::DNxHD,
Some("avi") => CodecType::Unknown,
Some("mkv") => CodecType::Unknown,
_ => CodecType::Unknown,
};
if codec == CodecType::Unknown {
return None;
}
Some(FrameAlignment {
codec,
frame_size: 0,
frame_boundary: 4096,
})
}
pub fn is_aligned(&self, offset: usize, size: usize) -> bool {
if self.frame_size == 0 {
return offset % self.frame_boundary == 0 && size % self.frame_boundary == 0;
}
offset % self.frame_size == 0 && size % self.frame_size == 0
}
pub fn optimal_chunk_size(&self) -> usize {
if self.frame_size == 0 {
return self.frame_boundary;
}
self.frame_size
}
pub fn align_offset(&self, offset: usize) -> usize {
if self.frame_size == 0 {
let boundary = self.frame_boundary;
(offset / boundary) * boundary
} else {
(offset / self.frame_size) * self.frame_size
}
}
pub fn align_size(&self, size: usize) -> usize {
if self.frame_size == 0 {
let boundary = self.frame_boundary;
((size + boundary - 1) / boundary) * boundary
} else {
((size + self.frame_size - 1) / self.frame_size) * self.frame_size
}
}
pub fn get_codec(&self) -> CodecType {
self.codec
}
}
impl Default for FrameAlignment {
fn default() -> Self {
FrameAlignment {
codec: CodecType::Unknown,
frame_size: 0,
frame_boundary: 4096,
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_frame_alignment_prores() {
let path = Path::new("/test/video.mov");
let alignment = FrameAlignment::detect(path);
assert!(alignment.is_some());
let align = alignment.unwrap();
assert_eq!(align.get_codec(), CodecType::ProRes);
}
#[test]
fn test_frame_alignment_h264() {
let path = Path::new("/test/video.mp4");
let alignment = FrameAlignment::detect(path);
assert!(alignment.is_some());
let align = alignment.unwrap();
assert_eq!(align.get_codec(), CodecType::H264);
}
#[test]
fn test_frame_alignment_dnxhd() {
let path = Path::new("/test/video.mxf");
let alignment = FrameAlignment::detect(path);
assert!(alignment.is_some());
let align = alignment.unwrap();
assert_eq!(align.get_codec(), CodecType::DNxHD);
}
#[test]
fn test_frame_alignment_unknown() {
let path = Path::new("/test/document.pdf");
let alignment = FrameAlignment::detect(path);
assert!(alignment.is_none());
}
#[test]
fn test_alignment_boundary() {
let align = FrameAlignment::default();
assert!(align.is_aligned(0, 4096));
assert!(align.is_aligned(4096, 4096));
assert!(!align.is_aligned(100, 4096));
}
#[test]
fn test_align_offset() {
let align = FrameAlignment::default();
assert_eq!(align.align_offset(0), 0);
assert_eq!(align.align_offset(4096), 4096);
assert_eq!(align.align_offset(5000), 4096);
assert_eq!(align.align_offset(8192), 8192);
}
#[test]
fn test_align_size() {
let align = FrameAlignment::default();
assert_eq!(align.align_size(0), 0);
assert_eq!(align.align_size(4096), 4096);
assert_eq!(align.align_size(5000), 8192);
assert_eq!(align.align_size(100), 4096);
}
}

View File

@@ -1,5 +1,7 @@
pub mod filesystem;
pub mod frame_align;
pub mod volume;
pub use filesystem::{MarkBaseFS, FileNodeData};
pub use volume::MarkBaseVolume;
pub use filesystem::MarkBaseFSFileSystem;
pub use frame_align::{CodecType, FrameAlignment};
pub use volume::{MarkBaseFSItem, MarkBaseVolume};

View File

@@ -1,68 +1,736 @@
use block2::DynBlock;
use objc2::rc::Retained;
use objc2::{define_class, AnyThread, DefinedClass};
use objc2_foundation::{NSError, NSInteger, NSObjectProtocol, NSString};
use objc2_fs_kit::{
FSDeactivateOptions, FSDirectoryCookie, FSDirectoryEntryPacker, FSDirectoryVerifier,
FSFileName, FSItem, FSItemAttributes, FSItemGetAttributesRequest, FSItemID,
FSItemSetAttributesRequest, FSItemType, FSMutableFileDataBuffer, FSStatFSResult, FSSyncFlags,
FSTaskOptions, FSVolume, FSVolumeSupportedCapabilities,
};
use rusqlite::Connection;
use std::collections::HashMap;
use std::ptr::null_mut;
use std::slice;
use std::sync::Mutex;
pub struct MarkBaseVolume {
pub struct MarkBaseFSItemIvars {
node_id: String,
item_id: u64,
}
define_class!(
#[unsafe(super(FSItem))]
#[thread_kind = AnyThread]
#[ivars = MarkBaseFSItemIvars]
#[name = "MarkBaseFSItem"]
pub struct MarkBaseFSItem;
unsafe impl NSObjectProtocol for MarkBaseFSItem {}
);
impl MarkBaseFSItem {
pub fn get_node_id(&self) -> &str {
&self.ivars().node_id
}
pub fn get_item_id(&self) -> u64 {
self.ivars().item_id
}
pub fn new(node_id: String, item_id: u64) -> Retained<Self> {
let ivars = MarkBaseFSItemIvars { node_id, item_id };
let this = Self::alloc().set_ivars(ivars);
unsafe { objc2::msg_send![super(this), init] }
}
#[allow(clippy::missing_safety_doc)]
pub unsafe fn from_fs_item(item: &FSItem) -> &Self {
let ptr: *const FSItem = item;
let this_ptr: *const Self = ptr.cast();
&*this_ptr
}
}
pub struct MarkBaseVolumeIvars {
sqlite: Mutex<Connection>,
user_id: String,
root_id: String,
item_map: Mutex<HashMap<u64, String>>,
}
define_class!(
#[unsafe(super(FSVolume))]
#[thread_kind = AnyThread]
#[ivars = MarkBaseVolumeIvars]
#[name = "MarkBaseVolume"]
pub struct MarkBaseVolume;
unsafe impl NSObjectProtocol for MarkBaseVolume {}
impl MarkBaseVolume {
#[unsafe(method(maximumLinkCount))]
fn maximum_link_count(&self) -> NSInteger {
1
}
#[unsafe(method(maximumNameLength))]
fn maximum_name_length(&self) -> NSInteger {
255
}
#[unsafe(method(restrictsOwnershipChanges))]
fn restricts_ownership_changes(&self) -> bool {
false
}
#[unsafe(method(truncatesLongNames))]
fn truncates_long_names(&self) -> bool {
false
}
#[unsafe(method(maximumXattrSize))]
fn maximum_xattr_size(&self) -> NSInteger {
4096
}
#[unsafe(method(maximumXattrSizeInBits))]
fn maximum_xattr_size_in_bits(&self) -> NSInteger {
12
}
#[unsafe(method(maximumFileSize))]
fn maximum_file_size(&self) -> u64 {
u64::MAX
}
#[unsafe(method(maximumFileSizeInBits))]
fn maximum_file_size_in_bits(&self) -> NSInteger {
64
}
#[unsafe(method_id(supportedVolumeCapabilities))]
fn supported_volume_capabilities(&self) -> Retained<FSVolumeSupportedCapabilities> {
let caps = unsafe {
FSVolumeSupportedCapabilities::init(FSVolumeSupportedCapabilities::alloc())
};
unsafe {
caps.setSupportsPersistentObjectIDs(true);
caps.setSupports64BitObjectIDs(true);
}
caps
}
#[unsafe(method_id(volumeStatistics))]
fn volume_statistics(&self) -> Retained<FSStatFSResult> {
let result = unsafe { FSStatFSResult::new() };
let ivars = self.ivars();
let conn = ivars.sqlite.lock().unwrap();
let total_nodes: i64 = conn.query_row(
"SELECT COUNT(*) FROM file_nodes",
[],
|row| row.get(0),
).unwrap_or(0);
let total_size: i64 = conn.query_row(
"SELECT COALESCE(SUM(file_size), 0) FROM file_nodes WHERE file_size IS NOT NULL",
[],
|row| row.get(0),
).unwrap_or(0);
unsafe { result.setTotalFiles(total_nodes as u64) };
unsafe { result.setTotalBytes(total_size as u64) };
unsafe { result.setBlockSize(4096) };
result
}
#[unsafe(method(unmountWithReplyHandler:))]
fn unmount_with_reply_handler(&self, reply: &DynBlock<dyn Fn()>) {
reply.call(());
}
#[unsafe(method(synchronizeWithFlags:replyHandler:))]
fn synchronize_with_flags_reply_handler(
&self,
_flags: FSSyncFlags,
reply: &DynBlock<dyn Fn(*mut NSError)>,
) {
reply.call((null_mut(),));
}
#[unsafe(method(activateWithOptions:replyHandler:))]
fn activate_with_options_reply_handler(
&self,
_options: &FSTaskOptions,
reply: &DynBlock<dyn Fn(*mut FSItem, *mut NSError)>,
) {
let ivars = self.ivars();
let root_id = &ivars.root_id;
let root_item_id = Self::node_id_to_item_id(root_id);
ivars.item_map.lock().unwrap().insert(root_item_id.0, root_id.clone());
let root_item = MarkBaseFSItem::new(root_id.clone(), root_item_id.0);
let root_ptr = Retained::into_raw(root_item) as *mut FSItem;
reply.call((root_ptr, null_mut()));
}
#[unsafe(method(deactivateWithOptions:replyHandler:))]
fn deactivate_with_options_reply_handler(
&self,
_options: FSDeactivateOptions,
reply: &DynBlock<dyn Fn(*mut NSError)>,
) {
reply.call((null_mut(),));
}
#[unsafe(method(lookupItemNamed:inDirectory:replyHandler:))]
fn lookup_item_named_in_directory_reply_handler(
&self,
name: &FSFileName,
directory: &FSItem,
reply: &DynBlock<dyn Fn(*mut FSItem, *mut FSFileName, *mut NSError)>,
) {
let name_nsstr = unsafe { name.string() };
let name_str = name_nsstr
.map(|s| s.to_string())
.unwrap_or_default();
let parent_node_id = unsafe {
let markbase_dir = MarkBaseFSItem::from_fs_item(directory);
markbase_dir.get_node_id().to_string()
};
let ivars = self.ivars();
let conn = ivars.sqlite.lock().unwrap();
let child_result: Option<(String, String, Option<i64>, String)> = conn.query_row(
"SELECT node_id, label, file_size, node_type FROM file_nodes WHERE label = ?1 AND parent_id = ?2 LIMIT 1",
rusqlite::params![&name_str, &parent_node_id],
|row| Ok((
row.get::<_, String>(0)?,
row.get::<_, String>(1)?,
row.get::<_, Option<i64>>(2)?,
row.get::<_, String>(3)?,
)),
).ok();
if let Some((node_id, _label, _file_size, _node_type)) = child_result {
let item_id = Self::node_id_to_item_id(&node_id);
ivars.item_map.lock().unwrap().insert(item_id.0, node_id.clone());
let item = MarkBaseFSItem::new(node_id.clone(), item_id.0);
let name_nsstr2 = NSString::from_str(&name_str);
let file_name = unsafe { FSFileName::initWithString(FSFileName::alloc(), &name_nsstr2) };
let item_ptr = Retained::into_raw(item) as *mut FSItem;
let name_ptr = Retained::into_raw(file_name);
reply.call((item_ptr, name_ptr, null_mut()));
} else {
reply.call((null_mut(), null_mut(), null_mut()));
}
}
#[unsafe(method(getAttributes:ofItem:replyHandler:))]
fn get_attributes_of_item_reply_handler(
&self,
_desired_attributes: &FSItemGetAttributesRequest,
item: &FSItem,
reply: &DynBlock<dyn Fn(*mut FSItemAttributes, *mut NSError)>,
) {
let markbase_item = unsafe { MarkBaseFSItem::from_fs_item(item) };
let node_id = markbase_item.get_node_id();
let ivars = self.ivars();
let conn = ivars.sqlite.lock().unwrap();
let attrs_result: Option<(String, Option<i64>)> = conn.query_row(
"SELECT node_type, file_size FROM file_nodes WHERE node_id = ?",
[node_id],
|row| Ok((
row.get::<_, String>(0)?,
row.get::<_, Option<i64>>(1)?,
)),
).ok();
let attrs = unsafe { FSItemAttributes::new() };
if let Some((node_type, file_size)) = attrs_result {
let item_type = if node_type == "folder" {
FSItemType::Directory
} else {
FSItemType::File
};
unsafe {
attrs.setType(item_type);
attrs.setSize(file_size.unwrap_or(0) as u64);
}
}
let attrs_ptr = Retained::into_raw(attrs);
reply.call((attrs_ptr, null_mut()));
}
#[unsafe(method(enumerateDirectory:startingAtCookie:verifier:providingAttributes:usingPacker:replyHandler:))]
fn enumerate_directory_starting_at_cookie_verifier_providing_attributes_using_packer_reply_handler(
&self,
directory: &FSItem,
cookie: FSDirectoryCookie,
verifier: FSDirectoryVerifier,
_attributes: Option<&FSItemGetAttributesRequest>,
packer: &FSDirectoryEntryPacker,
reply: &DynBlock<dyn Fn(FSDirectoryVerifier, *mut NSError)>,
) {
let parent_node_id = unsafe {
let markbase_dir = MarkBaseFSItem::from_fs_item(directory);
markbase_dir.get_node_id().to_string()
};
let ivars = self.ivars();
let conn = ivars.sqlite.lock().unwrap();
let children_result = conn.prepare(
"SELECT node_id, label, node_type, file_size FROM file_nodes WHERE parent_id = ?1 ORDER BY sort_order, label"
).and_then(|mut stmt| {
let rows = stmt.query_map([&parent_node_id], |row| {
Ok((
row.get::<_, String>(0)?,
row.get::<_, String>(1)?,
row.get::<_, String>(2)?,
row.get::<_, Option<i64>>(3)?
))
})?;
rows.collect::<Result<Vec<_>, _>>()
});
let children = children_result.unwrap_or_default();
let skip_count = if cookie == 0 { 0 } else { cookie as usize };
let mut next_cookie: u64;
for (idx, (node_id, label, node_type, _file_size)) in children.iter().enumerate() {
if idx < skip_count {
continue;
}
let item_type = if node_type == "folder" {
FSItemType::Directory
} else {
FSItemType::File
};
let item_id = Self::node_id_to_item_id(node_id);
ivars.item_map.lock().unwrap().insert(item_id.0, node_id.clone());
next_cookie = (idx + 1) as u64;
let name_nsstr = NSString::from_str(label);
let file_name = unsafe { FSFileName::initWithString(FSFileName::alloc(), &name_nsstr) };
let packed = unsafe {
packer.packEntryWithName_itemType_itemID_nextCookie_attributes(
&file_name,
item_type,
item_id,
next_cookie,
None,
)
};
if !packed {
break;
}
}
reply.call((verifier, null_mut()));
}
#[unsafe(method(readFromFile:offset:length:intoBuffer:replyHandler:))]
fn read_from_file_offset_length_into_buffer_reply_handler(
&self,
item: &FSItem,
offset: i64,
length: usize,
buffer: &FSMutableFileDataBuffer,
reply: &DynBlock<dyn Fn(usize, *mut NSError)>,
) {
let markbase_item = unsafe { MarkBaseFSItem::from_fs_item(item) };
let node_id = markbase_item.get_node_id();
let ivars = self.ivars();
let conn = ivars.sqlite.lock().unwrap();
let aliases_result: Option<String> = conn.query_row(
"SELECT aliases_json FROM file_nodes WHERE node_id = ?",
[node_id],
|row| row.get(0),
).ok();
drop(conn);
let file_path = aliases_result
.and_then(|json: String| {
serde_json::from_str::<serde_json::Value>(&json)
.ok()
.and_then(|v| v["path"].as_str().map(|s| s.to_string()))
});
let buf_len = unsafe { buffer.length() };
let buf_ptr = unsafe { buffer.mutableBytes() };
let buf_ptr_u8: *mut u8 = buf_ptr.as_ptr().cast();
let bytes = unsafe { slice::from_raw_parts_mut(buf_ptr_u8, buf_len) };
let bytes_read = if let Some(path) = file_path {
if std::path::Path::new(&path).exists() {
if let Ok(mut file) = std::fs::File::open(&path) {
use std::io::{Read, Seek};
if file.seek(std::io::SeekFrom::Start(offset as u64)).is_ok() {
let to_read = std::cmp::min(length, bytes.len());
file.read(&mut bytes[..to_read]).unwrap_or_default()
} else {
0
}
} else {
0
}
} else {
0
}
} else {
0
};
reply.call((bytes_read, null_mut()));
}
#[unsafe(method(writeContents:toFile:atOffset:replyHandler:))]
unsafe fn write_contents_to_file_at_offset_reply_handler(
&self,
contents: &objc2_foundation::NSData,
item: &FSItem,
offset: libc::off_t,
reply: &DynBlock<dyn Fn(usize, *mut NSError)>,
) {
let markbase_item = MarkBaseFSItem::from_fs_item(item);
let node_id = markbase_item.get_node_id();
let ivars = self.ivars();
let conn = ivars.sqlite.lock().unwrap();
let aliases_result: Option<String> = conn.query_row(
"SELECT aliases_json FROM file_nodes WHERE node_id = ?",
[node_id],
|row| row.get(0),
).ok();
drop(conn);
let file_path = aliases_result
.and_then(|json: String| {
serde_json::from_str::<serde_json::Value>(&json)
.ok()
.and_then(|v| v["path"].as_str().map(|s| s.to_string()))
});
let bytes_written = if let Some(path) = file_path {
let data_slice = contents.to_vec();
let offset = offset as u64;
if let Ok(mut file) = std::fs::OpenOptions::new()
.write(true)
.create(true)
.truncate(false)
.open(&path)
{
use std::io::{Write, Seek};
if file.seek(std::io::SeekFrom::Start(offset)).is_ok() {
if file.write_all(&data_slice).is_ok() {
data_slice.len()
} else {
0
}
} else {
0
}
} else {
0
}
} else {
0
};
reply.call((bytes_written, null_mut()));
}
#[unsafe(method(createItemNamed:type:inDirectory:attributes:replyHandler:))]
unsafe fn create_item_named_type_in_directory_attributes_reply_handler(
&self,
name: &FSFileName,
r#type: FSItemType,
directory: &FSItem,
_new_attributes: &FSItemSetAttributesRequest,
reply: &DynBlock<dyn Fn(*mut FSItem, *mut FSFileName, *mut NSError)>,
) {
let markbase_dir = MarkBaseFSItem::from_fs_item(directory);
let parent_node_id = markbase_dir.get_node_id();
let name_nsstr = name.string();
let name_str = name_nsstr
.map(|s| s.to_string())
.unwrap_or_default();
let new_node_id = Self::generate_node_id(&name_str);
let item_id = Self::node_id_to_item_id(&new_node_id);
let node_type = if r#type == FSItemType::Directory {
"folder"
} else {
"file"
};
let ivars = self.ivars();
let conn = ivars.sqlite.lock().unwrap();
let insert_result = conn.execute(
"INSERT INTO file_nodes (node_id, label, node_type, parent_id, aliases_json) VALUES (?1, ?2, ?3, ?4, ?5)",
rusqlite::params![&new_node_id, &name_str, &node_type, &parent_node_id, "{}"],
);
if insert_result.is_ok() {
ivars.item_map.lock().unwrap().insert(item_id.0, new_node_id.clone());
let item = MarkBaseFSItem::new(new_node_id, item_id.0);
let name_nsstr2 = NSString::from_str(&name_str);
let file_name = FSFileName::initWithString(FSFileName::alloc(), &name_nsstr2);
let item_ptr = Retained::into_raw(item) as *mut FSItem;
let name_ptr = Retained::into_raw(file_name);
reply.call((item_ptr, name_ptr, null_mut()));
} else {
reply.call((null_mut(), null_mut(), null_mut()));
}
}
#[unsafe(method(removeItem:named:fromDirectory:replyHandler:))]
unsafe fn remove_item_named_from_directory_reply_handler(
&self,
item: &FSItem,
name: &FSFileName,
_directory: &FSItem,
reply: &DynBlock<dyn Fn(*mut NSError)>,
) {
let markbase_item = MarkBaseFSItem::from_fs_item(item);
let node_id = markbase_item.get_node_id();
let ivars = self.ivars();
let conn = ivars.sqlite.lock().unwrap();
let _delete_result = conn.execute(
"DELETE FROM file_nodes WHERE node_id = ?1",
[node_id],
);
ivars.item_map.lock().unwrap().remove(&markbase_item.get_item_id());
reply.call((null_mut(),));
}
#[unsafe(method(reclaimItem:replyHandler:))]
unsafe fn reclaim_item_reply_handler(
&self,
item: &FSItem,
reply: &DynBlock<dyn Fn(*mut NSError)>,
) {
let markbase_item = MarkBaseFSItem::from_fs_item(item);
let node_id = markbase_item.get_node_id();
let ivars = self.ivars();
let conn = ivars.sqlite.lock().unwrap();
let aliases_result: Option<String> = conn.query_row(
"SELECT aliases_json FROM file_nodes WHERE node_id = ?",
[node_id],
|row| row.get(0),
).ok();
drop(conn);
let file_path = aliases_result
.and_then(|json: String| {
serde_json::from_str::<serde_json::Value>(&json)
.ok()
.and_then(|v| v["path"].as_str().map(|s| s.to_string()))
});
if let Some(path) = file_path {
if std::path::Path::new(&path).exists() {
std::fs::remove_file(&path).ok();
}
}
ivars.item_map.lock().unwrap().remove(&markbase_item.get_item_id());
reply.call((null_mut(),));
}
#[unsafe(method(renameItem:inDirectory:named:toNewName:inDirectory:overItem:replyHandler:))]
unsafe fn rename_item_in_directory_named_to_new_name_in_directory_over_item_reply_handler(
&self,
item: &FSItem,
_source_directory: &FSItem,
_source_name: &FSFileName,
new_name: &FSFileName,
_destination_directory: &FSItem,
_over_item: Option<&FSItem>,
reply: &DynBlock<dyn Fn(*mut FSFileName, *mut NSError)>,
) {
let markbase_item = MarkBaseFSItem::from_fs_item(item);
let node_id = markbase_item.get_node_id();
let new_name_nsstr = new_name.string();
let new_name_str = new_name_nsstr
.map(|s| s.to_string())
.unwrap_or_default();
let ivars = self.ivars();
let conn = ivars.sqlite.lock().unwrap();
let update_result = conn.execute(
"UPDATE file_nodes SET label = ?1 WHERE node_id = ?2",
rusqlite::params![&new_name_str, node_id],
);
if update_result.is_ok() {
let name_nsstr = NSString::from_str(&new_name_str);
let file_name = FSFileName::initWithString(FSFileName::alloc(), &name_nsstr);
let name_ptr = Retained::into_raw(file_name);
reply.call((name_ptr, null_mut()));
} else {
reply.call((null_mut(), null_mut()));
}
}
#[unsafe(method(setAttributes:onItem:replyHandler:))]
unsafe fn set_attributes_on_item_reply_handler(
&self,
_new_attributes: &FSItemSetAttributesRequest,
item: &FSItem,
reply: &DynBlock<dyn Fn(*mut FSItemAttributes, *mut NSError)>,
) {
let markbase_item = MarkBaseFSItem::from_fs_item(item);
let node_id = markbase_item.get_node_id();
let ivars = self.ivars();
let conn = ivars.sqlite.lock().unwrap();
let attrs_result: Option<(String, Option<i64>)> = conn.query_row(
"SELECT node_type, file_size FROM file_nodes WHERE node_id = ?",
[node_id],
|row| Ok((
row.get::<_, String>(0)?,
row.get::<_, Option<i64>>(1)?,
)),
).ok();
let attrs = FSItemAttributes::new();
if let Some((node_type, file_size)) = attrs_result {
let item_type = if node_type == "folder" {
FSItemType::Directory
} else {
FSItemType::File
};
attrs.setType(item_type);
attrs.setSize(file_size.unwrap_or(0) as u64);
}
let attrs_ptr = Retained::into_raw(attrs);
reply.call((attrs_ptr, null_mut()));
}
}
);
impl MarkBaseVolume {
pub fn new(conn: Connection, user_id: String) -> Self {
let root_id = Self::find_root_node(&conn, &user_id);
Self {
pub fn new(conn: Connection, user_id: String) -> Retained<Self> {
let root_id = Self::find_root_node(&conn);
let ivars = MarkBaseVolumeIvars {
sqlite: Mutex::new(conn),
user_id,
root_id,
}
item_map: Mutex::new(HashMap::new()),
};
let this = Self::alloc().set_ivars(ivars);
unsafe { objc2::msg_send![super(this), init] }
}
fn find_root_node(conn: &Connection, user_id: &str) -> String {
fn find_root_node(conn: &Connection) -> String {
conn.query_row(
"SELECT node_id FROM file_nodes
WHERE parent_id IS NULL
LIMIT 1",
"SELECT node_id FROM file_nodes WHERE parent_id IS NULL LIMIT 1",
[],
|row| row.get::<_, String>(0),
).unwrap_or_else(|_| "root".to_string())
)
.unwrap_or_else(|_| "root".to_string())
}
pub fn get_root_id(&self) -> &str {
&self.root_id
fn node_id_to_item_id(node_id: &str) -> FSItemID {
let bytes = if node_id.len() >= 16 {
&node_id[0..16]
} else {
node_id
};
let hex_bytes = bytes.as_bytes();
let mut result: u64 = 0;
for i in 0..8 {
if i < hex_bytes.len() {
result |= (hex_bytes[i] as u64) << (i * 8);
}
}
FSItemID(result)
}
fn generate_node_id(name: &str) -> String {
use std::time::{SystemTime, UNIX_EPOCH};
let timestamp = SystemTime::now()
.duration_since(UNIX_EPOCH)
.unwrap()
.as_nanos();
let hash_input = format!("{}{}", name, timestamp);
let mut hasher = std::collections::hash_map::DefaultHasher::new();
use std::hash::{Hash, Hasher};
hash_input.hash(&mut hasher);
let hash = hasher.finish();
format!("{:016x}", hash)
}
pub fn get_user_id(&self) -> &str {
&self.user_id
&self.ivars().user_id
}
pub fn statfs(&self) -> (i64, i64) {
let conn = self.sqlite.lock().unwrap();
let total_nodes: i64 = conn.query_row(
"SELECT COUNT(*) FROM file_nodes",
[],
|row| row.get(0),
).unwrap_or(0);
let total_size: i64 = conn.query_row(
"SELECT SUM(file_size) FROM file_nodes WHERE file_size IS NOT NULL",
[],
|row| row.get(0),
).unwrap_or(0);
(total_nodes, total_size)
pub fn get_root_id(&self) -> &str {
&self.ivars().root_id
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_volume_creation() {
let conn = Connection::open("data/users/test.sqlite").unwrap();
let vol = MarkBaseVolume::new(conn, "test".to_string());
assert_eq!(vol.get_user_id(), "test");
let conn = Connection::open("../data/users/warren.sqlite").unwrap();
let vol = MarkBaseVolume::new(conn, "warren".to_string());
assert_eq!(vol.get_user_id(), "warren");
}
}
}

View File

@@ -0,0 +1 @@
pub mod fskit;