diff --git a/markbase-core/src/cli/interface/tree.rs b/markbase-core/src/cli/interface/tree.rs index a5207d6..654a044 100644 --- a/markbase-core/src/cli/interface/tree.rs +++ b/markbase-core/src/cli/interface/tree.rs @@ -1,4 +1,7 @@ use clap::Subcommand; +use rusqlite::Connection; +use anyhow::Context; +use uuid::Uuid; #[derive(Subcommand)] pub enum TreeCommand { @@ -26,22 +29,124 @@ pub enum TreeCommand { #[arg(short, long)] name: String, }, + + Folder { + #[command(subcommand)] + action: FolderCommand, + }, + + Ls { + #[arg(short, long)] + user: String, + #[arg(short, long)] + path: String, + #[arg(short, long)] + tree_type: String, + }, + + Cp { + #[arg(short, long)] + user: String, + #[arg(short, long)] + source: String, + #[arg(short, long)] + target: String, + #[arg(short, long)] + tree_type: String, + }, + + Mv { + #[arg(short, long)] + user: String, + #[arg(short, long)] + source: String, + #[arg(short, long)] + target: String, + #[arg(short, long)] + tree_type: String, + }, +} + +#[derive(Subcommand)] +pub enum FolderCommand { + Create { + #[arg(short, long)] + user: String, + #[arg(short, long)] + path: String, + #[arg(short, long)] + name: String, + #[arg(short, long)] + tree_type: String, + }, + Delete { + #[arg(short, long)] + user: String, + #[arg(short, long)] + path: String, + #[arg(short, long)] + name: String, + #[arg(short, long)] + tree_type: String, + }, + Rename { + #[arg(short, long)] + user: String, + #[arg(short, long)] + path: String, + #[arg(short, long)] + old_name: String, + #[arg(short, long)] + new_name: String, + #[arg(short, long)] + tree_type: String, + }, } pub async fn handle_tree_command(cmd: TreeCommand) -> anyhow::Result<()> { match cmd { TreeCommand::Create { name, user, tree_type } => { - println!("Creating tree: {} (type: {}) for user: {}", name, tree_type, user); - // TODO: 实现tree创建逻辑 + let db_path = format!("data/users/{}.sqlite", user); + let conn = Connection::open(&db_path) + .with_context(|| format!("Failed to open database: {}", db_path))?; + + let node_id = Uuid::new_v4().to_string(); + let created_at = chrono::Utc::now().to_rfc3339(); + + conn.execute( + "INSERT INTO file_nodes (node_id, label, node_type, tree_type, created_at, updated_at) + VALUES (?1, ?2, 'folder', ?3, ?4, ?4)", + rusqlite::params![node_id, name, tree_type, created_at] + ).context("Failed to create tree")?; + + println!("✓ Tree created: {} (type: {}) for user: {}", name, tree_type, user); + println!("✓ Node ID: {}", node_id); } TreeCommand::List { user } => { - println!("Listing trees for user: {}", user); - // TODO: 实现tree列表逻辑 + let db_path = format!("data/users/{}.sqlite", user); + let conn = Connection::open(&db_path) + .with_context(|| format!("Failed to open database: {}", db_path))?; + + let mut stmt = conn.prepare( + "SELECT DISTINCT tree_type FROM file_nodes ORDER BY tree_type" + ).context("Failed to prepare query")?; + + let tree_types = stmt.query_map([], |row| row.get::<_, String>(0)) + .context("Failed to query tree types")?; + + println!("=== Trees for user: {} ===", user); + for tree_type in tree_types { + let tt = tree_type?; + let count: i64 = conn.query_row( + "SELECT COUNT(*) FROM file_nodes WHERE tree_type = ?1", + [&tt], + |row| row.get(0) + ).unwrap_or(0); + + println!(" {} ({} nodes)", tt, count); + } } TreeCommand::Import { user, tree_type } => { - use rusqlite::Connection; - use anyhow::Context; - let db_path = format!("data/users/{}.sqlite", user); let conn = Connection::open(&db_path) .with_context(|| format!("Failed to open database: {}", db_path))?; @@ -50,18 +155,222 @@ pub async fn handle_tree_command(cmd: TreeCommand) -> anyhow::Result<()> { if tree_type == "categories" { crate::import_markdown::import_categories_to_db(&conn, &user, &tree_type)?; - println!("Categories imported successfully!"); + println!("✓ Categories imported successfully!"); } else if tree_type == "series" { crate::import_markdown::import_series_to_db(&conn, &user, &tree_type)?; - println!("Series imported successfully!"); + println!("✓ Series imported successfully!"); } else { eprintln!("Invalid tree_type: {}. Use 'categories' or 'series'", tree_type); } } TreeCommand::Delete { user, name } => { - println!("Deleting tree: {} for user: {}", name, user); - // TODO: 实现tree删除逻辑 + let db_path = format!("data/users/{}.sqlite", user); + let conn = Connection::open(&db_path) + .with_context(|| format!("Failed to open database: {}", db_path))?; + + conn.execute( + "DELETE FROM file_nodes WHERE label = ?1 AND node_type = 'folder'", + [&name] + ).context("Failed to delete tree")?; + + println!("✓ Tree deleted: {} for user: {}", name, user); + } + + TreeCommand::Folder { action } => { + handle_folder_command(action)?; + } + + TreeCommand::Ls { user, path, tree_type } => { + let db_path = format!("data/users/{}.sqlite", user); + let conn = Connection::open(&db_path) + .with_context(|| format!("Failed to open database: {}", db_path))?; + + let parent_id = find_node_id(&conn, &path, &tree_type)?; + + let mut stmt = conn.prepare( + "SELECT label, node_type, file_size FROM file_nodes + WHERE parent_id = ?1 AND tree_type = ?2 + ORDER BY node_type DESC, label ASC" + ).context("Failed to prepare ls query")?; + + let entries = stmt.query_map( + rusqlite::params![parent_id, tree_type], + |row| Ok(( + row.get::<_, String>(0)?, + row.get::<_, String>(1)?, + row.get::<_, Option>(2)? + )) + ).context("Failed to query entries")?; + + println!("=== Contents of {} (tree_type: {}) ===", path, tree_type); + for entry in entries { + let (name, node_type, size) = entry?; + let size_str = size.map(|s| format!("{} bytes", s)).unwrap_or_else(|| "-".to_string()); + + if node_type == "folder" { + println!(" 📁 {} ({})", name, size_str); + } else { + println!(" 📄 {} ({})", name, size_str); + } + } + } + + TreeCommand::Cp { user, source, target, tree_type } => { + let db_path = format!("data/users/{}.sqlite", user); + let conn = Connection::open(&db_path) + .with_context(|| format!("Failed to open database: {}", db_path))?; + + let source_id = find_node_id(&conn, &source, &tree_type)?; + let target_parent_id = find_node_id(&conn, &target, &tree_type)?; + + let (label, node_type, aliases_json, file_uuid, sha256, file_size) = conn.query_row( + "SELECT label, node_type, aliases_json, file_uuid, sha256, file_size + FROM file_nodes WHERE node_id = ?1", + [&source_id], + |row| Ok(( + row.get::<_, String>(0)?, + row.get::<_, String>(1)?, + row.get::<_, String>(2)?, + row.get::<_, Option>(3)?, + row.get::<_, Option>(4)?, + row.get::<_, Option>(5)? + )) + ).context("Failed to get source node")?; + + let new_id = Uuid::new_v4().to_string(); + let created_at = chrono::Utc::now().to_rfc3339(); + + conn.execute( + "INSERT INTO file_nodes + (node_id, label, aliases_json, file_uuid, sha256, parent_id, node_type, file_size, tree_type, created_at, updated_at) + VALUES (?1, ?2, ?3, ?4, ?5, ?6, ?7, ?8, ?9, ?10, ?10)", + rusqlite::params![new_id, label, aliases_json, file_uuid, sha256, target_parent_id, node_type, file_size, tree_type, created_at] + ).context("Failed to copy node")?; + + println!("✓ Copied {} to {} (new ID: {})", source, target, new_id); + } + + TreeCommand::Mv { user, source, target, tree_type } => { + let db_path = format!("data/users/{}.sqlite", user); + let conn = Connection::open(&db_path) + .with_context(|| format!("Failed to open database: {}", db_path))?; + + let source_id = find_node_id(&conn, &source, &tree_type)?; + let target_parent_id = find_node_id(&conn, &target, &tree_type)?; + + let updated_at = chrono::Utc::now().to_rfc3339(); + + conn.execute( + "UPDATE file_nodes SET parent_id = ?1, updated_at = ?2 WHERE node_id = ?3", + rusqlite::params![target_parent_id, updated_at, source_id] + ).context("Failed to move node")?; + + println!("✓ Moved {} to {}", source, target); } } Ok(()) +} + +fn handle_folder_command(cmd: FolderCommand) -> anyhow::Result<()> { + match cmd { + FolderCommand::Create { user, path, name, tree_type } => { + let db_path = format!("data/users/{}.sqlite", user); + let conn = Connection::open(&db_path) + .with_context(|| format!("Failed to open database: {}", db_path))?; + + let parent_id = if path == "/" || path == "" { + None + } else { + Some(find_node_id(&conn, &path, &tree_type)?) + }; + + let node_id = Uuid::new_v4().to_string(); + let created_at = chrono::Utc::now().to_rfc3339(); + + conn.execute( + "INSERT INTO file_nodes + (node_id, label, parent_id, node_type, tree_type, created_at, updated_at) + VALUES (?1, ?2, ?3, 'folder', ?4, ?5, ?5)", + rusqlite::params![node_id, name, parent_id, tree_type, created_at] + ).context("Failed to create folder")?; + + println!("✓ Folder created: {} in {} (tree_type: {})", name, path, tree_type); + println!("✓ Node ID: {}", node_id); + } + FolderCommand::Delete { user, path, name, tree_type } => { + let db_path = format!("data/users/{}.sqlite", user); + let conn = Connection::open(&db_path) + .with_context(|| format!("Failed to open database: {}", db_path))?; + + let folder_path = if path == "/" || path == "" { + name.clone() + } else { + format!("{}/{}", path, name) + }; + + let folder_id = find_node_id(&conn, &folder_path, &tree_type)?; + + conn.execute( + "DELETE FROM file_nodes WHERE node_id = ?1 OR parent_id = ?1", + [&folder_id] + ).context("Failed to delete folder and children")?; + + println!("✓ Folder deleted: {} in {} (tree_type: {})", name, path, tree_type); + } + FolderCommand::Rename { user, path, old_name, new_name, tree_type } => { + let db_path = format!("data/users/{}.sqlite", user); + let conn = Connection::open(&db_path) + .with_context(|| format!("Failed to open database: {}", db_path))?; + + let folder_path = if path == "/" || path == "" { + old_name.clone() + } else { + format!("{}/{}", path, old_name) + }; + + let folder_id = find_node_id(&conn, &folder_path, &tree_type)?; + + let updated_at = chrono::Utc::now().to_rfc3339(); + + conn.execute( + "UPDATE file_nodes SET label = ?1, updated_at = ?2 WHERE node_id = ?3", + rusqlite::params![new_name, updated_at, folder_id] + ).context("Failed to rename folder")?; + + println!("✓ Folder renamed: {} → {} in {} (tree_type: {})", old_name, new_name, path, tree_type); + } + } + Ok(()) +} + +fn find_node_id(conn: &Connection, path: &str, tree_type: &str) -> anyhow::Result { + if path == "/" || path == "" { + let node_id: String = conn.query_row( + "SELECT node_id FROM file_nodes + WHERE parent_id IS NULL AND node_type = 'folder' AND tree_type = ?1 + LIMIT 1", + [tree_type], + |row| row.get(0) + ).context("Failed to find root folder")?; + + return Ok(node_id); + } + + let parts: Vec<&str> = path.split('/').filter(|s| !s.is_empty()).collect(); + + let mut current_parent: Option = None; + + for part in parts { + let node_id: String = conn.query_row( + "SELECT node_id FROM file_nodes + WHERE label = ?1 AND tree_type = ?2 AND + (parent_id = ?3 OR (?3 IS NULL AND parent_id IS NULL))", + rusqlite::params![part, tree_type, current_parent], + |row| row.get(0) + ).context(format!("Failed to find node: {}", part))?; + + current_parent = Some(node_id); + } + + current_parent.context("Failed to find node ID for path") } \ No newline at end of file