caldavpuller/src/main.rs
Alvaro Soliverez 932b6ae463 Fix timezone handling and update detection
- Fix timezone preservation in to_ical_simple() for import module
- Add timezone comparison to needs_update() method to detect timezone differences
- Add comprehensive test for timezone comparison logic
- Log Bug #3: recurring event end detection issue for future investigation
2025-11-21 11:56:27 -03:00

1074 lines
49 KiB
Rust

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<String>,
/// Username for authentication (overrides config file)
#[arg(short, long)]
username: Option<String>,
/// Password for authentication (overrides config file)
#[arg(short, long)]
password: Option<String>,
/// Calendar name to sync (overrides config file)
#[arg(long)]
calendar: Option<String>,
/// 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<String>,
/// Use specific calendar URL instead of discovering from config
#[arg(long)]
calendar_url: Option<String>,
/// 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<String>,
/// 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::<ImportBehavior>() {
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<caldav_sync::event::Event> = 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<CalendarEvent> = 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<CalendarEvent> = 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(())
}