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,