diff --git a/data/auth.sqlite b/data/auth.sqlite index fbe2b35..7054a79 100644 Binary files a/data/auth.sqlite and b/data/auth.sqlite differ diff --git a/src/page.html b/src/page.html index e331098..3022051 100644 --- a/src/page.html +++ b/src/page.html @@ -571,7 +571,7 @@ function submitTreeLogin(){ }); } -function loadTree(){ +function loadTree(searchQuery){ var b=document.getElementById("mb-tree-body"); if(!b)return; b.innerHTML="
Loading...
"; @@ -579,12 +579,23 @@ function loadTree(){ var token=localStorage.getItem('tree_token'); var user=_tree_user||localStorage.getItem('tree_user')||'demo'; - fetch("/api/v2/tree/"+user+"?mode="+_tm,{ + var url="/api/v2/tree/"+user+"?mode="+_tm; + if(searchQuery && searchQuery.trim()){ + url="/api/v2/tree/"+user+"/search?q="+encodeURIComponent(searchQuery.trim())+"&mode="+_tm; + } + + fetch(url,{ headers:{'Authorization':'Bearer '+token} }).then(function(r){return r.json()}).then(function(d){ - _td=d; _td=d; var h=""; + // Search box + h+="
"; + h+=""; + 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,