use std::{fs, io::Write, path::PathBuf}; use clap::Parser; use serde::Deserialize; use regex::Regex; use tempfile::tempdir; use reqwest::blocking::Client; /// ICS zu CalDAV Uploader #[derive(Parser, Debug)] #[command(author, version, about, long_about = None)] struct Args { /// Pfad zur Konfigurationsdatei (config.json) #[arg(short, long, value_name = "FILE")] config: PathBuf, } #[derive(Deserialize)] struct Config { caldav_base_url: String, caldav_username: String, caldav_password: String, calendars: Vec, } #[derive(Deserialize)] struct CalendarEntry { name: String, ics: String, } fn main() -> anyhow::Result<()> { let args = Args::parse(); let config_data = fs::read_to_string(&args.config)?; let config: Config = serde_json::from_str(&config_data)?; let tmp_dir = tempdir()?; let client = Client::builder() .timeout(std::time::Duration::from_secs(60)) .build()?; for cal in &config.calendars { println!("INFO: Lade ICS von: {}", cal.ics); let ics_path = tmp_dir.path().join(format!("{}.ics", cal.name)); let response = client.get(&cal.ics).send()?; if !response.status().is_success() { eprintln!("WARNING: Fehler beim Herunterladen von {}", cal.ics); continue; } let ics_content = response.text()?; fs::write(&ics_path, &ics_content)?; let caldav_url = format!("{}/{}", config.caldav_base_url.trim_end_matches('/'), cal.name); println!("INFO: Zerlege Datei: {:?}", ics_path); let vevent_blocks = split_vevents(&ics_content); for vevent in vevent_blocks { if let Some(uid) = extract_uid(&vevent) { let safe_uid = sanitize_filename(&uid); let event_path = tmp_dir.path().join(format!("{}.ics", safe_uid)); let mut event_file = fs::File::create(&event_path)?; writeln!(event_file, "BEGIN:VCALENDAR")?; writeln!(event_file, "VERSION:2.0")?; writeln!(event_file, "PRODID:-//calendar script//EN")?; writeln!(event_file, "{}", vevent.trim())?; writeln!(event_file, "END:VCALENDAR")?; println!("INFO: Lade UID={} → {}/{}.ics", safe_uid, caldav_url, safe_uid); let put_url = format!("{}/{}.ics", caldav_url, safe_uid); let body = fs::read(&event_path)?; let put_response = client .put(&put_url) .basic_auth(&config.caldav_username, Some(&config.caldav_password)) .header("Content-Type", "text/calendar; charset=utf-8") .body(body) .send()?; println!("→ HTTP-Code: {}", put_response.status()); } } } Ok(()) } fn split_vevents(ics: &str) -> Vec { let mut vevents = Vec::new(); let mut in_event = false; let mut buffer = String::new(); for line in ics.lines() { if line.trim() == "BEGIN:VEVENT" { in_event = true; buffer.clear(); } if in_event { buffer.push_str(line); buffer.push('\n'); } if line.trim() == "END:VEVENT" && in_event { in_event = false; vevents.push(buffer.clone()); } } vevents } fn extract_uid(event: &str) -> Option { for line in event.lines() { if line.starts_with("UID:") { return Some(line.trim_start_matches("UID:").trim().to_string()); } } None } fn sanitize_filename(name: &str) -> String { let re = Regex::new(r"[^a-zA-Z0-9._-]").unwrap(); re.replace_all(name, "_").to_string() }