Add purge command, simplify event RRULE until clients are fixed

This commit is contained in:
Matteo Settenvini 2022-08-02 15:41:55 +02:00
parent 04fadd3d89
commit 76968d5977
Signed by: matteo
GPG Key ID: 8576CC1AD97D42DF
9 changed files with 124 additions and 16 deletions

32
.vscode/launch.json vendored
View File

@ -19,7 +19,10 @@
} }
}, },
"args": ["groceries", "Cucina"], "args": ["groceries", "Cucina"],
"cwd": "${workspaceFolder}" "env": {
"RUST_LOG": "debug",
},
"cwd": "${workspaceFolder}",
}, },
{ {
"type": "lldb", "type": "lldb",
@ -37,6 +40,30 @@
} }
}, },
"args": ["schedule-csv", "Cucina", "examples/example-schedule.csv"], "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}" "cwd": "${workspaceFolder}"
}, },
{ {
@ -56,6 +83,9 @@
} }
}, },
"args": [], "args": [],
"env": {
"RUST_LOG": "debug",
},
"cwd": "${workspaceFolder}" "cwd": "${workspaceFolder}"
} }
] ]

View File

@ -72,6 +72,10 @@ impl ApiClient {
&self.base_url &self.base_url
} }
pub fn caldav_base_url(&self) -> &Url {
&self.caldav_base_url
}
pub fn username(&self) -> &str { pub fn username(&self) -> &str {
&self.username &self.username
} }

View File

@ -1,5 +1,3 @@
use reqwest::Method;
// SPDX-FileCopyrightText: 2022 Matteo Settenvini <matteo.settenvini@montecristosoftware.eu> // SPDX-FileCopyrightText: 2022 Matteo Settenvini <matteo.settenvini@montecristosoftware.eu>
// SPDX-License-Identifier: AGPL-3.0-or-later // SPDX-License-Identifier: AGPL-3.0-or-later
@ -10,7 +8,7 @@ use {
chrono::{Duration, Local}, chrono::{Duration, Local},
icalendar::Component, icalendar::Component,
regex::Regex, regex::Regex,
reqwest::StatusCode, reqwest::{Method, StatusCode},
std::collections::HashSet, std::collections::HashSet,
std::ops::Range, std::ops::Range,
}; };

View File

@ -4,5 +4,6 @@
pub mod groceries; pub mod groceries;
pub mod import; pub mod import;
pub mod init; pub mod init;
pub mod purge;
pub mod schedule; pub mod schedule;
pub mod schedule_csv; pub mod schedule_csv;

63
src/commands/purge.rs Normal file
View File

@ -0,0 +1,63 @@
// SPDX-FileCopyrightText: 2022 Matteo Settenvini <matteo.settenvini@montecristosoftware.eu>
// 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!(
"<c:calendar-query xmlns:d=\"DAV:\" xmlns:c=\"urn:ietf:params:xml:ns:caldav\">
<d:prop>
<d:getetag/>
</d:prop>
<c:filter>
<c:comp-filter name=\"VCALENDAR\">
<c:prop-filter name=\"PRODID\">
<c:text-match>{}</c:text-match>
</c:prop-filter>
</c:comp-filter>
</c:filter>
</c:calendar-query>
",
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::<Vec<_>>();
for url in events_to_purge {
api_client
.rest()
.delete(api_client.base_url().join(&url)?)
.send()
.await?;
log::debug!("Purged {}", &url);
}
Ok(())
}

View File

