diff --git a/.vscode/launch.json b/.vscode/launch.json index e880500..205502b 100644 --- a/.vscode/launch.json +++ b/.vscode/launch.json @@ -19,7 +19,10 @@ } }, "args": ["groceries", "Cucina"], - "cwd": "${workspaceFolder}" + "env": { + "RUST_LOG": "debug", + }, + "cwd": "${workspaceFolder}", }, { "type": "lldb", @@ -37,6 +40,30 @@ } }, "args": ["schedule-csv", "Cucina", "examples/example-schedule.csv"], + "env": { + "RUST_LOG": "debug", + }, + "cwd": "${workspaceFolder}" + }, + { + "type": "lldb", + "request": "launch", + "name": "Debug executable 'cook' -> purge", + "cargo": { + "args": [ + "build", + "--bin=cook", + "--package=cooking-schedule" + ], + "filter": { + "name": "cook", + "kind": "bin" + } + }, + "args": ["purge", "Cucina"], + "env": { + "RUST_LOG": "debug", + }, "cwd": "${workspaceFolder}" }, { @@ -56,6 +83,9 @@ } }, "args": [], + "env": { + "RUST_LOG": "debug", + }, "cwd": "${workspaceFolder}" } ] diff --git a/src/api_client.rs b/src/api_client.rs index 960503d..210d054 100644 --- a/src/api_client.rs +++ b/src/api_client.rs @@ -72,6 +72,10 @@ impl ApiClient { &self.base_url } + pub fn caldav_base_url(&self) -> &Url { + &self.caldav_base_url + } + pub fn username(&self) -> &str { &self.username } diff --git a/src/commands/groceries.rs b/src/commands/groceries.rs index bb70118..ee52dd9 100644 --- a/src/commands/groceries.rs +++ b/src/commands/groceries.rs @@ -1,5 +1,3 @@ -use reqwest::Method; - // SPDX-FileCopyrightText: 2022 Matteo Settenvini // SPDX-License-Identifier: AGPL-3.0-or-later @@ -10,7 +8,7 @@ use { chrono::{Duration, Local}, icalendar::Component, regex::Regex, - reqwest::StatusCode, + reqwest::{Method, StatusCode}, std::collections::HashSet, std::ops::Range, }; diff --git a/src/commands/mod.rs b/src/commands/mod.rs index 042df50..7c29889 100644 --- a/src/commands/mod.rs +++ b/src/commands/mod.rs @@ -4,5 +4,6 @@ pub mod groceries; pub mod import; pub mod init; +pub mod purge; pub mod schedule; pub mod schedule_csv; diff --git a/src/commands/purge.rs b/src/commands/purge.rs new file mode 100644 index 0000000..099e353 --- /dev/null +++ b/src/commands/purge.rs @@ -0,0 +1,63 @@ +// SPDX-FileCopyrightText: 2022 Matteo Settenvini +// SPDX-License-Identifier: AGPL-3.0-or-later + +use {crate::api_client::ApiClient, crate::constants, anyhow::Result, reqwest::Method}; + +pub async fn with(api_client: &ApiClient, calendar_name: &str) -> Result<()> { + let report_method = Method::from_bytes(b"REPORT")?; + let events_xml = api_client + .rest() + .request( + report_method, + // TODO extract into helper method + api_client.caldav_base_url().join(&format!( + "calendars/{}/{}/", + api_client.username(), + calendar_name.to_lowercase().replace(" ", "-") + ))?, + ) + .header("Prefer", "return-minimal") + .header("Content-Type", "application/xml; charset=utf-8") + .header("Depth", 1) + .body(format!( + " + + + + + + + {} + + + + + ", + constants::CALENDAR_PROVIDER, + )) + .send() + .await? + .text() + .await?; + + let xml_doc = libxml::parser::Parser::default().parse_string(events_xml.as_bytes())?; + let mut xpath_ctx = libxml::xpath::Context::new(&xml_doc).unwrap(); + xpath_ctx.register_namespace("d", "DAV:").unwrap(); + let events_to_purge = xpath_ctx + .findnodes("//d:response/d:href/text()", None) + .unwrap() + .into_iter() + .map(|n| icalendar::parser::unfold(&n.get_content())) + .collect::>(); + + for url in events_to_purge { + api_client + .rest() + .delete(api_client.base_url().join(&url)?) + .send() + .await?; + log::debug!("Purged {}", &url); + } + + Ok(()) +} diff --git a/src/commands/schedule_csv.rs b/src/commands/schedule_csv.rs index 69298cb..19728cd 100644 --- a/src/commands/schedule_csv.rs +++ b/src/commands/schedule_csv.rs @@ -115,13 +115,7 @@ async fn publish_events<'a, EventsIter>( where EventsIter: Iterator, { - let dav_base = api_client - .rest() - .head(api_client.base_url().join("/.well-known/caldav")?) - .send() - .await?; - - let calendar_url = dav_base.url().join(&format!( + let calendar_url = api_client.caldav_base_url().join(&format!( "calendars/{}/{}/", &api_client.username(), calendar.to_lowercase().as_str().replace(" ", "-") @@ -160,7 +154,6 @@ where // TODO: wrap this let _ = api_client.rest_semaphore.acquire().await.unwrap(); - log::info!("{}", info_message); let response = api_client .rest() .put(url) @@ -169,6 +162,8 @@ where .send() .await; + log::info!("{}", info_message); + // TODO: magic numbers are bad... std::thread::sleep(std::time::Duration::from_millis(300)); response diff --git a/src/constants.rs b/src/constants.rs index d1e38e0..f8c65f5 100644 --- a/src/constants.rs +++ b/src/constants.rs @@ -5,8 +5,6 @@ pub const USER_AGENT: &str = concat!(env!("CARGO_PKG_NAME"), "/", env!("CARGO_PK pub const CALENDAR_PROVIDER: &str = concat!( "-//IDN montecristosoftware.eu//", env!("CARGO_PKG_NAME"), - " ", - env!("CARGO_PKG_VERSION"), "//EN" ); pub const ICAL_UTCTIME_FMT: &str = "%Y%m%dT%H%M%SZ"; diff --git a/src/event.rs b/src/event.rs index 8a35e20..f0dd326 100644 --- a/src/event.rs +++ b/src/event.rs @@ -3,7 +3,7 @@ use { crate::recipe::Recipe, - chrono::{Datelike, NaiveDate, NaiveDateTime, NaiveTime, Utc}, + chrono::{/*Datelike, */ NaiveDate, NaiveDateTime, NaiveTime, Utc}, icalendar::Event as CalEvent, std::rc::Rc, }; @@ -48,7 +48,7 @@ impl From for CalEvent { fn from(ev: Event) -> Self { use icalendar::Component; - const DAY_NAMES: [&str; 7] = ["MO", "TU", "WE", "TH", "FR", "SA", "SU"]; + // const DAY_NAMES: [&str; 7] = ["MO", "TU", "WE", "TH", "FR", "SA", "SU"]; let start_time = ev.ends_at - ev.recipe.total_time(); let cal_event = CalEvent::new() @@ -59,6 +59,7 @@ impl From for CalEvent { .timestamp(Utc::now()) .starts(start_time) .ends(ev.ends_at) + /* FIXME .add_property( // TODO make configurable yearly repetition, for now this suits my personal uses "RRULE", @@ -69,6 +70,11 @@ impl From for CalEvent { .unwrap(), weekno = start_time.iso_week().week(), ), + )*/ + .add_property( + // TODO make configurable yearly repetition, for now this suits my personal uses + "RRULE", + "FREQ=YEARLY", ) .done(); diff --git a/src/main.rs b/src/main.rs index daa8691..77b195b 100644 --- a/src/main.rs +++ b/src/main.rs @@ -70,6 +70,12 @@ fn setup_args() -> ArgMatches { .arg(arg!( "")) .arg(arg!( "").value_parser(clap::value_parser!(PathBuf))) ) + .subcommand( + Command::new("purge") + .about("Removes all events created by this application from a given calendar") + .arg(server_arg.clone()) + .arg(arg!( "")) + ) .get_matches() } @@ -114,6 +120,13 @@ async fn parse_args(args: &ArgMatches) -> Result<()> { .expect(" is a mandatory parameter, it cannot be missing"); commands::schedule_csv::with(&api_client, calendar_name.as_str(), &csv_file).await } + Some(("purge", sub_matches)) => { + let api_client = get_api_client(&sub_matches, &configuration)?; + let calendar_name = sub_matches + .get_one::("calendar_name") + .expect(" is a mandatory parameter, it cannot be missing"); + commands::purge::with(&api_client, calendar_name.as_str()).await + } _ => unreachable!("Exhausted list of subcommands and subcommand_required prevents `None`"), } }