From 16d6fc375d399a6d4ca790c63b921f4d8294e1cf Mon Sep 17 00:00:00 2001 From: Alvaro Soliverez Date: Sun, 26 Oct 2025 13:10:16 -0300 Subject: [PATCH] Working correctly to fetch 1 Nextcloud calendar --- config/config.toml | 21 +++ src/config.rs | 82 +++++++++++- src/main.rs | 327 ++++++++++++++++++++++++++++++++++++++++++--- 3 files changed, 406 insertions(+), 24 deletions(-) diff --git a/config/config.toml b/config/config.toml index e2c65d9..c05af8e 100644 --- a/config/config.toml +++ b/config/config.toml @@ -39,6 +39,27 @@ delete_missing = false # Date range configuration date_range = { days_ahead = 30, days_back = 30, sync_all_events = false } +[import] +# Target server configuration (e.g., Nextcloud) +[import.target_server] +# Nextcloud CalDAV URL +url = "https://cloud.soliverez.com.ar/remote.php/dav/calendars/alvaro/" +# Username for Nextcloud authentication +username = "alvaro" +# Password for Nextcloud authentication (use app-specific password) +password = "D7F2o-fFoqp-j2ttJ-t4etE-yz3oS" +# Whether to use HTTPS (recommended) +use_https = true +# Request timeout in seconds +timeout = 30 + +# Target calendar configuration +[import.target_calendar] +# Target calendar name +name = "trabajo-alvaro" +enabled = true + + # Optional filtering configuration [filters] # Keywords to filter events by (events containing any of these will be included) diff --git a/src/config.rs b/src/config.rs index 5afb316..f13956a 100644 --- a/src/config.rs +++ b/src/config.rs @@ -7,10 +7,12 @@ use anyhow::Result; /// Main configuration structure #[derive(Debug, Clone, Serialize, Deserialize)] pub struct Config { - /// Server configuration + /// Source server configuration (e.g., Zoho) pub server: ServerConfig, - /// Calendar configuration + /// Source calendar configuration pub calendar: CalendarConfig, + /// Import configuration (e.g., Nextcloud as target) + pub import: Option, /// Filter configuration pub filters: Option, /// Sync configuration @@ -49,6 +51,47 @@ pub struct CalendarConfig { pub enabled: bool, } +/// Import configuration for unidirectional sync to target server +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct ImportConfig { + /// Target server configuration + pub target_server: ImportTargetServerConfig, + /// Target calendar configuration + pub target_calendar: ImportTargetCalendarConfig, +} + +/// Target server configuration for Nextcloud or other CalDAV servers +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct ImportTargetServerConfig { + /// Target CalDAV server URL + pub url: String, + /// Username for authentication + pub username: String, + /// Password for authentication + pub password: String, + /// Whether to use HTTPS + pub use_https: bool, + /// Timeout in seconds + pub timeout: u64, + /// Custom headers to send with requests + pub headers: Option>, +} + +/// Target calendar configuration +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct ImportTargetCalendarConfig { + /// Target calendar name + pub name: String, + /// Target calendar display name + pub display_name: Option, + /// Target calendar color + pub color: Option, + /// Target calendar timezone + pub timezone: Option, + /// Whether this calendar is enabled for import + pub enabled: bool, +} + /// Filter configuration for events #[derive(Debug, Clone, Serialize, Deserialize)] pub struct FilterConfig { @@ -97,6 +140,7 @@ impl Default for Config { Self { server: ServerConfig::default(), calendar: CalendarConfig::default(), + import: None, filters: None, sync: SyncConfig::default(), } @@ -128,6 +172,40 @@ impl Default for CalendarConfig { } } +impl Default for ImportConfig { + fn default() -> Self { + Self { + target_server: ImportTargetServerConfig::default(), + target_calendar: ImportTargetCalendarConfig::default(), + } + } +} + +impl Default for ImportTargetServerConfig { + fn default() -> Self { + Self { + url: "https://nextcloud.example.com/remote.php/dav/calendars/user".to_string(), + username: String::new(), + password: String::new(), + use_https: true, + timeout: 30, + headers: None, + } + } +} + +impl Default for ImportTargetCalendarConfig { + fn default() -> Self { + Self { + name: "Imported-Events".to_string(), + display_name: None, + color: None, + timezone: None, + enabled: true, + } + } +} + impl Default for SyncConfig { fn default() -> Self { Self { diff --git a/src/main.rs b/src/main.rs index da773d5..18cbdd7 100644 --- a/src/main.rs +++ b/src/main.rs @@ -58,6 +58,10 @@ struct Cli { /// 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, } #[tokio::main] @@ -126,40 +130,319 @@ async fn main() -> Result<()> { } async fn run_sync(config: Config, cli: &Cli) -> CalDavResult<()> { - // Create sync engine - let mut sync_engine = SyncEngine::new(config.clone()).await?; - if cli.list_calendars { // List calendars and exit info!("Listing available calendars from server"); - // Get calendars directly from the client - 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 display_name) = calendar.display_name { - println!(" Display Name: {}", display_name); + 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); } - if let Some(ref color) = calendar.color { - println!(" Color: {}", color); + + // Show target import calendars if configured + if let Some(ref import_config) = config.import { + 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"); } - if let Some(ref description) = calendar.description { - println!(" Description: {}", description); + } 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!(); } - if let Some(ref timezone) = calendar.timezone { - println!(" Timezone: {}", timezone); - } - println!(" Supported Components: {}", calendar.supported_components.join(", ")); - println!(); } return Ok(()); } + // Create sync engine for other operations + let mut sync_engine = SyncEngine::new(config.clone()).await?; + if cli.list_events { // List events and exit info!("Listing events from calendar: {}", config.calendar.name);