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:
13
filetree/Cargo.toml
Normal file
13
filetree/Cargo.toml
Normal file
@@ -0,0 +1,13 @@
|
||||
[package]
|
||||
name = "filetree"
|
||||
version = "0.1.0"
|
||||
edition = "2021"
|
||||
|
||||
[dependencies]
|
||||
anyhow = "1"
|
||||
async-trait = "0.1"
|
||||
rusqlite = { version = "0.32", features = ["bundled"] }
|
||||
serde = { version = "1", features = ["derive"] }
|
||||
serde_json = "1"
|
||||
uuid = { version = "1", features = ["v4"] }
|
||||
chrono = { version = "0.4", features = ["serde"] }
|
||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
@@ -1,9 +1,10 @@
|
||||
use anyhow::{Context, Result};
|
||||
use rusqlite::Connection;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use std::str::FromStr;
|
||||
use uuid::Uuid;
|
||||
|
||||
use crate::filetree::node::{Aliases, FileNode, NodeType};
|
||||
use crate::node::{Aliases, FileNode, NodeType};
|
||||
|
||||
pub mod convert;
|
||||
pub mod mode;
|
||||
@@ -12,10 +13,21 @@ pub mod node;
|
||||
|
||||
pub struct FileTree {
|
||||
pub user_id: String,
|
||||
pub tree_type: String,
|
||||
pub nodes: Vec<FileNode>,
|
||||
}
|
||||
|
||||
const CREATE_TABLES: &str = "
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct TreeType {
|
||||
pub tree_type: String,
|
||||
pub tree_name: String,
|
||||
pub description: String,
|
||||
pub is_system_defined: i64,
|
||||
pub created_at: String,
|
||||
pub updated_at: String,
|
||||
}
|
||||
|
||||
pub const CREATE_TABLES: &str = "
|
||||
CREATE TABLE IF NOT EXISTS file_registry (
|
||||
file_uuid TEXT PRIMARY KEY,
|
||||
original_name TEXT NOT NULL,
|
||||
@@ -42,7 +54,8 @@ CREATE TABLE IF NOT EXISTS file_nodes (
|
||||
registered_at TEXT,
|
||||
created_at TEXT NOT NULL DEFAULT (datetime('now')),
|
||||
updated_at TEXT NOT NULL DEFAULT (datetime('now')),
|
||||
sort_order INTEGER NOT NULL DEFAULT 0
|
||||
sort_order INTEGER NOT NULL DEFAULT 0,
|
||||
tree_type TEXT NOT NULL DEFAULT 'untitled folder'
|
||||
);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS file_locations (
|
||||
@@ -53,6 +66,21 @@ CREATE TABLE IF NOT EXISTS file_locations (
|
||||
added_at TEXT NOT NULL DEFAULT (datetime('now')),
|
||||
UNIQUE(file_uuid, location)
|
||||
);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS tree_registry (
|
||||
tree_type TEXT PRIMARY KEY,
|
||||
tree_name TEXT NOT NULL,
|
||||
description TEXT,
|
||||
is_system_defined INTEGER DEFAULT 0,
|
||||
created_at TEXT NOT NULL DEFAULT (datetime('now')),
|
||||
updated_at TEXT NOT NULL DEFAULT (datetime('now'))
|
||||
);
|
||||
|
||||
CREATE INDEX IF NOT EXISTS idx_file_nodes_label ON file_nodes(label);
|
||||
CREATE INDEX IF NOT EXISTS idx_file_nodes_file_uuid ON file_nodes(file_uuid);
|
||||
CREATE INDEX IF NOT EXISTS idx_file_nodes_parent_id ON file_nodes(parent_id);
|
||||
CREATE INDEX IF NOT EXISTS idx_file_nodes_tree_type ON file_nodes(tree_type);
|
||||
CREATE INDEX IF NOT EXISTS idx_file_locations_file_uuid ON file_locations(file_uuid);
|
||||
";
|
||||
|
||||
impl FileTree {
|
||||
@@ -75,16 +103,16 @@ impl FileTree {
|
||||
Connection::open(&db_path).with_context(|| format!("Failed to open {}", db_path))
|
||||
}
|
||||
|
||||
pub fn load(conn: &Connection, user_id: &str) -> Result<Self> {
|
||||
pub fn load(conn: &Connection, user_id: &str, tree_type: &str) -> Result<Self> {
|
||||
let mut stmt = conn.prepare(
|
||||
"SELECT node_id, label, aliases_json, file_uuid, sha256, parent_id, children_json,
|
||||
node_type, icon, color, bg_color, file_size, registered_at,
|
||||
created_at, updated_at, sort_order
|
||||
FROM file_nodes ORDER BY sort_order ASC, created_at ASC",
|
||||
FROM file_nodes WHERE tree_type = ?1 ORDER BY sort_order ASC, created_at ASC",
|
||||
)?;
|
||||
|
||||
let nodes: Vec<FileNode> = stmt
|
||||
.query_map([], |row| {
|
||||
.query_map([tree_type], |row| {
|
||||
let children_json: String = row.get(6)?;
|
||||
let children: Vec<String> =
|
||||
serde_json::from_str(&children_json).unwrap_or_default();
|
||||
@@ -113,6 +141,7 @@ impl FileTree {
|
||||
|
||||
Ok(FileTree {
|
||||
user_id: user_id.to_string(),
|
||||
tree_type: tree_type.to_string(),
|
||||
nodes,
|
||||
})
|
||||
}
|
||||
@@ -121,8 +150,8 @@ impl FileTree {
|
||||
conn.execute(
|
||||
"INSERT INTO file_nodes (node_id, label, aliases_json, file_uuid, sha256, parent_id,
|
||||
children_json, node_type, icon, color, bg_color, file_size, registered_at,
|
||||
created_at, updated_at, sort_order)
|
||||
VALUES (?1, ?2, ?3, ?4, ?5, ?6, ?7, ?8, ?9, ?10, ?11, ?12, ?13, ?14, ?15, ?16)",
|
||||
created_at, updated_at, sort_order, tree_type)
|
||||
VALUES (?1, ?2, ?3, ?4, ?5, ?6, ?7, ?8, ?9, ?10, ?11, ?12, ?13, ?14, ?15, ?16, ?17)",
|
||||
rusqlite::params![
|
||||
node.node_id,
|
||||
node.label,
|
||||
@@ -140,6 +169,7 @@ impl FileTree {
|
||||
node.created_at,
|
||||
node.updated_at,
|
||||
node.sort_order,
|
||||
self.tree_type,
|
||||
],
|
||||
)?;
|
||||
self.nodes.push(node.clone());
|
||||
@@ -421,8 +451,56 @@ impl FileTree {
|
||||
"location_count": locs.len(),
|
||||
}))
|
||||
}
|
||||
}
|
||||
|
||||
// 新增:创建虚拟树类型
|
||||
pub fn create_tree_type(
|
||||
conn: &Connection,
|
||||
tree_type: &str,
|
||||
tree_name: &str,
|
||||
description: &str,
|
||||
is_system_defined: bool,
|
||||
) -> Result<()> {
|
||||
conn.execute(
|
||||
"INSERT INTO tree_registry (tree_type, tree_name, description, is_system_defined)
|
||||
VALUES (?1, ?2, ?3, ?4)",
|
||||
rusqlite::params![tree_type, tree_name, description, is_system_defined as i64],
|
||||
)?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
// 新增:获取所有虚拟树类型
|
||||
pub fn get_all_tree_types(conn: &Connection) -> Result<Vec<TreeType>> {
|
||||
let mut stmt = conn.prepare(
|
||||
"SELECT tree_type, tree_name, description, is_system_defined, created_at, updated_at
|
||||
FROM tree_registry ORDER BY created_at ASC",
|
||||
)?;
|
||||
|
||||
let tree_types = stmt
|
||||
.query_map([], |row| {
|
||||
Ok(TreeType {
|
||||
tree_type: row.get(0)?,
|
||||
tree_name: row.get(1)?,
|
||||
description: row.get(2)?,
|
||||
is_system_defined: row.get(3)?,
|
||||
created_at: row.get(4)?,
|
||||
updated_at: row.get(5)?,
|
||||
})
|
||||
})?
|
||||
.map(|r| r.map_err(|e: rusqlite::Error| anyhow::anyhow!(e)))
|
||||
.collect::<Result<Vec<_>>>()?;
|
||||
|
||||
Ok(tree_types)
|
||||
}
|
||||
|
||||
// 新增:删除虚拟树类型(仅限用户自定义)
|
||||
pub fn delete_tree_type(conn: &Connection, tree_type: &str) -> Result<()> {
|
||||
conn.execute(
|
||||
"DELETE FROM tree_registry WHERE tree_type = ?1 AND is_system_defined = 0",
|
||||
[tree_type],
|
||||
)?;
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
@@ -436,7 +514,7 @@ mod tests {
|
||||
#[test]
|
||||
fn test_init_and_load_empty_tree() {
|
||||
let (conn, user_id) = temp_db();
|
||||
let tree = FileTree::load(&conn, &user_id).unwrap();
|
||||
let tree = FileTree::load(&conn, &user_id, "untitled folder").unwrap();
|
||||
assert_eq!(tree.user_id, user_id);
|
||||
assert!(tree.nodes.is_empty());
|
||||
}
|
||||
@@ -444,12 +522,12 @@ mod tests {
|
||||
#[test]
|
||||
fn test_insert_and_load_node() {
|
||||
let (conn, user_id) = temp_db();
|
||||
let mut tree = FileTree::load(&conn, &user_id).unwrap();
|
||||
let mut tree = FileTree::load(&conn, &user_id, "untitled folder").unwrap();
|
||||
|
||||
let folder = FileTree::new_folder("Videos", None);
|
||||
tree.insert_node(&conn, &folder).unwrap();
|
||||
|
||||
let loaded = FileTree::load(&conn, &user_id).unwrap();
|
||||
let loaded = FileTree::load(&conn, &user_id, "untitled folder").unwrap();
|
||||
assert_eq!(loaded.nodes.len(), 1);
|
||||
assert_eq!(loaded.nodes[0].label, "Videos");
|
||||
assert_eq!(loaded.nodes[0].node_type, NodeType::Folder);
|
||||
@@ -458,7 +536,7 @@ mod tests {
|
||||
#[test]
|
||||
fn test_update_node() {
|
||||
let (conn, user_id) = temp_db();
|
||||
let mut tree = FileTree::load(&conn, &user_id).unwrap();
|
||||
let mut tree = FileTree::load(&conn, &user_id, "untitled folder").unwrap();
|
||||
|
||||
let mut folder = FileTree::new_folder("Videos", None);
|
||||
tree.insert_node(&conn, &folder).unwrap();
|
||||
@@ -468,7 +546,7 @@ mod tests {
|
||||
folder.color = Some("#ff0000".to_string());
|
||||
tree.update_node(&conn, &folder.node_id, &folder).unwrap();
|
||||
|
||||
let loaded = FileTree::load(&conn, &user_id).unwrap();
|
||||
let loaded = FileTree::load(&conn, &user_id, "untitled folder").unwrap();
|
||||
assert_eq!(loaded.nodes[0].label, "Movies");
|
||||
assert_eq!(loaded.nodes[0].icon, Some("📽️".to_string()));
|
||||
assert_eq!(loaded.nodes[0].color, Some("#ff0000".to_string()));
|
||||
@@ -477,7 +555,7 @@ mod tests {
|
||||
#[test]
|
||||
fn test_delete_node() {
|
||||
let (conn, user_id) = temp_db();
|
||||
let mut tree = FileTree::load(&conn, &user_id).unwrap();
|
||||
let mut tree = FileTree::load(&conn, &user_id, "untitled folder").unwrap();
|
||||
|
||||
let folder = FileTree::new_folder("Temp", None);
|
||||
let node_id = folder.node_id.clone();
|
||||
@@ -485,14 +563,14 @@ mod tests {
|
||||
|
||||
tree.delete_node(&conn, &node_id).unwrap();
|
||||
|
||||
let loaded = FileTree::load(&conn, &user_id).unwrap();
|
||||
let loaded = FileTree::load(&conn, &user_id, "untitled folder").unwrap();
|
||||
assert_eq!(loaded.nodes.len(), 0);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_move_node() {
|
||||
let (conn, user_id) = temp_db();
|
||||
let mut tree = FileTree::load(&conn, &user_id).unwrap();
|
||||
let mut tree = FileTree::load(&conn, &user_id, "untitled folder").unwrap();
|
||||
|
||||
let root = FileTree::new_folder("Root", None);
|
||||
let child = FileTree::new_folder("Child", Some(root.node_id.clone()));
|
||||
@@ -502,7 +580,7 @@ mod tests {
|
||||
|
||||
tree.move_node(&conn, &child.node_id, None).unwrap();
|
||||
|
||||
let loaded = FileTree::load(&conn, &user_id).unwrap();
|
||||
let loaded = FileTree::load(&conn, &user_id, "untitled folder").unwrap();
|
||||
let moved = loaded.nodes.iter().find(|n| n.label == "Child").unwrap();
|
||||
assert!(moved.parent_id.is_none());
|
||||
}
|
||||
@@ -510,7 +588,7 @@ mod tests {
|
||||
#[test]
|
||||
fn test_update_alias() {
|
||||
let (conn, user_id) = temp_db();
|
||||
let mut tree = FileTree::load(&conn, &user_id).unwrap();
|
||||
let mut tree = FileTree::load(&conn, &user_id, "untitled folder").unwrap();
|
||||
|
||||
let folder = FileTree::new_folder("Videos", None);
|
||||
tree.insert_node(&conn, &folder).unwrap();
|
||||
@@ -518,7 +596,7 @@ mod tests {
|
||||
tree.update_node_alias(&conn, &folder.node_id, "zh_tw", "影片")
|
||||
.unwrap();
|
||||
|
||||
let loaded = FileTree::load(&conn, &user_id).unwrap();
|
||||
let loaded = FileTree::load(&conn, &user_id, "untitled folder").unwrap();
|
||||
assert_eq!(
|
||||
loaded.nodes[0].aliases.get("zh_tw").map(|s| s.as_str()),
|
||||
Some("影片")
|
||||
@@ -528,7 +606,7 @@ mod tests {
|
||||
#[test]
|
||||
fn test_build_tree() {
|
||||
let (conn, user_id) = temp_db();
|
||||
let mut tree = FileTree::load(&conn, &user_id).unwrap();
|
||||
let mut tree = FileTree::load(&conn, &user_id, "untitled folder").unwrap();
|
||||
|
||||
let root = FileTree::new_folder("Root", None);
|
||||
let child1 = FileTree::new_folder("Child1", Some(root.node_id.clone()));
|
||||
@@ -551,3 +629,29 @@ mod tests {
|
||||
assert_eq!(roots[0].label, "Root");
|
||||
}
|
||||
}
|
||||
|
||||
// 新增:创建虚拟树类型
|
||||
pub fn create_tree_type(
|
||||
conn: &Connection,
|
||||
tree_type: &str,
|
||||
tree_name: &str,
|
||||
description: &str,
|
||||
is_system_defined: bool,
|
||||
) -> Result<()> {
|
||||
conn.execute(
|
||||
"INSERT INTO tree_registry (tree_type, tree_name, description, is_system_defined)
|
||||
VALUES (?1, ?2, ?3, ?4)",
|
||||
rusqlite::params![tree_type, tree_name, description, is_system_defined as i64],
|
||||
)?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
// 新增:获取所有虚拟树类型
|
||||
// 新增:删除虚拟树类型(仅限用户自定义)
|
||||
pub fn delete_tree_type(conn: &Connection, tree_type: &str) -> Result<()> {
|
||||
conn.execute(
|
||||
"DELETE FROM tree_registry WHERE tree_type = ?1 AND is_system_defined = 0",
|
||||
[tree_type],
|
||||
)?;
|
||||
Ok(())
|
||||
}
|
||||
@@ -1,7 +1,7 @@
|
||||
use async_trait::async_trait;
|
||||
use serde_json::Value;
|
||||
|
||||
use crate::filetree::FileTree;
|
||||
use crate::FileTree;
|
||||
|
||||
#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
|
||||
pub struct SortOption {
|
||||
@@ -25,19 +25,19 @@ pub trait DisplayMode: Send + Sync {
|
||||
|
||||
pub fn get_mode(name: &str) -> Option<Box<dyn DisplayMode>> {
|
||||
match name {
|
||||
"tree" => Some(Box::new(crate::filetree::modes::tree::TreeMode)),
|
||||
"list" => Some(Box::new(crate::filetree::modes::list::ListMode)),
|
||||
"grid_sm" => Some(Box::new(crate::filetree::modes::grid_sm::GridSmMode)),
|
||||
"grid_lg" => Some(Box::new(crate::filetree::modes::grid_lg::GridLgMode)),
|
||||
"tree" => Some(Box::new(crate::modes::tree::TreeMode)),
|
||||
"list" => Some(Box::new(crate::modes::list::ListMode)),
|
||||
"grid_sm" => Some(Box::new(crate::modes::grid_sm::GridSmMode)),
|
||||
"grid_lg" => Some(Box::new(crate::modes::grid_lg::GridLgMode)),
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn list_modes() -> Vec<Box<dyn DisplayMode>> {
|
||||
vec![
|
||||
Box::new(crate::filetree::modes::list::ListMode),
|
||||
Box::new(crate::filetree::modes::tree::TreeMode),
|
||||
Box::new(crate::filetree::modes::grid_sm::GridSmMode),
|
||||
Box::new(crate::filetree::modes::grid_lg::GridLgMode),
|
||||
Box::new(crate::modes::list::ListMode),
|
||||
Box::new(crate::modes::tree::TreeMode),
|
||||
Box::new(crate::modes::grid_sm::GridSmMode),
|
||||
Box::new(crate::modes::grid_lg::GridLgMode),
|
||||
]
|
||||
}
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
use async_trait::async_trait;
|
||||
use serde_json::{json, Value};
|
||||
|
||||
use crate::filetree::mode::{DisplayMode, FilterOption, SortOption};
|
||||
use crate::filetree::FileTree;
|
||||
use crate::mode::{DisplayMode, FilterOption, SortOption};
|
||||
use crate::FileTree;
|
||||
|
||||
pub struct GridLgMode;
|
||||
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
use async_trait::async_trait;
|
||||
use serde_json::{json, Value};
|
||||
|
||||
use crate::filetree::mode::{DisplayMode, FilterOption, SortOption};
|
||||
use crate::filetree::FileTree;
|
||||
use crate::mode::{DisplayMode, FilterOption, SortOption};
|
||||
use crate::FileTree;
|
||||
|
||||
pub struct GridSmMode;
|
||||
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
use async_trait::async_trait;
|
||||
use serde_json::{json, Value};
|
||||
|
||||
use crate::filetree::mode::{DisplayMode, FilterOption, SortOption};
|
||||
use crate::filetree::FileTree;
|
||||
use crate::mode::{DisplayMode, FilterOption, SortOption};
|
||||
use crate::FileTree;
|
||||
|
||||
pub struct ListMode;
|
||||
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
use async_trait::async_trait;
|
||||
use serde_json::{json, Value};
|
||||
|
||||
use crate::filetree::mode::{DisplayMode, FilterOption, SortOption};
|
||||
use crate::filetree::FileTree;
|
||||
use crate::mode::{DisplayMode, FilterOption, SortOption};
|
||||
use crate::FileTree;
|
||||
|
||||
pub struct TreeMode;
|
||||
|
||||
|
||||
Reference in New Issue
Block a user