botserver/src/basic/keywords/on_change.rs

457 lines
13 KiB
Rust
Raw Normal View History

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<Self, Self::Err> {
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<String>,
pub recursive: bool,
pub event_types: Vec<String>,
pub script_path: String,
pub enabled: bool,
pub last_check: Option<chrono::DateTime<chrono::Utc>>,
pub last_token: Option<String>,
pub created_at: chrono::DateTime<chrono::Utc>,
pub updated_at: chrono::DateTime<chrono::Utc>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct FolderChangeEvent {
pub path: String,
pub event_type: String,
pub timestamp: chrono::DateTime<chrono::Utc>,
pub size: Option<i64>,
pub is_directory: bool,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct OnChangeConfig {
pub provider: FolderProvider,
pub folder_path: String,
pub folder_id: Option<String>,
pub recursive: bool,
pub event_types: Vec<String>,
pub filters: Option<FileFilters>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct FileFilters {
pub extensions: Option<Vec<String>>,
pub min_size: Option<i64>,
pub max_size: Option<i64>,
pub name_pattern: Option<String>,
}
pub struct OnChangeKeyword;
impl OnChangeKeyword {
pub fn execute(
_state: &AppState,
config: OnChangeConfig,
callback_script: &str,
) -> Result<Value, String> {
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<Vec<FolderChangeEvent>, String> {
info!("Checking for folder changes for monitor {}", monitor_id);
fetch_folder_changes(state, monitor_id)
}
pub fn stop_monitor(monitor_id: Uuid) -> Result<Value, String> {
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<Vec<FolderChangeEvent>, 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<Vec<FolderChangeEvent>, 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<Vec<FolderChangeEvent>, 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<Vec<FolderChangeEvent>, 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<Vec<FolderChangeEvent>, 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<Uuid, String> {
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<Vec<FolderMonitor>, String> {
Ok(Vec::new())
}
fn _apply_filters(events: Vec<FolderChangeEvent>, filters: &Option<FileFilters>) -> Vec<FolderChangeEvent> {
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::<FolderProvider>().unwrap(),
FolderProvider::GDrive
);
assert_eq!(
"onedrive".parse::<FolderProvider>().unwrap(),
FolderProvider::OneDrive
);
assert_eq!(
"dropbox".parse::<FolderProvider>().unwrap(),
FolderProvider::Dropbox
);
assert_eq!(
"local".parse::<FolderProvider>().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"));
}
}