@ -115,13 +115,7 @@ async fn publish_events<'a, EventsIter>(
where where
EventsIter: Iterator<Item = Event>, EventsIter: Iterator<Item = Event>,
{ {
let dav_base = api_client let calendar_url = api_client.caldav_base_url().join(&format!(
.rest()
.head(api_client.base_url().join("/.well-known/caldav")?)
.send()
.await?;
let calendar_url = dav_base.url().join(&format!(
"calendars/{}/{}/", "calendars/{}/{}/",
&api_client.username(), &api_client.username(),
calendar.to_lowercase().as_str().replace(" ", "-") calendar.to_lowercase().as_str().replace(" ", "-")
@ -160,7 +154,6 @@ where
// TODO: wrap this // TODO: wrap this
let _ = api_client.rest_semaphore.acquire().await.unwrap(); let _ = api_client.rest_semaphore.acquire().await.unwrap();
log::info!("{}", info_message);
let response = api_client let response = api_client
.rest() .rest()
.put(url) .put(url)
@ -169,6 +162,8 @@ where
.send() .send()
.await; .await;
log::info!("{}", info_message);
// TODO: magic numbers are bad... // TODO: magic numbers are bad...
std::thread::sleep(std::time::Duration::from_millis(300)); std::thread::sleep(std::time::Duration::from_millis(300));
response response

View File

@ -5,8 +5,6 @@ pub const USER_AGENT: &str = concat!(env!("CARGO_PKG_NAME"), "/", env!("CARGO_PK
pub const CALENDAR_PROVIDER: &str = concat!( pub const CALENDAR_PROVIDER: &str = concat!(
"-//IDN montecristosoftware.eu//", "-//IDN montecristosoftware.eu//",
env!("CARGO_PKG_NAME"), env!("CARGO_PKG_NAME"),
" ",
env!("CARGO_PKG_VERSION"),
"//EN" "//EN"
); );
pub const ICAL_UTCTIME_FMT: &str = "%Y%m%dT%H%M%SZ"; pub const ICAL_UTCTIME_FMT: &str = "%Y%m%dT%H%M%SZ";

View File

@ -3,7 +3,7 @@
use { use {
crate::recipe::Recipe, crate::recipe::Recipe,
chrono::{Datelike, NaiveDate, NaiveDateTime, NaiveTime, Utc}, chrono::{/*Datelike, */ NaiveDate, NaiveDateTime, NaiveTime, Utc},
icalendar::Event as CalEvent, icalendar::Event as CalEvent,
std::rc::Rc, std::rc::Rc,
}; };
@ -48,7 +48,7 @@ impl From<Event> for CalEvent {
fn from(ev: Event) -> Self { fn from(ev: Event) -> Self {
use icalendar::Component; 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 start_time = ev.ends_at - ev.recipe.total_time();
let cal_event = CalEvent::new() let cal_event = CalEvent::new()
@ -59,6 +59,7 @@ impl From<Event> for CalEvent {
.timestamp(Utc::now()) .timestamp(Utc::now())
.starts(start_time) .starts(start_time)
.ends(ev.ends_at) .ends(ev.ends_at)
/* FIXME
.add_property( .add_property(
// TODO make configurable yearly repetition, for now this suits my personal uses // TODO make configurable yearly repetition, for now this suits my personal uses
"RRULE", "RRULE",
@ -69,6 +70,11 @@ impl From<Event> for CalEvent {
.unwrap(), .unwrap(),
weekno = start_time.iso_week().week(), weekno = start_time.iso_week().week(),
), ),
)*/
.add_property(
// TODO make configurable yearly repetition, for now this suits my personal uses
"RRULE",
"FREQ=YEARLY",
) )
.done(); .done();

View File

@ -70,6 +70,12 @@ fn setup_args() -> ArgMatches {
.arg(arg!(<calendar_name> "")) .arg(arg!(<calendar_name> ""))
.arg(arg!(<csv_file> "").value_parser(clap::value_parser!(PathBuf))) .arg(arg!(<csv_file> "").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!(<calendar_name> ""))
)
.get_matches() .get_matches()
} }
@ -114,6 +120,13 @@ async fn parse_args(args: &ArgMatches) -> Result<()> {
.expect("<calendar_name> is a mandatory parameter, it cannot be missing"); .expect("<calendar_name> is a mandatory parameter, it cannot be missing");
commands::schedule_csv::with(&api_client, calendar_name.as_str(), &csv_file).await 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::<String>("calendar_name")
.expect("<calendar_name> 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`"), _ => unreachable!("Exhausted list of subcommands and subcommand_required prevents `None`"),
} }
} }