use std::sync::Arc; use axum::{ extract::State, http::StatusCode, routing::{get, post}, Json, Router, }; use serde_json::Value; use tracing::{error, info, warn}; use crate::{ auth::AuthUser, models::{ WebDAVConnectionResult, WebDAVCrawlEstimate, WebDAVSyncStatus, WebDAVTestConnection, }, AppState, }; use crate::webdav_service::WebDAVConfig; use crate::webdav_service::WebDAVService; pub mod webdav_sync; use webdav_sync::perform_webdav_sync_with_tracking; pub fn router() -> Router> { Router::new() .route("/test-connection", post(test_webdav_connection)) .route("/estimate-crawl", post(estimate_webdav_crawl)) .route("/sync-status", get(get_webdav_sync_status)) .route("/start-sync", post(start_webdav_sync)) .route("/cancel-sync", post(cancel_webdav_sync)) } async fn get_user_webdav_config(state: &Arc, user_id: uuid::Uuid) -> Result { let settings = state .db .get_user_settings(user_id) .await .map_err(|e| { error!("Failed to get user settings: {}", e); StatusCode::INTERNAL_SERVER_ERROR })?; let settings = settings.unwrap_or_default(); if !settings.webdav_enabled { error!("WebDAV is not enabled for user {}", user_id); return Err(StatusCode::BAD_REQUEST); } let server_url = settings.webdav_server_url.unwrap_or_default(); let username = settings.webdav_username.unwrap_or_default(); let password = settings.webdav_password.unwrap_or_default(); if server_url.is_empty() || username.is_empty() { error!("WebDAV configuration incomplete for user {}", user_id); return Err(StatusCode::BAD_REQUEST); } Ok(WebDAVConfig { server_url, username, password, watch_folders: settings.webdav_watch_folders, file_extensions: settings.webdav_file_extensions, timeout_seconds: 30, // Default timeout server_type: Some("nextcloud".to_string()), // Default to Nextcloud }) } #[utoipa::path( post, path = "/api/webdav/test-connection", tag = "webdav", security( ("bearer_auth" = []) ), request_body = WebDAVTestConnection, responses( (status = 200, description = "Connection test result", body = WebDAVConnectionResult), (status = 400, description = "Invalid request data"), (status = 401, description = "Unauthorized"), (status = 500, description = "Internal server error") ) )] async fn test_webdav_connection( State(state): State>, auth_user: AuthUser, Json(test_config): Json, ) -> Result, StatusCode> { info!("Testing WebDAV connection to: {} for user: {}", test_config.server_url, auth_user.user.username); // Create WebDAV config from test data let webdav_config = WebDAVConfig { server_url: test_config.server_url.clone(), username: test_config.username.clone(), password: test_config.password.clone(), watch_folders: Vec::new(), file_extensions: Vec::new(), timeout_seconds: 30, server_type: test_config.server_type.clone(), }; // Create WebDAV service and test connection match WebDAVService::new(webdav_config) { Ok(webdav_service) => { match webdav_service.test_connection(test_config).await { Ok(result) => { info!("WebDAV connection test completed: {}", result.message); Ok(Json(result)) } Err(e) => { error!("WebDAV connection test failed: {}", e); Ok(Json(WebDAVConnectionResult { success: false, message: format!("Connection test failed: {}", e), server_version: None, server_type: None, })) } } } Err(e) => { error!("Failed to create WebDAV service: {}", e); Ok(Json(WebDAVConnectionResult { success: false, message: format!("Service creation failed: {}", e), server_version: None, server_type: None, })) } } } #[utoipa::path( post, path = "/api/webdav/estimate-crawl", tag = "webdav", security( ("bearer_auth" = []) ), request_body = Value, responses( (status = 200, description = "Crawl estimate", body = WebDAVCrawlEstimate), (status = 400, description = "Invalid request data"), (status = 401, description = "Unauthorized"), (status = 500, description = "Internal server error") ) )] async fn estimate_webdav_crawl( State(state): State>, auth_user: AuthUser, Json(request): Json, ) -> Result, StatusCode> { let folders = request .get("folders") .and_then(|f| f.as_array()) .ok_or(StatusCode::BAD_REQUEST)? .iter() .filter_map(|v| v.as_str().map(|s| s.to_string())) .collect::>(); info!("Estimating crawl for {} folders for user: {}", folders.len(), auth_user.user.username); // Get user's WebDAV configuration let webdav_config = match get_user_webdav_config(&state, auth_user.user.id).await { Ok(config) => config, Err(status_code) => { warn!("Could not get WebDAV config for user {}: {:?}", auth_user.user.id, status_code); return Ok(Json(WebDAVCrawlEstimate { folders: vec![], total_files: 0, total_supported_files: 0, total_estimated_time_hours: 0.0, total_size_mb: 0.0, })); } }; // Create WebDAV service and estimate crawl match WebDAVService::new(webdav_config) { Ok(webdav_service) => { match webdav_service.estimate_crawl(&folders).await { Ok(estimate) => { info!("Crawl estimation completed: {} total files, {} supported files", estimate.total_files, estimate.total_supported_files); Ok(Json(estimate)) } Err(e) => { error!("Crawl estimation failed: {}", e); Ok(Json(WebDAVCrawlEstimate { folders: vec![], total_files: 0, total_supported_files: 0, total_estimated_time_hours: 0.0, total_size_mb: 0.0, })) } } } Err(e) => { error!("Failed to create WebDAV service for crawl estimation: {}", e); Ok(Json(WebDAVCrawlEstimate { folders: vec![], total_files: 0, total_supported_files: 0, total_estimated_time_hours: 0.0, total_size_mb: 0.0, })) } } } #[utoipa::path( get, path = "/api/webdav/sync-status", tag = "webdav", security( ("bearer_auth" = []) ), responses( (status = 200, description = "Current sync status", body = WebDAVSyncStatus), (status = 401, description = "Unauthorized"), (status = 500, description = "Internal server error") ) )] async fn get_webdav_sync_status( State(state): State>, auth_user: AuthUser, ) -> Result, StatusCode> { info!("Getting WebDAV sync status for user: {}", auth_user.user.username); // Check if WebDAV is configured let webdav_config = match get_user_webdav_config(&state, auth_user.user.id).await { Ok(config) => config, Err(_) => { return Ok(Json(WebDAVSyncStatus { is_running: false, last_sync: None, files_processed: 0, files_remaining: 0, current_folder: None, errors: vec!["WebDAV not configured".to_string()], })); } }; // Get sync state from database match state.db.get_webdav_sync_state(auth_user.user.id).await { Ok(Some(sync_state)) => { Ok(Json(WebDAVSyncStatus { is_running: sync_state.is_running, last_sync: sync_state.last_sync_at, files_processed: sync_state.files_processed, files_remaining: sync_state.files_remaining, current_folder: sync_state.current_folder, errors: sync_state.errors, })) } Ok(None) => { // No sync state yet Ok(Json(WebDAVSyncStatus { is_running: false, last_sync: None, files_processed: 0, files_remaining: 0, current_folder: None, errors: Vec::new(), })) } Err(e) => { error!("Failed to get WebDAV sync state: {}", e); Ok(Json(WebDAVSyncStatus { is_running: false, last_sync: None, files_processed: 0, files_remaining: 0, current_folder: None, errors: vec![format!("Error retrieving sync state: {}", e)], })) } } } #[utoipa::path( post, path = "/api/webdav/start-sync", tag = "webdav", security( ("bearer_auth" = []) ), responses( (status = 200, description = "Sync started successfully"), (status = 400, description = "WebDAV not configured or already running"), (status = 401, description = "Unauthorized"), (status = 500, description = "Internal server error") ) )] async fn start_webdav_sync( State(state): State>, auth_user: AuthUser, ) -> Result, StatusCode> { info!("Starting WebDAV sync for user: {}", auth_user.user.username); // Check if a sync is already running for this user match state.db.get_webdav_sync_state(auth_user.user.id).await { Ok(Some(sync_state)) if sync_state.is_running => { warn!("WebDAV sync already running for user {}", auth_user.user.id); return Ok(Json(serde_json::json!({ "success": false, "error": "sync_already_running", "message": "A WebDAV sync is already in progress. Please wait for it to complete before starting a new sync." }))); } Ok(_) => { // No sync running or no sync state exists yet - proceed } Err(e) => { error!("Failed to check sync state for user {}: {}", auth_user.user.id, e); return Err(StatusCode::INTERNAL_SERVER_ERROR); } } // Get user's WebDAV configuration and settings let webdav_config = get_user_webdav_config(&state, auth_user.user.id).await?; let user_settings = state .db .get_user_settings(auth_user.user.id) .await .map_err(|_| StatusCode::INTERNAL_SERVER_ERROR)? .unwrap_or_default(); // Create WebDAV service let webdav_service = WebDAVService::new(webdav_config.clone()) .map_err(|e| { error!("Failed to create WebDAV service: {}", e); StatusCode::INTERNAL_SERVER_ERROR })?; // Start sync in background task let state_clone = state.clone(); let user_id = auth_user.user.id; let enable_background_ocr = user_settings.enable_background_ocr; tokio::spawn(async move { match perform_webdav_sync_with_tracking(state_clone.clone(), user_id, webdav_service, webdav_config, enable_background_ocr).await { Ok(files_processed) => { info!("WebDAV sync completed successfully for user {}: {} files processed", user_id, files_processed); // Send success notification let notification = crate::models::CreateNotification { notification_type: "success".to_string(), title: "Manual WebDAV Sync Completed".to_string(), message: if files_processed > 0 { format!("Successfully processed {} files from manual WebDAV sync", files_processed) } else { "Manual WebDAV sync completed - no new files found".to_string() }, action_url: Some("/documents".to_string()), metadata: Some(serde_json::json!({ "sync_type": "webdav_manual", "files_processed": files_processed })), }; if let Err(e) = state_clone.db.create_notification(user_id, ¬ification).await { error!("Failed to create success notification: {}", e); } } Err(e) => { error!("WebDAV sync failed for user {}: {}", user_id, e); // Send error notification let notification = crate::models::CreateNotification { notification_type: "error".to_string(), title: "Manual WebDAV Sync Failed".to_string(), message: format!("Manual WebDAV sync encountered an error: {}", e), action_url: Some("/settings".to_string()), metadata: Some(serde_json::json!({ "sync_type": "webdav_manual", "error": e.to_string() })), }; if let Err(e) = state_clone.db.create_notification(user_id, ¬ification).await { error!("Failed to create error notification: {}", e); } } } }); Ok(Json(serde_json::json!({ "success": true, "message": "WebDAV sync started successfully" }))) } #[utoipa::path( post, path = "/api/webdav/cancel-sync", tag = "webdav", security( ("bearer_auth" = []) ), responses( (status = 200, description = "Sync cancelled successfully"), (status = 400, description = "No sync running or WebDAV not configured"), (status = 401, description = "Unauthorized"), (status = 500, description = "Internal server error") ) )] async fn cancel_webdav_sync( State(state): State>, auth_user: AuthUser, ) -> Result, StatusCode> { info!("Cancelling WebDAV sync for user: {}", auth_user.user.username); // Check if a sync is currently running match state.db.get_webdav_sync_state(auth_user.user.id).await { Ok(Some(sync_state)) if sync_state.is_running => { // Mark sync as cancelled let cancelled_state = crate::models::UpdateWebDAVSyncState { last_sync_at: Some(chrono::Utc::now()), sync_cursor: sync_state.sync_cursor, is_running: false, files_processed: sync_state.files_processed, files_remaining: 0, current_folder: None, errors: vec!["Sync cancelled by user".to_string()], }; if let Err(e) = state.db.update_webdav_sync_state(auth_user.user.id, &cancelled_state).await { error!("Failed to update sync state for cancellation: {}", e); return Err(StatusCode::INTERNAL_SERVER_ERROR); } info!("WebDAV sync cancelled for user {}", auth_user.user.id); // Send cancellation notification let notification = crate::models::CreateNotification { notification_type: "info".to_string(), title: "WebDAV Sync Cancelled".to_string(), message: "WebDAV sync was cancelled by user request".to_string(), action_url: Some("/settings".to_string()), metadata: Some(serde_json::json!({ "sync_type": "webdav_manual", "cancelled": true })), }; if let Err(e) = state.db.create_notification(auth_user.user.id, ¬ification).await { error!("Failed to create cancellation notification: {}", e); } Ok(Json(serde_json::json!({ "success": true, "message": "WebDAV sync cancelled successfully" }))) } Ok(Some(_)) => { // No sync running warn!("Attempted to cancel WebDAV sync for user {} but no sync is running", auth_user.user.id); Ok(Json(serde_json::json!({ "success": false, "error": "no_sync_running", "message": "No WebDAV sync is currently running" }))) } Ok(None) => { // No sync state exists warn!("No WebDAV sync state found for user {}", auth_user.user.id); Ok(Json(serde_json::json!({ "success": false, "error": "no_sync_state", "message": "No WebDAV sync state found" }))) } Err(e) => { error!("Failed to get sync state for user {}: {}", auth_user.user.id, e); Err(StatusCode::INTERNAL_SERVER_ERROR) } } }