From 33539e8c7a5523bd48e0926995fcdee3aac3e38a Mon Sep 17 00:00:00 2001 From: Lennart <18233294+lennart-k@users.noreply.github.com> Date: Sun, 28 Jul 2024 17:49:15 +0200 Subject: [PATCH] Add basic sync-token implementation --- crates/caldav/src/calendar/resource.rs | 16 +++ .../store/migrations/20240621161002_init.sql | 13 +++ crates/store/src/calendar.rs | 1 + crates/store/src/sqlite_store.rs | 101 ++++++++++++++++-- 4 files changed, 121 insertions(+), 10 deletions(-) diff --git a/crates/caldav/src/calendar/resource.rs b/crates/caldav/src/calendar/resource.rs index 1bb7386..d0f19bf 100644 --- a/crates/caldav/src/calendar/resource.rs +++ b/crates/caldav/src/calendar/resource.rs @@ -42,6 +42,8 @@ pub enum CalendarPropName { CurrentUserPrivilegeSet, MaxResourceSize, SupportedReportSet, + SyncToken, + Getctag, } #[derive(Debug, Clone, Deserialize, Serialize)] @@ -73,6 +75,8 @@ pub enum CalendarProp { MaxResourceSize(String), CurrentUserPrivilegeSet(UserPrivilegeSet), SupportedReportSet(SupportedReportSet), + SyncToken(String), + Getctag(String), #[serde(other)] Invalid, } @@ -141,6 +145,14 @@ impl Resource for CalendarFile { CalendarPropName::SupportedReportSet => { CalendarProp::SupportedReportSet(SupportedReportSet::default()) } + CalendarPropName::SyncToken => CalendarProp::SyncToken(format!( + "github.com/lennart-k/rustical/ns/{}", + self.calendar.synctoken + )), + CalendarPropName::Getctag => CalendarProp::Getctag(format!( + "github.com/lennart-k/rustical/ns/{}", + self.calendar.synctoken + )), }) } @@ -180,6 +192,8 @@ impl Resource for CalendarFile { CalendarProp::MaxResourceSize(_) => Err(rustical_dav::Error::PropReadOnly), CalendarProp::CurrentUserPrivilegeSet(_) => Err(rustical_dav::Error::PropReadOnly), CalendarProp::SupportedReportSet(_) => Err(rustical_dav::Error::PropReadOnly), + CalendarProp::SyncToken(_) => Err(rustical_dav::Error::PropReadOnly), + CalendarProp::Getctag(_) => Err(rustical_dav::Error::PropReadOnly), CalendarProp::Invalid => Err(rustical_dav::Error::PropReadOnly), } } @@ -217,6 +231,8 @@ impl Resource for CalendarFile { CalendarPropName::MaxResourceSize => Err(rustical_dav::Error::PropReadOnly), CalendarPropName::CurrentUserPrivilegeSet => Err(rustical_dav::Error::PropReadOnly), CalendarPropName::SupportedReportSet => Err(rustical_dav::Error::PropReadOnly), + CalendarPropName::SyncToken => Err(rustical_dav::Error::PropReadOnly), + CalendarPropName::Getctag => Err(rustical_dav::Error::PropReadOnly), } } } diff --git a/crates/store/migrations/20240621161002_init.sql b/crates/store/migrations/20240621161002_init.sql index ba5baa7..d08c6a4 100644 --- a/crates/store/migrations/20240621161002_init.sql +++ b/crates/store/migrations/20240621161002_init.sql @@ -1,6 +1,7 @@ CREATE TABLE calendars ( principal TEXT NOT NULL, id TEXT NOT NULL, + synctoken INTEGER DEFAULT 0 NOT NULL, displayname TEXT, description TEXT, 'order' INT DEFAULT 0 NOT NULL, @@ -21,3 +22,15 @@ CREATE TABLE events ( FOREIGN KEY (principal, cid) REFERENCES calendars(principal, id) ); +CREATE TABLE eventchangelog ( + -- The actual sync token is the SQLite field 'ROWID' + principal TEXT NOT NULL, + cid TEXT NOT NULL, + uid TEXT NOT NULL, + operation INTEGER NOT NULL, + synctoken INTEGER DEFAULT 0 NOT NULL, + created_at DATETIME DEFAULT CURRENT_TIMESTAMP, + PRIMARY KEY (principal, cid, created_at), + FOREIGN KEY (principal, cid) REFERENCES calendars(principal, id) ON DELETE CASCADE +) + diff --git a/crates/store/src/calendar.rs b/crates/store/src/calendar.rs index 9d5344f..a6fca04 100644 --- a/crates/store/src/calendar.rs +++ b/crates/store/src/calendar.rs @@ -11,4 +11,5 @@ pub struct Calendar { pub color: Option, pub timezone: Option, pub deleted_at: Option, + pub synctoken: i64, } diff --git a/crates/store/src/sqlite_store.rs b/crates/store/src/sqlite_store.rs index 0445494..42c865e 100644 --- a/crates/store/src/sqlite_store.rs +++ b/crates/store/src/sqlite_store.rs @@ -1,10 +1,12 @@ +use crate::calendar::Calendar; +use crate::event::Event; +use crate::{CalendarStore, Error}; use anyhow::Result; use async_trait::async_trait; +use serde::Serialize; +use sqlx::Transaction; use sqlx::{sqlite::SqliteConnectOptions, Pool, Sqlite, SqlitePool}; -use crate::event::Event; -use crate::{calendar::Calendar, CalendarStore, Error}; - #[derive(Debug)] pub struct SqliteCalendarStore { db: SqlitePool, @@ -30,12 +32,55 @@ impl TryFrom for Event { } } +#[derive(Serialize, sqlx::Type)] +#[serde(rename_all = "kebab-case")] +enum CalendarChangeOperation { + // There's no distinction between Add and Modify + Add, + Delete, +} + +// Logs an operation to the events +async fn log_event_operation( + tx: &mut Transaction<'_, Sqlite>, + principal: &str, + cid: &str, + uid: &str, + operation: CalendarChangeOperation, +) -> Result<(), Error> { + sqlx::query!( + r#" + UPDATE calendars + SET synctoken = synctoken + 1 + WHERE (principal, id) = (?1, ?2)"#, + principal, + cid + ) + .execute(&mut **tx) + .await?; + + sqlx::query!( + r#" + INSERT INTO eventchangelog (principal, cid, uid, operation, synctoken) + VALUES (?1, ?2, ?3, ?4, ( + SELECT synctoken FROM calendars WHERE (principal, id) = (?1, ?2) + ))"#, + principal, + cid, + uid, + operation + ) + .execute(&mut **tx) + .await?; + Ok(()) +} + #[async_trait] impl CalendarStore for SqliteCalendarStore { async fn get_calendar(&self, principal: &str, id: &str) -> Result { let cal = sqlx::query_as!( Calendar, - r#"SELECT principal, id, "order", displayname, description, color, timezone, deleted_at + r#"SELECT principal, id, synctoken, "order", displayname, description, color, timezone, deleted_at FROM calendars WHERE (principal, id) = (?, ?)"#, principal, @@ -49,7 +94,7 @@ impl CalendarStore for SqliteCalendarStore { async fn get_calendars(&self, principal: &str) -> Result, Error> { let cals = sqlx::query_as!( Calendar, - r#"SELECT principal, id, displayname, "order", description, color, timezone, deleted_at + r#"SELECT principal, id, synctoken, displayname, "order", description, color, timezone, deleted_at FROM calendars WHERE principal = ? AND deleted_at IS NULL"#, principal @@ -176,7 +221,10 @@ impl CalendarStore for SqliteCalendarStore { uid: String, ics: String, ) -> Result<(), Error> { - let _ = Event::from_ics(uid.to_owned(), ics.to_owned())?; + let mut tx = self.db.begin().await?; + + // input validation + Event::from_ics(uid.to_owned(), ics.to_owned())?; sqlx::query!( "REPLACE INTO events (principal, cid, uid, ics) VALUES (?, ?, ?, ?)", principal, @@ -184,8 +232,18 @@ impl CalendarStore for SqliteCalendarStore { uid, ics, ) - .execute(&self.db) + .execute(&mut *tx) .await?; + + log_event_operation( + &mut tx, + &principal, + &cid, + &uid, + CalendarChangeOperation::Add, + ) + .await?; + tx.commit().await?; Ok(()) } @@ -196,6 +254,8 @@ impl CalendarStore for SqliteCalendarStore { uid: &str, use_trashbin: bool, ) -> Result<(), Error> { + let mut tx = self.db.begin().await?; + match use_trashbin { true => { sqlx::query!( @@ -204,27 +264,48 @@ impl CalendarStore for SqliteCalendarStore { cid, uid ) - .execute(&self.db) + .execute(&mut *tx) .await?; } false => { sqlx::query!("DELETE FROM events WHERE cid = ? AND uid = ?", cid, uid) - .execute(&self.db) + .execute(&mut *tx) .await?; } }; + log_event_operation( + &mut tx, + principal, + cid, + uid, + CalendarChangeOperation::Delete, + ) + .await?; + tx.commit().await?; Ok(()) } async fn restore_event(&mut self, principal: &str, cid: &str, uid: &str) -> Result<(), Error> { + let mut tx = self.db.begin().await?; + sqlx::query!( r#"UPDATE events SET deleted_at = NULL, updated_at = datetime() WHERE (principal, cid, uid) = (?, ?, ?)"#, principal, cid, uid ) - .execute(&self.db) + .execute(&mut *tx) .await?; + + log_event_operation( + &mut tx, + principal, + cid, + uid, + CalendarChangeOperation::Delete, + ) + .await?; + tx.commit().await?; Ok(()) } }