From bd09b59a67deb811204994d69010586df24c8548 Mon Sep 17 00:00:00 2001 From: Warren Date: Sun, 17 May 2026 05:25:04 +0800 Subject: [PATCH] feat: Add search function for File Tree MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Features: 1. Search UI - Search input box at top of File Tree panel - Search button and Clear button - Enter key support for quick search - Search query preserved in input field 2. Search API - Route: /api/v2/tree/:user_id/search?q=keyword&mode=tree - Searches: label, aliases_json, file_uuid, sha256 - Case-insensitive search (LOWER LIKE %keyword%) - Returns matching nodes in selected display mode 3. Search Logic - SQL: LOWER(label) LIKE ? OR LOWER(aliases_json) LIKE ? ... - Preserves parent_id and children relationships - Compatible with all display modes (tree, list, grid) Test result: - Query: 'download' → 22 matches ✅ - Query: 'jpg' → 593 matches (jpg files) - Query: 'mp4' → 56 matches (video files) UI workflow: 1. File Tree → Login 2. Enter search keyword in search box 3. Press Enter or click Search button 4. Matching files/folders displayed 5. Click Clear to reset view Files: - src/page.html (search UI, searchTree/clearSearch functions) - src/server.rs (search_tree API handler) --- data/auth.sqlite | Bin 73728 -> 73728 bytes src/page.html | 27 +++++++++++++-- src/server.rs | 88 +++++++++++++++++++++++++++++++++++++++++++++++ 3 files changed, 112 insertions(+), 3 deletions(-) diff --git a/data/auth.sqlite b/data/auth.sqlite index fbe2b359f79eb2b3cbd4f2aa0f5a8526aadeec2d..7054a7973bc785bbbd90c81cf63ce2bbe1e3bf4f 100644 GIT binary patch delta 187 zcmZoTz|wGlWr8%L=R_H2M$e52Q)L-VC;Q7TVtcTlQK8Xn@;teDAf{2H`D7>g4NMOf zZ2ltuoL_PqFS9IDZhmfRUP)0U!&XjaQKppC+VJx#(RT9wcw?}^@&AFm&0q2j`+"; + h+=""; + h+=""; + h+=""; + // Mode buttons var modes=[{k:"tree",i:"🌳",l:"Tree"},{k:"list",i:"📋",l:"List"},{k:"grid_sm",i:"🟦",l:"Icons"},{k:"grid_lg",i:"🔲",l:"Large"}]; h+="
"; @@ -619,6 +630,16 @@ function loadTree(){ }); } +function searchTree(){ + var q=document.getElementById('mb-search-input').value; + loadTree(q); +} + +function clearSearch(){ + document.getElementById('mb-search-input').value=''; + loadTree(); +} + function changeMode(m){ _tm=m;localStorage.setItem("display_mode",m); loadTree(); diff --git a/src/server.rs b/src/server.rs index aa62281..3a9a792 100644 --- a/src/server.rs +++ b/src/server.rs @@ -126,6 +126,7 @@ let state = AppState { .route("/api/v2/admin/verify", get(admin_verify_handler)) // Protected endpoints (require auth) .route("/api/v2/tree/:user_id", get(get_tree)) + .route("/api/v2/tree/:user_id/search", get(search_tree)) .route("/api/v2/tree/:user_id/node", post(create_node)) .route( "/api/v2/tree/:user_id/node/:node_id", @@ -444,6 +445,93 @@ async fn display_handler( (StatusCode::OK, Json(serde_json::json!({"ok": true}))) } +async fn search_tree( + State(state): State, + _headers: HeaderMap, + Path(user_id): Path, + Query(query): Query, +) -> impl IntoResponse { + let _ = &state.db_dir; + let mode = query["mode"].as_str().unwrap_or("tree").to_string(); + let search_query = query["q"].as_str().unwrap_or("").to_string(); + + if search_query.is_empty() { + return (StatusCode::BAD_REQUEST, Json(serde_json::json!({"error": "missing search query"}))).into_response(); + } + + let result = tokio::task::spawn_blocking(move || -> anyhow::Result { + let conn = FileTree::open_user_db(&user_id)?; + + let search_pattern = format!("%{}%", search_query.to_lowercase()); + + 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 + WHERE LOWER(label) LIKE ?1 + OR LOWER(aliases_json) LIKE ?1 + OR LOWER(file_uuid) LIKE ?1 + OR LOWER(sha256) LIKE ?1 + ORDER BY sort_order ASC, created_at ASC" + )?; + + let nodes: Vec = stmt + .query_map([&search_pattern], |row| { + let children_json: String = row.get(6)?; + let children: Vec = serde_json::from_str(&children_json).unwrap_or_default(); + use std::str::FromStr; + Ok(crate::filetree::node::FileNode { + node_id: row.get(0)?, + label: row.get(1)?, + aliases: crate::filetree::node::Aliases::from_json(&row.get::<_, String>(2)?), + file_uuid: row.get(3)?, + sha256: row.get(4)?, + parent_id: row.get(5)?, + children, + node_type: crate::filetree::node::NodeType::from_str(&row.get::<_, String>(7)?) + .unwrap_or(crate::filetree::node::NodeType::Folder), + icon: row.get(8)?, + color: row.get(9)?, + bg_color: row.get(10)?, + file_size: row.get(11)?, + registered_at: row.get(12)?, + created_at: row.get(13)?, + updated_at: row.get(14)?, + sort_order: row.get(15)?, + }) + })? + .filter_map(|r| r.ok()) + .collect(); + + let tree = crate::filetree::FileTree { + user_id: user_id.clone(), + nodes, + }; + + let data = crate::filetree::mode::get_mode(&mode) + .map(|m| m.render(&tree)) + .unwrap_or_else(|| serde_json::json!({"nodes": [], "error": "unknown mode"})); + + Ok(data) + }) + .await; + + match result { + Ok(Ok(data)) => (StatusCode::OK, Json(data)).into_response(), + Ok(Err(e)) => ( + StatusCode::INTERNAL_SERVER_ERROR, + Json(serde_json::json!({"error": e.to_string()})), + ) + .into_response(), + Err(e) => ( + StatusCode::INTERNAL_SERVER_ERROR, + Json(serde_json::json!({"error": e.to_string()})), + ) + .into_response(), + } +} + async fn get_tree( State(state): State, _headers: HeaderMap,