use log::info; use serde::{Deserialize, Serialize}; use serde_json::{json, Value}; use std::path::Path; use uuid::Uuid; use crate::core::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) } #[allow(dead_code)] 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::().ok(), Some(FolderProvider::GDrive) ); assert_eq!( "onedrive".parse::().ok(), Some(FolderProvider::OneDrive) ); assert_eq!( "dropbox".parse::().ok(), Some(FolderProvider::Dropbox) ); assert_eq!( "local".parse::().ok(), Some(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")); } }