use anyhow::Result; use clap::Parser; use tracing::{info, warn, error, Level}; use tracing_subscriber; use caldav_sync::{Config, CalDavResult, SyncEngine}; use caldav_sync::nextcloud_import::{ImportEngine, ImportBehavior}; use caldav_sync::minicaldav_client::CalendarEvent; use std::path::PathBuf; use chrono::{Utc, Duration}; #[derive(Parser)] #[command(name = "caldav-sync")] #[command(about = "A CalDAV calendar synchronization tool")] #[command(version)] struct Cli { /// Configuration file path #[arg(short, long, default_value = "config/config.toml")] config: PathBuf, /// CalDAV server URL (overrides config file) #[arg(short, long)] server_url: Option, /// Username for authentication (overrides config file) #[arg(short, long)] username: Option, /// Password for authentication (overrides config file) #[arg(short, long)] password: Option, /// Calendar name to sync (overrides config file) #[arg(long)] calendar: Option, /// Enable debug logging #[arg(short, long)] debug: bool, /// Perform a one-time sync and exit #[arg(long)] once: bool, /// Force a full resynchronization #[arg(long)] full_resync: bool, /// List events and exit #[arg(long)] list_events: bool, /// List available calendars and exit #[arg(long)] list_calendars: bool, /// Use specific CalDAV approach (report-simple, propfind-depth, simple-propfind, multiget, report-filter, ical-export, zoho-export, zoho-events-list, zoho-events-direct) #[arg(long)] approach: Option, /// Use specific calendar URL instead of discovering from config #[arg(long)] calendar_url: Option, /// Show detailed import-relevant information for calendars #[arg(long)] import_info: bool, /// Import events into Nextcloud calendar #[arg(long)] import_nextcloud: bool, /// Target calendar name for Nextcloud import (overrides config) #[arg(long)] nextcloud_calendar: Option, /// Import behavior: strict, strict_with_cleanup #[arg(long, default_value = "strict")] import_behavior: String, /// Dry run - show what would be imported without actually doing it #[arg(long)] dry_run: bool, /// List events from import target calendar and exit #[arg(long)] list_import_events: bool, } #[tokio::main] async fn main() -> Result<()> { let cli = Cli::parse(); // Initialize logging let log_level = if cli.debug { Level::DEBUG } else { Level::INFO }; tracing_subscriber::fmt() .with_max_level(log_level) .with_target(false) .compact() .init(); info!("Starting CalDAV synchronization tool v{}", env!("CARGO_PKG_VERSION")); // Load configuration let mut config = match Config::from_file(&cli.config) { Ok(config) => { info!("Loaded configuration from: {}", cli.config.display()); config } Err(e) => { warn!("Failed to load config file: {}", e); info!("Using default configuration and environment variables"); Config::from_env()? } }; // Override configuration with command line arguments if let Some(ref server_url) = cli.server_url { config.server.url = server_url.clone(); } if let Some(ref username) = cli.username { config.server.username = username.clone(); } if let Some(ref password) = cli.password { config.server.password = password.clone(); } if let Some(ref calendar) = cli.calendar { config.calendar.name = calendar.clone(); } // Validate configuration if let Err(e) = config.validate() { error!("Configuration validation failed: {}", e); return Err(e.into()); } info!("Server URL: {}", config.server.url); info!("Username: {}", config.server.username); info!("Calendar: {}", config.calendar.name); // Initialize and run synchronization match run_sync(config, &cli).await { Ok(_) => { info!("CalDAV synchronization completed successfully"); } Err(e) => { error!("CalDAV synchronization failed: {}", e); return Err(e.into()); } } Ok(()) } async fn run_sync(config: Config, cli: &Cli) -> CalDavResult<()> { if cli.list_calendars { // List calendars and exit info!("Listing available calendars from server"); if cli.import_info { println!("šŸ” Import Analysis Report"); println!("========================\n"); // Show source calendars (current configuration) println!("šŸ“¤ SOURCE CALENDARS (Zoho/Current Server)"); println!("=========================================="); // Get calendars from the source server - handle errors gracefully let source_calendars = match SyncEngine::new(config.clone()).await { Ok(sync_engine) => { match sync_engine.client.discover_calendars().await { Ok(calendars) => { Some(calendars) } Err(e) => { println!("āš ļø Failed to discover source calendars: {}", e); println!("Source server may be unavailable or credentials may be incorrect.\n"); None } } } Err(e) => { println!("āš ļø Failed to connect to source server: {}", e); println!("Source server configuration may need checking.\n"); None } }; let target_calendar_name = &config.calendar.name; if let Some(ref calendars) = source_calendars { println!("Found {} source calendars:", calendars.len()); println!("Current source calendar: {}\n", target_calendar_name); for (i, calendar) in calendars.iter().enumerate() { let is_target = calendar.name == *target_calendar_name || calendar.display_name.as_ref().map_or(false, |dn| dn == target_calendar_name); // Calendar header with target indicator if is_target { println!(" {}. {} šŸŽÆ [CURRENT SOURCE]", i + 1, calendar.display_name.as_ref().unwrap_or(&calendar.name)); } else { println!(" {}. {}", i + 1, calendar.display_name.as_ref().unwrap_or(&calendar.name)); } // Basic information println!(" Name: {}", calendar.name); println!(" URL: {}", calendar.url); if let Some(ref display_name) = calendar.display_name { println!(" Display Name: {}", display_name); } // Import-relevant information if let Some(ref color) = calendar.color { println!(" Color: {}", color); } if let Some(ref description) = calendar.description { println!(" Description: {}", description); } if let Some(ref timezone) = calendar.timezone { println!(" Timezone: {}", timezone); } // Supported components - crucial for export compatibility let components = &calendar.supported_components; println!(" Supported Components: {}", components.join(", ")); // Export suitability analysis let supports_events = components.contains(&"VEVENT".to_string()); let supports_todos = components.contains(&"VTODO".to_string()); let supports_journals = components.contains(&"VJOURNAL".to_string()); println!(" šŸ“¤ Export Analysis:"); println!(" Event Support: {}", if supports_events { "āœ… Yes" } else { "āŒ No" }); println!(" Task Support: {}", if supports_todos { "āœ… Yes" } else { "āŒ No" }); println!(" Journal Support: {}", if supports_journals { "āœ… Yes" } else { "āŒ No" }); // Server type detection if calendar.url.contains("/zoho/") || calendar.url.contains("zoho.com") { println!(" Server Type: šŸ”µ Zoho"); println!(" CalDAV Standard: āš ļø Partially Compliant"); println!(" Special Features: Zoho-specific APIs available"); } else { println!(" Server Type: šŸ”§ Generic CalDAV"); println!(" CalDAV Standard: āœ… Likely Compliant"); } println!(); } } else { println!("āš ļø Could not retrieve source calendars"); println!("Please check your source server configuration:\n"); println!(" URL: {}", config.server.url); println!(" Username: {}", config.server.username); println!(" Calendar: {}\n", config.calendar.name); } // Show target import calendars if configured if let Some(ref import_config) = config.get_import_config() { println!("šŸ“„ TARGET IMPORT CALENDARS (Nextcloud/Destination)"); println!("================================================="); println!("Configured target server: {}", import_config.target_server.url); println!("Configured target calendar: {}\n", import_config.target_calendar.name); // Create a temporary config for the target server let mut target_config = config.clone(); target_config.server.url = import_config.target_server.url.clone(); target_config.server.username = import_config.target_server.username.clone(); target_config.server.password = import_config.target_server.password.clone(); target_config.server.timeout = import_config.target_server.timeout; target_config.server.use_https = import_config.target_server.use_https; target_config.server.headers = import_config.target_server.headers.clone(); println!("Attempting to connect to target server..."); // Try to connect to target server and list calendars match SyncEngine::new(target_config).await { Ok(target_sync_engine) => { println!("āœ… Successfully connected to target server!"); match target_sync_engine.client.discover_calendars().await { Ok(target_calendars) => { println!("Found {} target calendars:", target_calendars.len()); for (i, calendar) in target_calendars.iter().enumerate() { let is_target = calendar.name == import_config.target_calendar.name || calendar.display_name.as_ref().map_or(false, |dn| *dn == import_config.target_calendar.name); // Calendar header with target indicator if is_target { println!(" {}. {} šŸŽÆ [IMPORT TARGET]", i + 1, calendar.display_name.as_ref().unwrap_or(&calendar.name)); } else { println!(" {}. {}", i + 1, calendar.display_name.as_ref().unwrap_or(&calendar.name)); } // Basic information println!(" Name: {}", calendar.name); println!(" URL: {}", calendar.url); if let Some(ref display_name) = calendar.display_name { println!(" Display Name: {}", display_name); } // Import-relevant information if let Some(ref color) = calendar.color { println!(" Color: {}", color); } if let Some(ref description) = calendar.description { println!(" Description: {}", description); } if let Some(ref timezone) = calendar.timezone { println!(" Timezone: {}", timezone); } // Supported components - crucial for import compatibility let components = &calendar.supported_components; println!(" Supported Components: {}", components.join(", ")); // Import suitability analysis let supports_events = components.contains(&"VEVENT".to_string()); let supports_todos = components.contains(&"VTODO".to_string()); let supports_journals = components.contains(&"VJOURNAL".to_string()); println!(" šŸ“„ Import Analysis:"); println!(" Event Support: {}", if supports_events { "āœ… Yes" } else { "āŒ No" }); println!(" Task Support: {}", if supports_todos { "āœ… Yes" } else { "āŒ No" }); println!(" Journal Support: {}", if supports_journals { "āœ… Yes" } else { "āŒ No" }); // Server type detection if calendar.url.contains("/remote.php/dav/calendars/") { println!(" Server Type: ā˜ļø Nextcloud"); println!(" CalDAV Standard: āœ… RFC 4791 Compliant"); println!(" Recommended: āœ… High compatibility"); println!(" Special Features: Full SabreDAV support"); } else { println!(" Server Type: šŸ”§ Generic CalDAV"); println!(" CalDAV Standard: āœ… Likely Compliant"); } // Additional Nextcloud-specific checks if calendar.url.contains("/remote.php/dav/calendars/") && supports_events { println!(" āœ… Ready for Nextcloud event import"); } else if !supports_events { println!(" āš ļø This calendar doesn't support events - not suitable for import"); } println!(); } // Import compatibility summary let target_calendar = target_calendars.iter() .find(|c| c.name == import_config.target_calendar.name || c.display_name.as_ref().map_or(false, |dn| *dn == import_config.target_calendar.name)); if let Some(target_cal) = target_calendar { let supports_events = target_cal.supported_components.contains(&"VEVENT".to_string()); let is_nextcloud = target_cal.url.contains("/remote.php/dav/calendars/"); println!("šŸ“‹ IMPORT READINESS SUMMARY"); println!("============================"); println!("Target Calendar: {}", target_cal.display_name.as_ref().unwrap_or(&target_cal.name)); println!("Supports Events: {}", if supports_events { "āœ… Yes" } else { "āŒ No" }); println!("Server Type: {}", if is_nextcloud { "ā˜ļø Nextcloud" } else { "šŸ”§ Generic CalDAV" }); if supports_events { if is_nextcloud { println!("Overall Status: āœ… Excellent - Nextcloud with full event support"); } else { println!("Overall Status: āœ… Good - Generic CalDAV with event support"); } } else { println!("Overall Status: āŒ Not suitable - No event support"); } } else { println!("āš ļø Target calendar '{}' not found on server", import_config.target_calendar.name); println!("Available calendars:"); for calendar in &target_calendars { println!(" - {}", calendar.display_name.as_ref().unwrap_or(&calendar.name)); } } } Err(e) => { println!("āŒ Failed to discover calendars on target server: {}", e); println!("The server connection was successful, but calendar discovery failed."); println!("Please check your import configuration:"); println!(" URL: {}", import_config.target_server.url); println!(" Username: {}", import_config.target_server.username); println!(" Target Calendar: {}", import_config.target_calendar.name); } } } Err(e) => { println!("āŒ Failed to connect to target server: {}", e); println!("Please check your import configuration:"); println!(" URL: {}", import_config.target_server.url); println!(" Username: {}", import_config.target_server.username); println!(" Target Calendar: {}", import_config.target_calendar.name); // Provide guidance based on the error if e.to_string().contains("401") || e.to_string().contains("Unauthorized") { println!(""); println!("šŸ’” Troubleshooting tips:"); println!(" - Check username and password"); println!(" - For Nextcloud with 2FA, use app-specific passwords"); println!(" - Verify the URL format: https://your-nextcloud.com/remote.php/dav/calendars/username/"); } else if e.to_string().contains("404") || e.to_string().contains("Not Found") { println!(""); println!("šŸ’” Troubleshooting tips:"); println!(" - Verify the Nextcloud URL is correct"); println!(" - Check if CalDAV is enabled in Nextcloud settings"); println!(" - Ensure the username is correct (case-sensitive)"); } else if e.to_string().contains("timeout") || e.to_string().contains("connection") { println!(""); println!("šŸ’” Troubleshooting tips:"); println!(" - Check network connectivity"); println!(" - Verify the Nextcloud server is accessible"); println!(" - Try increasing timeout value in configuration"); } } } } else { println!("šŸ“„ No import target configured"); println!("To configure import target, add [import] section to config.toml:"); println!(""); println!("[import]"); println!("[import.target_server]"); println!("url = \"https://your-nextcloud.com/remote.php/dav/calendars/user\""); println!("username = \"your-username\""); println!("password = \"your-password\""); println!("[import.target_calendar]"); println!("name = \"Imported-Zoho-Events\""); println!("enabled = true"); } } else { // Regular calendar listing (original behavior) - only if not import_info let sync_engine = SyncEngine::new(config.clone()).await?; let calendars = sync_engine.client.discover_calendars().await?; println!("Found {} calendars:", calendars.len()); for (i, calendar) in calendars.iter().enumerate() { println!(" {}. {}", i + 1, calendar.display_name.as_ref().unwrap_or(&calendar.name)); println!(" Name: {}", calendar.name); println!(" URL: {}", calendar.url); if let Some(ref color) = calendar.color { println!(" Color: {}", color); } if let Some(ref description) = calendar.description { println!(" Description: {}", description); } if let Some(ref timezone) = calendar.timezone { println!(" Timezone: {}", timezone); } println!(" Supported Components: {}", calendar.supported_components.join(", ")); println!(); } } return Ok(()); } // Handle Nextcloud import if cli.import_nextcloud { info!("Starting Nextcloud import process"); // Validate import configuration let import_config = match config.get_import_config() { Some(config) => config, None => { error!("No import target configured. Please add [import] section to config.toml"); return Err(anyhow::anyhow!("Import configuration not found").into()); } }; // Parse import behavior let behavior = match cli.import_behavior.parse::() { Ok(behavior) => behavior, Err(e) => { error!("Invalid import behavior '{}': {}", cli.import_behavior, e); return Err(anyhow::anyhow!("Invalid import behavior").into()); } }; // Override target calendar if specified via CLI let target_calendar_name = cli.nextcloud_calendar.as_ref() .unwrap_or(&import_config.target_calendar.name); info!("Importing to calendar: {}", target_calendar_name); info!("Import behavior: {}", behavior); info!("Dry run: {}", cli.dry_run); // Create import engine let import_engine = ImportEngine::new(import_config, behavior, cli.dry_run); // Get source events from the source calendar info!("Retrieving events from source calendar..."); let mut source_sync_engine = match SyncEngine::new(config.clone()).await { Ok(engine) => engine, Err(e) => { error!("Failed to connect to source server: {}", e); return Err(e.into()); } }; // Perform sync to get events let _sync_result = match source_sync_engine.sync_full().await { Ok(result) => result, Err(e) => { error!("Failed to sync events from source: {}", e); return Err(e.into()); } }; let source_events = source_sync_engine.get_local_events(); info!("Retrieved {} events from source calendar", source_events.len()); if source_events.is_empty() { info!("No events found in source calendar to import"); return Ok(()); } // Convert source events to import events (Event type conversion needed) // TODO: For now, we'll simulate with test events since Event types might differ let import_events: Vec = source_events .iter() .enumerate() .map(|(_i, event)| { // Convert CalendarEvent to Event for import // This is a simplified conversion - you may need to adjust based on actual Event structure caldav_sync::event::Event { uid: event.id.clone(), summary: event.summary.clone(), description: event.description.clone(), start: event.start, end: event.end, all_day: false, // TODO: Extract from event data location: event.location.clone(), status: caldav_sync::event::EventStatus::Confirmed, // TODO: Extract from event event_type: caldav_sync::event::EventType::Public, // TODO: Extract from event organizer: None, // TODO: Extract from event attendees: Vec::new(), // TODO: Extract from event recurrence: None, // TODO: Extract from event alarms: Vec::new(), // TODO: Extract from event properties: std::collections::HashMap::new(), created: event.last_modified.unwrap_or_else(Utc::now), last_modified: event.last_modified.unwrap_or_else(Utc::now), sequence: 0, // TODO: Extract from event timezone: event.start_tzid.clone(), } }) .collect(); // Perform import match import_engine.import_events(import_events).await { Ok(result) => { // Display import results println!("\nšŸŽ‰ Import Completed Successfully!"); println!("====================================="); println!("Target Calendar: {}", result.target_calendar); println!("Import Behavior: {}", result.behavior); println!("Dry Run: {}", if result.dry_run { "Yes" } else { "No" }); println!(); if let Some(duration) = result.duration() { println!("Duration: {}ms", duration.num_milliseconds()); } println!("Results:"); println!(" Total events processed: {}", result.total_events); println!(" Successfully imported: {}", result.imported); println!(" Skipped: {}", result.skipped); println!(" Failed: {}", result.failed); println!(" Success rate: {:.1}%", result.success_rate()); if !result.errors.is_empty() { println!("\nāš ļø Errors encountered:"); for error in &result.errors { println!(" - {}: {}", error.event_summary.as_deref().unwrap_or("Unknown event"), error.message); } } if !result.conflicts.is_empty() { println!("\nšŸ”„ Conflicts resolved:"); for conflict in &result.conflicts { println!(" - {}: {:?}", conflict.event_summary, conflict.resolution); } } if result.dry_run { println!("\nšŸ’” This was a dry run. No actual changes were made."); println!(" Run without --dry-run to perform the actual import."); } } Err(e) => { error!("Import failed: {}", e); return Err(e.into()); } } return Ok(()); } // Handle listing events from import target calendar if cli.list_import_events { info!("Listing events from import target calendar"); // Validate import configuration let import_config = match config.get_import_config() { Some(config) => config, None => { error!("No import target configured. Please add [import] section to config.toml"); return Err(anyhow::anyhow!("Import configuration not found").into()); } }; // Override target calendar if specified via CLI let target_calendar_name = cli.nextcloud_calendar.as_ref() .unwrap_or(&import_config.target_calendar.name); println!("šŸ“… Events from Import Target Calendar"); println!("====================================="); println!("Target Server: {}", import_config.target_server.url); println!("Target Calendar: {}\n", target_calendar_name); // Create a temporary config for the target server let mut target_config = config.clone(); target_config.server.url = import_config.target_server.url.clone(); target_config.server.username = import_config.target_server.username.clone(); target_config.server.password = import_config.target_server.password.clone(); target_config.server.timeout = import_config.target_server.timeout; target_config.server.use_https = import_config.target_server.use_https; target_config.server.headers = import_config.target_server.headers.clone(); target_config.calendar.name = target_calendar_name.clone(); // Connect to target server let target_sync_engine = match SyncEngine::new(target_config).await { Ok(engine) => engine, Err(e) => { error!("Failed to connect to target server: {}", e); println!("āŒ Failed to connect to target server: {}", e); println!("Please check your import configuration:"); println!(" URL: {}", import_config.target_server.url); println!(" Username: {}", import_config.target_server.username); println!(" Target Calendar: {}", target_calendar_name); return Err(e.into()); } }; println!("āœ… Successfully connected to target server!"); // Discover calendars to find the target calendar URL let target_calendars = match target_sync_engine.client.discover_calendars().await { Ok(calendars) => calendars, Err(e) => { error!("Failed to discover calendars on target server: {}", e); println!("āŒ Failed to discover calendars: {}", e); return Err(e.into()); } }; // Find the target calendar let target_calendar = target_calendars.iter() .find(|c| c.name == *target_calendar_name || c.display_name.as_ref().map_or(false, |dn| dn == target_calendar_name)); let target_calendar = match target_calendar { Some(calendar) => { println!("āœ… Found target calendar: {}", calendar.display_name.as_ref().unwrap_or(&calendar.name)); calendar } None => { println!("āŒ Target calendar '{}' not found on server", target_calendar_name); println!("Available calendars:"); for calendar in &target_calendars { println!(" - {}", calendar.display_name.as_ref().unwrap_or(&calendar.name)); } return Err(anyhow::anyhow!("Target calendar not found").into()); } }; // Check if calendar supports events let supports_events = target_calendar.supported_components.contains(&"VEVENT".to_string()); if !supports_events { println!("āŒ Target calendar does not support events"); println!("Supported components: {}", target_calendar.supported_components.join(", ")); return Err(anyhow::anyhow!("Calendar does not support events").into()); } // Set date range for event listing (past 30 days to next 30 days) let now = Utc::now(); let start_date = now - Duration::days(30); let end_date = now + Duration::days(30); println!("\nRetrieving events from {} to {}...", start_date.format("%Y-%m-%d"), end_date.format("%Y-%m-%d")); // Get events from the target calendar using the full URL let events: Vec = match target_sync_engine.client.get_events(&target_calendar.url, start_date, end_date).await { Ok(events) => events, Err(e) => { error!("Failed to retrieve events from target calendar: {}", e); println!("āŒ Failed to retrieve events: {}", e); return Err(e.into()); } }; println!("\nšŸ“Š Event Summary"); println!("================"); println!("Total events found: {}", events.len()); if events.is_empty() { println!("\nNo events found in the specified date range."); return Ok(()); } // Count events by status and other properties let mut confirmed_events = 0; let mut tentative_events = 0; let mut cancelled_events = 0; let mut all_day_events = 0; let mut events_with_location = 0; let mut upcoming_events = 0; let mut past_events = 0; for event in &events { // Count by status if let Some(ref status) = event.status { match status.to_lowercase().as_str() { "confirmed" => confirmed_events += 1, "tentative" => tentative_events += 1, "cancelled" => cancelled_events += 1, _ => {} } } // Check if all-day (simple heuristic) if event.start.time() == chrono::NaiveTime::from_hms_opt(0, 0, 0).unwrap() && event.end.time() == chrono::NaiveTime::from_hms_opt(23, 59, 59).unwrap_or(chrono::NaiveTime::from_hms_opt(0, 0, 0).unwrap()) { all_day_events += 1; } // Count events with locations if let Some(ref location) = event.location { if !location.is_empty() { events_with_location += 1; } } // Count upcoming vs past events if event.end > now { upcoming_events += 1; } else { past_events += 1; } } println!(" Confirmed: {}", confirmed_events); println!(" Tentative: {}", tentative_events); println!(" Cancelled: {}", cancelled_events); println!(" All-day: {}", all_day_events); println!(" With location: {}", events_with_location); println!(" Upcoming: {}", upcoming_events); println!(" Past: {}", past_events); // Display detailed event information println!("\nšŸ“… Event Details"); println!("================="); // Sort events by start time let mut sorted_events = events.clone(); sorted_events.sort_by(|a, b| a.start.cmp(&b.start)); for (i, event) in sorted_events.iter().enumerate() { println!("\n{}. {}", i + 1, event.summary); // Format dates and times let start_formatted = event.start.format("%Y-%m-%d %H:%M"); let end_formatted = event.end.format("%Y-%m-%d %H:%M"); println!(" šŸ“… {} to {}", start_formatted, end_formatted); // Event ID println!(" šŸ†” ID: {}", event.id); // Status let status_icon = if let Some(ref status) = event.status { match status.to_lowercase().as_str() { "confirmed" => "āœ…", "tentative" => "šŸ”„", "cancelled" => "āŒ", _ => "ā“", } } else { "ā“" }; let status_display = event.status.as_deref().unwrap_or("Unknown"); println!(" šŸ“Š Status: {} {}", status_icon, status_display); // Location if let Some(ref location) = event.location { if !location.is_empty() { println!(" šŸ“ Location: {}", location); } } // Description (truncated if too long) if let Some(ref description) = event.description { if !description.is_empty() { let truncated = if description.len() > 100 { format!("{}...", &description[..97]) } else { description.clone() }; println!(" šŸ“ Description: {}", truncated); } } // ETag for synchronization info if let Some(ref etag) = event.etag { println!(" šŸ·ļø ETag: {}", etag); } } // Import analysis println!("\nšŸ” Import Analysis"); println!("=================="); println!("This target calendar contains {} events.", events.len()); if cli.import_info { println!("\nBased on the strict unidirectional import behavior:"); println!("- These events would be checked against source events"); println!("- Events not present in source would be deleted (if using strict_with_cleanup)"); println!("- Events present in both would be updated if source is newer"); println!("- New events from source would be added to this calendar"); println!("\nRecommendations:"); if events.len() > 100 { println!("- āš ļø Large number of events - consider using strict behavior first"); } if cancelled_events > 0 { println!("- šŸ—‘ļø {} cancelled events could be cleaned up", cancelled_events); } if past_events > events.len() / 2 { println!("- šŸ“š Many past events - consider cleanup if not needed"); } } return Ok(()); } // Create sync engine for other operations let mut sync_engine = SyncEngine::new(config.clone()).await?; if cli.list_events { // Check if we should list events from import target calendar if cli.import_info { // List events from import target calendar (similar to list_import_events but simplified) info!("Listing events from import target calendar"); // Validate import configuration let import_config = match config.get_import_config() { Some(config) => config, None => { error!("No import target configured. Please add [import] section to config.toml"); return Err(anyhow::anyhow!("Import configuration not found").into()); } }; // Override target calendar if specified via CLI let target_calendar_name = cli.nextcloud_calendar.as_ref() .unwrap_or(&import_config.target_calendar.name); println!("šŸ“… Events from Import Target Calendar"); println!("====================================="); println!("Target Server: {}", import_config.target_server.url); println!("Target Calendar: {}\n", target_calendar_name); // Create a temporary config for the target server let mut target_config = config.clone(); target_config.server.url = import_config.target_server.url.clone(); target_config.server.username = import_config.target_server.username.clone(); target_config.server.password = import_config.target_server.password.clone(); target_config.server.timeout = import_config.target_server.timeout; target_config.server.use_https = import_config.target_server.use_https; target_config.server.headers = import_config.target_server.headers.clone(); target_config.calendar.name = target_calendar_name.clone(); // Connect to target server let target_sync_engine = match SyncEngine::new(target_config).await { Ok(engine) => engine, Err(e) => { error!("Failed to connect to target server: {}", e); println!("āŒ Failed to connect to target server: {}", e); return Err(e.into()); } }; println!("āœ… Successfully connected to target server!"); // Discover calendars to find the target calendar URL let target_calendars = match target_sync_engine.client.discover_calendars().await { Ok(calendars) => calendars, Err(e) => { error!("Failed to discover calendars on target server: {}", e); println!("āŒ Failed to discover calendars: {}", e); return Err(e.into()); } }; // Find the target calendar let target_calendar = target_calendars.iter() .find(|c| c.name == *target_calendar_name || c.display_name.as_ref().map_or(false, |dn| dn == target_calendar_name)); let target_calendar = match target_calendar { Some(calendar) => { println!("āœ… Found target calendar: {}", calendar.display_name.as_ref().unwrap_or(&calendar.name)); calendar } None => { println!("āŒ Target calendar '{}' not found on server", target_calendar_name); println!("Available calendars:"); for calendar in &target_calendars { println!(" - {}", calendar.display_name.as_ref().unwrap_or(&calendar.name)); } return Err(anyhow::anyhow!("Target calendar not found").into()); } }; // Set date range for event listing (past 30 days to next 30 days) let now = Utc::now(); let start_date = now - Duration::days(30); let end_date = now + Duration::days(30); println!("\nRetrieving events from {} to {}...", start_date.format("%Y-%m-%d"), end_date.format("%Y-%m-%d")); // Get events from the target calendar using the full URL let events: Vec = match target_sync_engine.client.get_events(&target_calendar.url, start_date, end_date).await { Ok(events) => events, Err(e) => { error!("Failed to retrieve events from target calendar: {}", e); println!("āŒ Failed to retrieve events: {}", e); return Err(e.into()); } }; println!("Found {} events:\n", events.len()); // Display events in a simple format similar to the original list_events for event in events { let start_tz = event.start_tzid.as_deref().unwrap_or("UTC"); let end_tz = event.end_tzid.as_deref().unwrap_or("UTC"); println!(" - {} ({} {} to {} {})", event.summary, event.start.format("%Y-%m-%d %H:%M"), start_tz, event.end.format("%Y-%m-%d %H:%M"), end_tz ); } return Ok(()); } // Original behavior: List events from source calendar and exit info!("Listing events from calendar: {}", config.calendar.name); // Use the specific approach if provided if let Some(ref approach) = cli.approach { info!("Using specific approach: {}", approach); // Use the provided calendar URL if available, otherwise discover calendars let calendar_url = if let Some(ref url) = cli.calendar_url { url.clone() } else { let calendars = sync_engine.client.discover_calendars().await?; if let Some(calendar) = calendars.iter().find(|c| c.name == config.calendar.name || c.display_name.as_ref().map_or(false, |n| n == &config.calendar.name)) { calendar.url.clone() } else { warn!("Calendar '{}' not found", config.calendar.name); return Ok(()); } }; let now = Utc::now(); let start_date = now - Duration::days(30); let end_date = now + Duration::days(30); match sync_engine.client.get_events_with_approach(&calendar_url, start_date, end_date, Some(approach.clone())).await { Ok(events) => { println!("Found {} events using approach {}:", events.len(), approach); for event in events { let start_tz = event.start_tzid.as_deref().unwrap_or("UTC"); let end_tz = event.end_tzid.as_deref().unwrap_or("UTC"); println!(" - {} ({} {} to {} {})", event.summary, event.start.format("%Y-%m-%d %H:%M"), start_tz, event.end.format("%Y-%m-%d %H:%M"), end_tz ); } } Err(e) => { error!("Failed to get events with approach {}: {}", approach, e); } } return Ok(()); } // Perform a sync to get events let sync_result = sync_engine.sync_full().await?; info!("Sync completed: {} events processed", sync_result.events_processed); // Get and display events let events = sync_engine.get_local_events(); println!("Found {} events:", events.len()); for event in events { let start_tz = event.start_tzid.as_deref().unwrap_or("UTC"); let end_tz = event.end_tzid.as_deref().unwrap_or("UTC"); println!(" - {} ({} {} to {} {})", event.summary, event.start.format("%Y-%m-%d %H:%M"), start_tz, event.end.format("%Y-%m-%d %H:%M"), end_tz ); } return Ok(()); } if cli.once || cli.full_resync { // Perform one-time sync if cli.full_resync { info!("Performing full resynchronization"); let result = sync_engine.force_full_resync().await?; info!("Full resync completed: {} events processed", result.events_processed); } else { info!("Performing one-time synchronization"); let result = sync_engine.sync_incremental().await?; info!("Sync completed: {} events processed", result.events_processed); } } else { // Start continuous synchronization info!("Starting continuous synchronization"); if config.sync.sync_on_startup { info!("Performing initial sync"); match sync_engine.sync_incremental().await { Ok(result) => { info!("Initial sync completed: {} events processed", result.events_processed); } Err(e) => { warn!("Initial sync failed: {}", e); } } } // Start auto-sync loop sync_engine.start_auto_sync().await?; } Ok(()) }