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:
commit
8362ebe44b
16 changed files with 6192 additions and 0 deletions
174
src/main.rs
Normal file
174
src/main.rs
Normal 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(())
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue