use log::info; use serde::{Deserialize, Serialize}; use serde_json::{json, Value}; use std::path::Path; use uuid::Uuid; use crate::shared::state::AppState; #[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)] pub enum FolderProvider { GDrive, OneDrive, Dropbox, Local, } impl std::str::FromStr for FolderProvider { type Err = (); fn from_str(s: &str) -> Result { match s.to_lowercase().as_str() { "gdrive" | "google" | "googledrive" => Ok(FolderProvider::GDrive), "onedrive" | "microsoft" => Ok(FolderProvider::OneDrive), "dropbox" => Ok(FolderProvider::Dropbox), "local" | "filesystem" => Ok(FolderProvider::Local), _ => Err(()), } } } impl std::fmt::Display for FolderProvider { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { match self { FolderProvider::GDrive => write!(f, "gdrive"), FolderProvider::OneDrive => write!(f, "onedrive"), FolderProvider::Dropbox => write!(f, "dropbox"), FolderProvider::Local => write!(f, "local"), } } } #[derive(Debug, Clone, Serialize, Deserialize)] pub struct FolderMonitor { pub id: Uuid, pub user_id: Uuid, pub org_id: Uuid, pub provider: FolderProvider, pub folder_path: String, pub folder_id: Option, pub recursive: bool, pub event_types: Vec, pub script_path: String, pub enabled: bool, pub last_check: Option>, pub last_token: Option, pub created_at: chrono::DateTime, pub updated_at: chrono::DateTime, } #[derive(Debug, Clone, Serialize, Deserialize)] pub struct FolderChangeEvent { pub path: String, pub event_type: String, pub timestamp: chrono::DateTime, pub size: Option, pub is_directory: bool, } #[derive(Debug, Clone, Serialize, Deserialize)] pub struct OnChangeConfig { pub provider: FolderProvider, pub folder_path: String, pub folder_id: Option, pub recursive: bool, pub event_types: Vec, pub filters: Option, } #[derive(Debug, Clone, Serialize, Deserialize)] pub struct FileFilters { pub extensions: Option>, pub min_size: Option, pub max_size: Option, pub name_pattern: Option, } pub struct OnChangeKeyword; impl OnChangeKeyword { pub fn execute( _state: &AppState, config: OnChangeConfig, callback_script: &str, ) -> Result { info!( "Setting up folder monitor for {:?} at {}", config.provider, config.folder_path ); let monitor = FolderMonitor { id: Uuid::new_v4(), user_id: Uuid::new_v4(), org_id: Uuid::new_v4(), provider: config.provider, folder_path: config.folder_path.clone(), folder_id: config.folder_id.clone(), recursive: config.recursive, event_types: config.event_types.clone(), script_path: callback_script.to_string(), enabled: true, last_check: None, last_token: None, created_at: chrono::Utc::now(), updated_at: chrono::Utc::now(), }; Ok(json!({ "success": true, "monitor_id": monitor.id, "provider": monitor.provider.to_string(), "folder_path": monitor.folder_path, "message": "Folder monitor configured (simulation mode)" })) } pub fn check_changes( state: &AppState, monitor_id: Uuid, ) -> Result, String> { info!("Checking for folder changes for monitor {}", monitor_id); fetch_folder_changes(state, monitor_id) } pub fn stop_monitor(monitor_id: Uuid) -> Result { info!("Stopping folder monitor {}", monitor_id); Ok(json!({ "success": true, "monitor_id": monitor_id, "message": "Monitor stopped" })) } } pub fn fetch_folder_changes( _state: &AppState, _monitor_id: Uuid, ) -> Result, String> { let now = chrono::Utc::now(); let events = vec![ FolderChangeEvent { path: "documents/report.pdf".to_string(), event_type: "modified".to_string(), timestamp: now, size: Some(125000), is_directory: false, }, FolderChangeEvent { path: "documents/new_file.docx".to_string(), event_type: "created".to_string(), timestamp: now, size: Some(45000), is_directory: false, }, ]; info!( "Folder change check: returning {} simulated events (real APIs require OAuth setup)", events.len() ); Ok(events) } fn _fetch_local_changes( folder_path: &str, _recursive: bool, event_types: &[String], ) -> Result, String> { let now = chrono::Utc::now(); let include_created = event_types.is_empty() || event_types.iter().any(|e| e == "created" || e == "all"); let include_modified = event_types.is_empty() || event_types.iter().any(|e| e == "modified" || e == "all"); let mut events = Vec::new(); if include_modified { events.push(FolderChangeEvent { path: format!("{}/example.txt", folder_path), event_type: "modified".to_string(), timestamp: now, size: Some(1024), is_directory: false, }); } if include_created { events.push(FolderChangeEvent { path: format!("{}/new_document.pdf", folder_path), event_type: "created".to_string(), timestamp: now, size: Some(50000), is_directory: false, }); } info!("Local folder monitoring: returning {} simulated events", events.len()); Ok(events) } fn _fetch_gdrive_changes( _state: &AppState, folder_id: Option<&str>, _last_token: Option<&str>, event_types: &[String], ) -> Result, String> { let now = chrono::Utc::now(); let include_created = event_types.is_empty() || event_types.iter().any(|e| e == "created" || e == "all"); let include_modified = event_types.is_empty() || event_types.iter().any(|e| e == "modified" || e == "all"); let mut events = Vec::new(); if include_created { events.push(FolderChangeEvent { path: folder_id.map(|f| format!("{}/new_document.docx", f)).unwrap_or_else(|| "new_document.docx".to_string()), event_type: "created".to_string(), timestamp: now, size: Some(15000), is_directory: false, }); } if include_modified { events.push(FolderChangeEvent { path: folder_id.map(|f| format!("{}/report.pdf", f)).unwrap_or_else(|| "report.pdf".to_string()), event_type: "modified".to_string(), timestamp: now, size: Some(250000), is_directory: false, }); } info!("GDrive folder monitoring: returning {} simulated events (requires OAuth setup for real API)", events.len()); Ok(events) } fn _fetch_onedrive_changes( _state: &AppState, folder_id: Option<&str>, _last_token: Option<&str>, event_types: &[String], ) -> Result, String> { let now = chrono::Utc::now(); let include_created = event_types.is_empty() || event_types.iter().any(|e| e == "created" || e == "all"); let include_modified = event_types.is_empty() || event_types.iter().any(|e| e == "modified" || e == "all"); let mut events = Vec::new(); if include_created { events.push(FolderChangeEvent { path: folder_id.map(|f| format!("{}/spreadsheet.xlsx", f)).unwrap_or_else(|| "spreadsheet.xlsx".to_string()), event_type: "created".to_string(), timestamp: now, size: Some(35000), is_directory: false, }); } if include_modified { events.push(FolderChangeEvent { path: folder_id.map(|f| format!("{}/presentation.pptx", f)).unwrap_or_else(|| "presentation.pptx".to_string()), event_type: "modified".to_string(), timestamp: now, size: Some(500000), is_directory: false, }); } info!("OneDrive folder monitoring: returning {} simulated events (requires OAuth setup for real API)", events.len()); Ok(events) } fn _fetch_dropbox_changes( _state: &AppState, folder_path: &str, _last_token: Option<&str>, event_types: &[String], ) -> Result, String> { let now = chrono::Utc::now(); let include_created = event_types.is_empty() || event_types.iter().any(|e| e == "created" || e == "all"); let include_modified = event_types.is_empty() || event_types.iter().any(|e| e == "modified" || e == "all"); let mut events = Vec::new(); if include_created { events.push(FolderChangeEvent { path: format!("{}/backup.zip", folder_path), event_type: "created".to_string(), timestamp: now, size: Some(1500000), is_directory: false, }); } if include_modified { events.push(FolderChangeEvent { path: format!("{}/notes.md", folder_path), event_type: "modified".to_string(), timestamp: now, size: Some(8000), is_directory: false, }); } info!("Dropbox folder monitoring: returning {} simulated events (requires OAuth setup for real API)", events.len()); Ok(events) } pub fn process_folder_event( _state: &AppState, event: &FolderChangeEvent, script_path: &str, ) -> Result<(), String> { info!( "Processing folder event ({}) for {} with script {}", event.event_type, event.path, script_path ); Ok(()) } pub fn register_folder_trigger( _state: &AppState, config: OnChangeConfig, _callback_script: &str, ) -> Result { let monitor_id = Uuid::new_v4(); info!( "Registered folder trigger {} for {:?} at {} (simulation mode)", monitor_id, config.provider, config.folder_path ); Ok(monitor_id) } pub fn unregister_folder_trigger(_state: &AppState, monitor_id: Uuid) -> Result<(), String> { info!("Unregistered folder trigger {}", monitor_id); Ok(()) } pub fn list_folder_triggers(_state: &AppState, _user_id: Uuid) -> Result, String> { Ok(Vec::new()) } fn _apply_filters(events: Vec, filters: &Option) -> Vec { let Some(filters) = filters else { return events; }; events .into_iter() .filter(|event| { if let Some(ref extensions) = filters.extensions { let ext = Path::new(&event.path) .extension() .and_then(|e| e.to_str()) .unwrap_or(""); if !extensions.iter().any(|e| e.eq_ignore_ascii_case(ext)) { return false; } } if let Some(min_size) = filters.min_size { if event.size.unwrap_or(0) < min_size { return false; } } if let Some(max_size) = filters.max_size { if event.size.unwrap_or(i64::MAX) > max_size { return false; } } if let Some(ref pattern) = filters.name_pattern { let file_name = Path::new(&event.path) .file_name() .and_then(|n| n.to_str()) .unwrap_or(""); if !file_name.contains(pattern) { return false; } } true }) .collect() } #[cfg(test)] mod tests { use super::*; #[test] fn test_folder_provider_from_str() { assert_eq!( "gdrive".parse::().unwrap(), FolderProvider::GDrive ); assert_eq!( "onedrive".parse::().unwrap(), FolderProvider::OneDrive ); assert_eq!( "dropbox".parse::().unwrap(), FolderProvider::Dropbox ); assert_eq!( "local".parse::().unwrap(), FolderProvider::Local ); } #[test] fn test_apply_filters_extension() { let events = vec![ FolderChangeEvent { path: "test.pdf".to_string(), event_type: "created".to_string(), timestamp: chrono::Utc::now(), size: Some(1000), is_directory: false, }, FolderChangeEvent { path: "test.txt".to_string(), event_type: "created".to_string(), timestamp: chrono::Utc::now(), size: Some(500), is_directory: false, }, ]; let filters = Some(FileFilters { extensions: Some(vec!["pdf".to_string()]), min_size: None, max_size: None, name_pattern: None, }); let filtered = apply_filters(events, &filters); assert_eq!(filtered.len(), 1); assert!(filtered[0].path.ends_with(".pdf")); } }