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:
52
markbase-fskit/src/bin.rs
Normal file
52
markbase-fskit/src/bin.rs
Normal 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!");
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
161
markbase-fskit/src/fskit/frame_align.rs
Normal file
161
markbase-fskit/src/fskit/frame_align.rs
Normal 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);
|
||||
}
|
||||
}
|
||||
@@ -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};
|
||||
|
||||
@@ -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");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
1
markbase-fskit/src/lib.rs
Normal file
1
markbase-fskit/src/lib.rs
Normal file
@@ -0,0 +1 @@
|
||||
pub mod fskit;
|
||||
Reference in New Issue
Block a user