Initial commit: Complete CalDAV calendar synchronizer

- Rust-based CLI tool for Zoho to Nextcloud calendar sync
- Selective calendar import from Zoho to single Nextcloud calendar
- Timezone-aware event handling for next-week synchronization
- Comprehensive configuration system with TOML support
- CLI interface with debug, list, and sync operations
- Complete documentation and example configurations
This commit is contained in:
Alvaro Soliverez 2025-10-04 11:57:44 -03:00
commit 8362ebe44b
16 changed files with 6192 additions and 0 deletions

174
src/main.rs Normal file
View file

@ -0,0 +1,174 @@
use anyhow::Result;
use clap::Parser;
use tracing::{info, warn, error, Level};
use tracing_subscriber;
use caldav_sync::{Config, SyncEngine, CalDavResult};
use std::path::PathBuf;
#[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/default.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,
}
#[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<()> {
// Create sync engine
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);
// 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 {
println!(" - {} ({} to {})",
event.summary,
event.start.format("%Y-%m-%d %H:%M"),
event.end.format("%Y-%m-%d %H:%M")
);
}
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(())
}