From 8bf0adac445465f2ecd3121f688171895cd61dd1 Mon Sep 17 00:00:00 2001 From: Leonard Kugis Date: Sat, 12 Apr 2025 12:46:52 +0200 Subject: Implemented calendar in Rust --- .gitignore | 22 ++++++++-- calendar/Cargo.toml | 12 ++++++ calendar/src/main.rs | 118 +++++++++++++++++++++++++++++++++++++++++++++++++++ 3 files changed, 149 insertions(+), 3 deletions(-) create mode 100644 calendar/Cargo.toml create mode 100644 calendar/src/main.rs diff --git a/.gitignore b/.gitignore index ce73bef..0fcb377 100644 --- a/.gitignore +++ b/.gitignore @@ -1,6 +1,6 @@ config.json -# Created by https://www.toptal.com/developers/gitignore/api/linux,visualstudiocode -# Edit at https://www.toptal.com/developers/gitignore?templates=linux,visualstudiocode +# Created by https://www.toptal.com/developers/gitignore/api/rust,visualstudiocode,linux +# Edit at https://www.toptal.com/developers/gitignore?templates=rust,visualstudiocode,linux ### Linux ### *~ @@ -17,6 +17,22 @@ config.json # .nfs files are created when an open file is removed but is still being accessed .nfs* +### Rust ### +# Generated by Cargo +# will have compiled files and executables +debug/ +target/ + +# Remove Cargo.lock from gitignore if creating an executable, leave it for libraries +# More information here https://doc.rust-lang.org/cargo/guide/cargo-toml-vs-cargo-lock.html +Cargo.lock + +# These are backup files generated by rustfmt +**/*.rs.bk + +# MSVC Windows builds of rustc generate these, which store debugging information +*.pdb + ### VisualStudioCode ### .vscode/* !.vscode/settings.json @@ -36,4 +52,4 @@ config.json .history .ionide -# End of https://www.toptal.com/developers/gitignore/api/linux,visualstudiocode \ No newline at end of file +# End of https://www.toptal.com/developers/gitignore/api/rust,visualstudiocode,linux \ No newline at end of file diff --git a/calendar/Cargo.toml b/calendar/Cargo.toml new file mode 100644 index 0000000..1ab2d69 --- /dev/null +++ b/calendar/Cargo.toml @@ -0,0 +1,12 @@ +[package] +name = "calendar" +version = "0.1.0" +edition = "2021" + +[dependencies] +serde = { version = "1.0", features = ["derive"] } +serde_json = "1.0" +reqwest = { version = "0.11", features = ["blocking", "json"] } +regex = "1.10" +tempfile = "3.8" +anyhow = "1.0" \ No newline at end of file diff --git a/calendar/src/main.rs b/calendar/src/main.rs new file mode 100644 index 0000000..77271fd --- /dev/null +++ b/calendar/src/main.rs @@ -0,0 +1,118 @@ +use std::{fs, io::Write, path::PathBuf}; +use serde::Deserialize; +use regex::Regex; +use tempfile::tempdir; +use reqwest::blocking::Client; + +#[derive(Deserialize)] +struct Config { + calendar: CalendarConfig, +} + +#[derive(Deserialize)] +struct CalendarConfig { + 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 base_dir = "/home/leonard/Projects/scripts"; + let config_path = format!("{}/config.json", base_dir); + let config_data = fs::read_to_string(&config_path)?; + let config: Config = serde_json::from_str(&config_data)?; + + let tmp_dir = tempdir()?; + let client = Client::new(); + + for cal in &config.calendar.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.calendar.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.calendar.caldav_username, Some(&config.calendar.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() +} -- cgit v1.2.3