mirror of
https://github.com/lennart-k/rustical.git
synced 2025-12-14 16:32:29 +00:00
Add basic sync-token implementation
This commit is contained in:
@@ -42,6 +42,8 @@ pub enum CalendarPropName {
|
|||||||
CurrentUserPrivilegeSet,
|
CurrentUserPrivilegeSet,
|
||||||
MaxResourceSize,
|
MaxResourceSize,
|
||||||
SupportedReportSet,
|
SupportedReportSet,
|
||||||
|
SyncToken,
|
||||||
|
Getctag,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug, Clone, Deserialize, Serialize)]
|
#[derive(Debug, Clone, Deserialize, Serialize)]
|
||||||
@@ -73,6 +75,8 @@ pub enum CalendarProp {
|
|||||||
MaxResourceSize(String),
|
MaxResourceSize(String),
|
||||||
CurrentUserPrivilegeSet(UserPrivilegeSet),
|
CurrentUserPrivilegeSet(UserPrivilegeSet),
|
||||||
SupportedReportSet(SupportedReportSet),
|
SupportedReportSet(SupportedReportSet),
|
||||||
|
SyncToken(String),
|
||||||
|
Getctag(String),
|
||||||
#[serde(other)]
|
#[serde(other)]
|
||||||
Invalid,
|
Invalid,
|
||||||
}
|
}
|
||||||
@@ -141,6 +145,14 @@ impl Resource for CalendarFile {
|
|||||||
CalendarPropName::SupportedReportSet => {
|
CalendarPropName::SupportedReportSet => {
|
||||||
CalendarProp::SupportedReportSet(SupportedReportSet::default())
|
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::MaxResourceSize(_) => Err(rustical_dav::Error::PropReadOnly),
|
||||||
CalendarProp::CurrentUserPrivilegeSet(_) => Err(rustical_dav::Error::PropReadOnly),
|
CalendarProp::CurrentUserPrivilegeSet(_) => Err(rustical_dav::Error::PropReadOnly),
|
||||||
CalendarProp::SupportedReportSet(_) => 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),
|
CalendarProp::Invalid => Err(rustical_dav::Error::PropReadOnly),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -217,6 +231,8 @@ impl Resource for CalendarFile {
|
|||||||
CalendarPropName::MaxResourceSize => Err(rustical_dav::Error::PropReadOnly),
|
CalendarPropName::MaxResourceSize => Err(rustical_dav::Error::PropReadOnly),
|
||||||
CalendarPropName::CurrentUserPrivilegeSet => Err(rustical_dav::Error::PropReadOnly),
|
CalendarPropName::CurrentUserPrivilegeSet => Err(rustical_dav::Error::PropReadOnly),
|
||||||
CalendarPropName::SupportedReportSet => 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),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
CREATE TABLE calendars (
|
CREATE TABLE calendars (
|
||||||
principal TEXT NOT NULL,
|
principal TEXT NOT NULL,
|
||||||
id TEXT NOT NULL,
|
id TEXT NOT NULL,
|
||||||
|
synctoken INTEGER DEFAULT 0 NOT NULL,
|
||||||
displayname TEXT,
|
displayname TEXT,
|
||||||
description TEXT,
|
description TEXT,
|
||||||
'order' INT DEFAULT 0 NOT NULL,
|
'order' INT DEFAULT 0 NOT NULL,
|
||||||
@@ -21,3 +22,15 @@ CREATE TABLE events (
|
|||||||
FOREIGN KEY (principal, cid) REFERENCES calendars(principal, id)
|
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
|
||||||
|
)
|
||||||
|
|
||||||
|
|||||||
@@ -11,4 +11,5 @@ pub struct Calendar {
|
|||||||
pub color: Option<String>,
|
pub color: Option<String>,
|
||||||
pub timezone: Option<String>,
|
pub timezone: Option<String>,
|
||||||
pub deleted_at: Option<NaiveDateTime>,
|
pub deleted_at: Option<NaiveDateTime>,
|
||||||
|
pub synctoken: i64,
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,10 +1,12 @@
|
|||||||
|
use crate::calendar::Calendar;
|
||||||
|
use crate::event::Event;
|
||||||
|
use crate::{CalendarStore, Error};
|
||||||
use anyhow::Result;
|
use anyhow::Result;
|
||||||
use async_trait::async_trait;
|
use async_trait::async_trait;
|
||||||
|
use serde::Serialize;
|
||||||
|
use sqlx::Transaction;
|
||||||
use sqlx::{sqlite::SqliteConnectOptions, Pool, Sqlite, SqlitePool};
|
use sqlx::{sqlite::SqliteConnectOptions, Pool, Sqlite, SqlitePool};
|
||||||
|
|
||||||
use crate::event::Event;
|
|
||||||
use crate::{calendar::Calendar, CalendarStore, Error};
|
|
||||||
|
|
||||||
#[derive(Debug)]
|
#[derive(Debug)]
|
||||||
pub struct SqliteCalendarStore {
|
pub struct SqliteCalendarStore {
|
||||||
db: SqlitePool,
|
db: SqlitePool,
|
||||||
@@ -30,12 +32,55 @@ impl TryFrom<EventRow> 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]
|
#[async_trait]
|
||||||
impl CalendarStore for SqliteCalendarStore {
|
impl CalendarStore for SqliteCalendarStore {
|
||||||
async fn get_calendar(&self, principal: &str, id: &str) -> Result<Calendar, Error> {
|
async fn get_calendar(&self, principal: &str, id: &str) -> Result<Calendar, Error> {
|
||||||
let cal = sqlx::query_as!(
|
let cal = sqlx::query_as!(
|
||||||
Calendar,
|
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
|
FROM calendars
|
||||||
WHERE (principal, id) = (?, ?)"#,
|
WHERE (principal, id) = (?, ?)"#,
|
||||||
principal,
|
principal,
|
||||||
@@ -49,7 +94,7 @@ impl CalendarStore for SqliteCalendarStore {
|
|||||||
async fn get_calendars(&self, principal: &str) -> Result<Vec<Calendar>, Error> {
|
async fn get_calendars(&self, principal: &str) -> Result<Vec<Calendar>, Error> {
|
||||||
let cals = sqlx::query_as!(
|
let cals = sqlx::query_as!(
|
||||||
Calendar,
|
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
|
FROM calendars
|
||||||
WHERE principal = ? AND deleted_at IS NULL"#,
|
WHERE principal = ? AND deleted_at IS NULL"#,
|
||||||
principal
|
principal
|
||||||
@@ -176,7 +221,10 @@ impl CalendarStore for SqliteCalendarStore {
|
|||||||
uid: String,
|
uid: String,
|
||||||
ics: String,
|
ics: String,
|
||||||
) -> Result<(), Error> {
|
) -> 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!(
|
sqlx::query!(
|
||||||
"REPLACE INTO events (principal, cid, uid, ics) VALUES (?, ?, ?, ?)",
|
"REPLACE INTO events (principal, cid, uid, ics) VALUES (?, ?, ?, ?)",
|
||||||
principal,
|
principal,
|
||||||
@@ -184,8 +232,18 @@ impl CalendarStore for SqliteCalendarStore {
|
|||||||
uid,
|
uid,
|
||||||
ics,
|
ics,
|
||||||
)
|
)
|
||||||
.execute(&self.db)
|
.execute(&mut *tx)
|
||||||
.await?;
|
.await?;
|
||||||
|
|
||||||
|
log_event_operation(
|
||||||
|
&mut tx,
|
||||||
|
&principal,
|
||||||
|
&cid,
|
||||||
|
&uid,
|
||||||
|
CalendarChangeOperation::Add,
|
||||||
|
)
|
||||||
|
.await?;
|
||||||
|
tx.commit().await?;
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -196,6 +254,8 @@ impl CalendarStore for SqliteCalendarStore {
|
|||||||
uid: &str,
|
uid: &str,
|
||||||
use_trashbin: bool,
|
use_trashbin: bool,
|
||||||
) -> Result<(), Error> {
|
) -> Result<(), Error> {
|
||||||
|
let mut tx = self.db.begin().await?;
|
||||||
|
|
||||||
match use_trashbin {
|
match use_trashbin {
|
||||||
true => {
|
true => {
|
||||||
sqlx::query!(
|
sqlx::query!(
|
||||||
@@ -204,27 +264,48 @@ impl CalendarStore for SqliteCalendarStore {
|
|||||||
cid,
|
cid,
|
||||||
uid
|
uid
|
||||||
)
|
)
|
||||||
.execute(&self.db)
|
.execute(&mut *tx)
|
||||||
.await?;
|
.await?;
|
||||||
}
|
}
|
||||||
false => {
|
false => {
|
||||||
sqlx::query!("DELETE FROM events WHERE cid = ? AND uid = ?", cid, uid)
|
sqlx::query!("DELETE FROM events WHERE cid = ? AND uid = ?", cid, uid)
|
||||||
.execute(&self.db)
|
.execute(&mut *tx)
|
||||||
.await?;
|
.await?;
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
log_event_operation(
|
||||||
|
&mut tx,
|
||||||
|
principal,
|
||||||
|
cid,
|
||||||
|
uid,
|
||||||
|
CalendarChangeOperation::Delete,
|
||||||
|
)
|
||||||
|
.await?;
|
||||||
|
tx.commit().await?;
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
async fn restore_event(&mut self, principal: &str, cid: &str, uid: &str) -> Result<(), Error> {
|
async fn restore_event(&mut self, principal: &str, cid: &str, uid: &str) -> Result<(), Error> {
|
||||||
|
let mut tx = self.db.begin().await?;
|
||||||
|
|
||||||
sqlx::query!(
|
sqlx::query!(
|
||||||
r#"UPDATE events SET deleted_at = NULL, updated_at = datetime() WHERE (principal, cid, uid) = (?, ?, ?)"#,
|
r#"UPDATE events SET deleted_at = NULL, updated_at = datetime() WHERE (principal, cid, uid) = (?, ?, ?)"#,
|
||||||
principal,
|
principal,
|
||||||
cid,
|
cid,
|
||||||
uid
|
uid
|
||||||
)
|
)
|
||||||
.execute(&self.db)
|
.execute(&mut *tx)
|
||||||
.await?;
|
.await?;
|
||||||
|
|
||||||
|
log_event_operation(
|
||||||
|
&mut tx,
|
||||||
|
principal,
|
||||||
|
cid,
|
||||||
|
uid,
|
||||||
|
CalendarChangeOperation::Delete,
|
||||||
|
)
|
||||||
|
.await?;
|
||||||
|
tx.commit().await?;
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user