summaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorLeonard Kugis <leonard@kug.is>2025-04-12 12:46:52 +0200
committerLeonard Kugis <leonard@kug.is>2025-04-12 12:46:52 +0200
commit8bf0adac445465f2ecd3121f688171895cd61dd1 (patch)
tree25e2cb4a82cdf097e63a62fccfd51dc69d3db20f
parent5575ed931b95bb1f4a041c9fc41790ad02911b88 (diff)
downloadscripts-8bf0adac445465f2ecd3121f688171895cd61dd1.tar.gz
Implemented calendar in Rust
-rw-r--r--.gitignore22
-rw-r--r--calendar/Cargo.toml12
-rw-r--r--calendar/src/main.rs118
3 files changed, 149 insertions, 3 deletions
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<CalendarEntry>,
+}
+
+#[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<String> {
+ 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<String> {
+ 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()
+}