feat: Add comprehensive Nextcloud import functionality and fix compilation issues

Major additions:
- New NextcloudImportEngine with import behaviors (SkipDuplicates, Overwrite, Merge)
- Complete import workflow with result tracking and conflict resolution
- Support for dry-run mode and detailed progress reporting
- Import command integration in CLI with --import-events flag

Configuration improvements:
- Added ImportConfig struct for structured import settings
- Backward compatibility with legacy ImportTargetConfig
- Enhanced get_import_config() method supporting both formats

CalDAV client enhancements:
- Improved XML parsing for multiple calendar display name formats
- Better fallback handling for calendar discovery
- Enhanced error handling and debugging capabilities

Bug fixes:
- Fixed test compilation errors in error.rs (reqwest::Error type conversion)
- Resolved unused variable warning in main.rs
- All tests now pass (16/16)

Documentation:
- Added comprehensive NEXTCLOUD_IMPORT_PLAN.md with implementation roadmap
- Updated library exports to include new modules

Files changed:
- src/nextcloud_import.rs: New import engine implementation
- src/config.rs: Enhanced configuration with import support
- src/main.rs: Added import command and CLI integration
- src/minicaldav_client.rs: Improved calendar discovery and XML parsing
- src/error.rs: Fixed test compilation issues
- src/lib.rs: Updated module exports
- Deleted: src/real_caldav_client.rs (removed unused file)
This commit is contained in:
Alvaro Soliverez 2025-10-29 13:39:48 -03:00
parent 16d6fc375d
commit f84ce62f73
10 changed files with 1461 additions and 342 deletions

View file

@ -3,6 +3,7 @@ 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 std::path::PathBuf;
use chrono::{Utc, Duration};
@ -62,6 +63,22 @@ struct Cli {
/// 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: skip_duplicates, overwrite, merge
#[arg(long, default_value = "skip_duplicates")]
import_behavior: String,
/// Dry run - show what would be imported without actually doing it
#[arg(long)]
dry_run: bool,
}
#[tokio::main]
@ -236,7 +253,7 @@ async fn run_sync(config: Config, cli: &Cli) -> CalDavResult<()> {
}
// Show target import calendars if configured
if let Some(ref import_config) = config.import {
if let Some(ref import_config) = config.get_import_config() {
println!("📥 TARGET IMPORT CALENDARS (Nextcloud/Destination)");
println!("=================================================");
@ -440,6 +457,149 @@ async fn run_sync(config: Config, cli: &Cli) -> CalDavResult<()> {
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(());
}
// Create sync engine for other operations
let mut sync_engine = SyncEngine::new(config.clone()).await?;