rusty_files/server/
api.rs

1use actix_web::{web, HttpResponse, Result};
2use std::time::Instant;
3use std::sync::atomic::Ordering;
4use tracing::{info, error};
5use chrono::Utc;
6
7use crate::{Query, MatchMode, SearchScope, SizeFilter};
8use crate::server::models::*;
9use crate::server::state::AppState;
10
11// ============ Search Endpoint ============
12
13pub async fn search(
14    state: web::Data<AppState>,
15    req: web::Json<SearchRequest>,
16) -> Result<HttpResponse> {
17    let start = Instant::now();
18
19    info!("Search request: {:?}", req.query);
20
21    // Build query from request
22    let query = build_query(&req)?;
23
24    // Execute search
25    let engine = state.engine.read();
26    let results = engine
27        .search_with_query(&query)
28        .map_err(|e| {
29            error!("Search failed: {}", e);
30            actix_web::error::ErrorInternalServerError(e)
31        })?;
32
33    let took_ms = start.elapsed().as_millis() as u64;
34
35    // Record metrics
36    state.metrics.record_search(took_ms);
37
38    // Convert to API response
39    let total = results.len();
40    let has_more = total > req.limit;
41    let results: Vec<FileResult> = results
42        .into_iter()
43        .skip(req.offset)
44        .take(req.limit)
45        .map(convert_result)
46        .collect();
47
48    Ok(HttpResponse::Ok().json(SearchResponse {
49        results,
50        total,
51        took_ms,
52        has_more,
53    }))
54}
55
56// ============ Index Endpoint ============
57
58pub async fn index(
59    state: web::Data<AppState>,
60    req: web::Json<IndexRequest>,
61) -> Result<HttpResponse> {
62    let start = Instant::now();
63
64    info!("Index request: {:?}", req.path);
65
66    // Validate path
67    if !req.path.exists() {
68        return Ok(HttpResponse::BadRequest().json(ErrorResponse {
69            error: "invalid_path".to_string(),
70            message: "Path does not exist".to_string(),
71            code: 400,
72            details: None,
73        }));
74    }
75
76    let engine = state.engine.read();
77
78    let count = engine
79        .index_directory(&req.path, None)
80        .map_err(|e| {
81            error!("Indexing failed: {}", e);
82            actix_web::error::ErrorInternalServerError(e)
83        })?;
84
85    let took_ms = start.elapsed().as_millis() as u64;
86
87    Ok(HttpResponse::Ok().json(IndexResponse {
88        indexed_count: count,
89        skipped_count: 0,
90        error_count: 0,
91        took_ms,
92        status: IndexStatus::Completed,
93    }))
94}
95
96// ============ Update Endpoint ============
97
98pub async fn update(
99    state: web::Data<AppState>,
100    req: web::Json<UpdateRequest>,
101) -> Result<HttpResponse> {
102    let start = Instant::now();
103
104    info!("Update request: {:?}", req.path);
105
106    let engine = state.engine.read();
107
108    let stats = engine
109        .update_index(&req.path, None)
110        .map_err(|e| {
111            error!("Update failed: {}", e);
112            actix_web::error::ErrorInternalServerError(e)
113        })?;
114
115    let took_ms = start.elapsed().as_millis() as u64;
116
117    Ok(HttpResponse::Ok().json(UpdateResponse {
118        added: stats.added,
119        updated: stats.updated,
120        removed: stats.removed,
121        took_ms,
122    }))
123}
124
125// ============ Watch Endpoint ============
126
127pub async fn start_watch(
128    state: web::Data<AppState>,
129    req: web::Json<WatchRequest>,
130) -> Result<HttpResponse> {
131    info!("Watch request: {:?}", req.path);
132
133    let watch_id = uuid::Uuid::new_v4().to_string();
134
135    // Start watching
136    let mut engine = state.engine.write();
137    engine
138        .start_watching(&req.path)
139        .map_err(|e| {
140            error!("Watch failed: {}", e);
141            actix_web::error::ErrorInternalServerError(e)
142        })?;
143
144    // Store watch handle
145    use crate::server::state::WatchHandle;
146    state.watchers.insert(
147        watch_id.clone(),
148        WatchHandle {
149            path: req.path.clone(),
150            recursive: req.recursive,
151            created_at: Utc::now(),
152        },
153    );
154
155    Ok(HttpResponse::Ok().json(WatchResponse {
156        watch_id,
157        path: req.path.clone(),
158        status: "active".to_string(),
159    }))
160}
161
162pub async fn stop_watch(
163    state: web::Data<AppState>,
164    watch_id: web::Path<String>,
165) -> Result<HttpResponse> {
166    info!("Stop watch request: {}", watch_id);
167
168    if let Some((_, handle)) = state.watchers.remove(watch_id.as_str()) {
169        let mut engine = state.engine.write();
170        engine
171            .stop_watching()
172            .map_err(|e| {
173                error!("Stop watch failed: {}", e);
174                actix_web::error::ErrorInternalServerError(e)
175            })?;
176
177        Ok(HttpResponse::Ok().json(serde_json::json!({
178            "message": "Watch stopped",
179            "path": handle.path
180        })))
181    } else {
182        Ok(HttpResponse::NotFound().json(ErrorResponse {
183            error: "not_found".to_string(),
184            message: "Watch ID not found".to_string(),
185            code: 404,
186            details: None,
187        }))
188    }
189}
190
191// ============ Stats Endpoint ============
192
193pub async fn get_stats(state: web::Data<AppState>) -> Result<HttpResponse> {
194    let engine = state.engine.read();
195    let db_stats = engine.get_stats().map_err(|e| {
196        error!("Failed to get stats: {}", e);
197        actix_web::error::ErrorInternalServerError(e)
198    })?;
199
200    Ok(HttpResponse::Ok().json(StatsResponse {
201        total_files: db_stats.total_files,
202        total_directories: db_stats.total_directories,
203        total_size: db_stats.total_size,
204        index_size_mb: db_stats.index_size as f64 / 1_000_000.0,
205        last_update: Some(db_stats.last_update),
206        uptime_seconds: state.uptime_seconds(),
207        performance: PerformanceStats {
208            total_searches: state.metrics.total_searches.load(Ordering::Relaxed),
209            avg_search_time_ms: state.metrics.avg_search_time_ms(),
210            cache_hit_rate: state.metrics.cache_hit_rate(),
211            memory_usage_mb: get_memory_usage_mb(),
212        },
213    }))
214}
215
216// ============ Health Endpoint ============
217
218pub async fn health_check(state: web::Data<AppState>) -> Result<HttpResponse> {
219    let mut checks = Vec::new();
220
221    // Database check
222    let db_check_start = Instant::now();
223    let engine = state.engine.read();
224    let db_healthy = engine.get_stats().is_ok();
225    checks.push(HealthCheck {
226        name: "database".to_string(),
227        status: if db_healthy {
228            HealthStatus::Healthy
229        } else {
230            HealthStatus::Unhealthy
231        },
232        message: None,
233        response_time_ms: Some(db_check_start.elapsed().as_millis() as u64),
234    });
235
236    // Memory check
237    let memory_mb = get_memory_usage_mb();
238    let memory_healthy = memory_mb < 1000.0; // Less than 1GB
239    checks.push(HealthCheck {
240        name: "memory".to_string(),
241        status: if memory_healthy {
242            HealthStatus::Healthy
243        } else {
244            HealthStatus::Degraded
245        },
246        message: Some(format!("{:.2} MB", memory_mb)),
247        response_time_ms: None,
248    });
249
250    let overall_status = if checks
251        .iter()
252        .all(|c| matches!(c.status, HealthStatus::Healthy))
253    {
254        HealthStatus::Healthy
255    } else if checks
256        .iter()
257        .any(|c| matches!(c.status, HealthStatus::Unhealthy))
258    {
259        HealthStatus::Unhealthy
260    } else {
261        HealthStatus::Degraded
262    };
263
264    Ok(HttpResponse::Ok().json(HealthResponse {
265        status: overall_status,
266        version: env!("CARGO_PKG_VERSION").to_string(),
267        uptime_seconds: state.uptime_seconds(),
268        checks,
269    }))
270}
271
272// ============ Helper Functions ============
273
274fn build_query(req: &SearchRequest) -> Result<Query> {
275    let mut query = Query::new(req.query.clone());
276
277    // Set match mode
278    query = match req.mode {
279        SearchMode::Exact => query.with_match_mode(MatchMode::Exact),
280        SearchMode::Fuzzy => query.with_match_mode(MatchMode::Fuzzy),
281        SearchMode::Regex => query.with_match_mode(MatchMode::Regex),
282        SearchMode::Glob => query.with_match_mode(MatchMode::Glob),
283    };
284
285    // Apply filters
286    if let Some(ref extensions) = req.filters.extensions {
287        query = query.with_extensions(extensions.clone());
288    }
289
290    if let Some(size_min) = req.filters.size_min {
291        query = query.with_size_filter(SizeFilter::GreaterThan(size_min));
292    }
293
294    if let Some(ref scope) = req.filters.scope {
295        query = query.with_scope(match scope {
296            crate::server::models::SearchScope::Name => SearchScope::Name,
297            crate::server::models::SearchScope::Path => SearchScope::Path,
298            crate::server::models::SearchScope::Content => SearchScope::Content,
299            crate::server::models::SearchScope::All => SearchScope::All,
300        });
301    }
302
303    // Set limit
304    query = query.with_max_results(req.limit);
305
306    Ok(query)
307}
308
309fn convert_result(result: crate::SearchResult) -> FileResult {
310    FileResult {
311        path: result.file.path.clone(),
312        name: result.file.name.clone(),
313        size: result.file.size,
314        modified: result.file.modified_at.unwrap_or_else(|| Utc::now()),
315        file_type: if result.file.is_directory {
316            FileType::Directory
317        } else if result.file.is_symlink {
318            FileType::Symlink
319        } else {
320            FileType::File
321        },
322        score: result.score as f32,
323        content_preview: result.snippet,
324    }
325}
326
327fn get_memory_usage_mb() -> f64 {
328    #[cfg(target_os = "linux")]
329    {
330        // Read from /proc/self/status
331        if let Ok(status) = std::fs::read_to_string("/proc/self/status") {
332            for line in status.lines() {
333                if line.starts_with("VmRSS:") {
334                    if let Some(kb_str) = line.split_whitespace().nth(1) {
335                        if let Ok(kb) = kb_str.parse::<f64>() {
336                            return kb / 1024.0; // Convert to MB
337                        }
338                    }
339                }
340            }
341        }
342    }
343
344    0.0 // Fallback
345}