mirror of
https://github.com/lennart-k/rustical.git
synced 2025-12-14 08:12:24 +00:00
Compare commits
9 Commits
v0.10.3
...
eba377b980
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
eba377b980 | ||
|
|
54f1ee0788 | ||
|
|
22a0337375 | ||
|
|
21902e108a | ||
|
|
08f526fa5b | ||
|
|
ac73f3aaff | ||
|
|
9fdc8434db | ||
|
|
85f3d89235 | ||
|
|
092604694a |
38
.sqlx/query-053c17f3b54ae3e153137926115486eb19a801bd73a74230bcf72a9a7254824a.json
generated
Normal file
38
.sqlx/query-053c17f3b54ae3e153137926115486eb19a801bd73a74230bcf72a9a7254824a.json
generated
Normal file
@@ -0,0 +1,38 @@
|
|||||||
|
{
|
||||||
|
"db_name": "SQLite",
|
||||||
|
"query": "\n SELECT principal, cal_id, id, (deleted_at IS NOT NULL) AS \"deleted: bool\"\n FROM calendarobjects\n WHERE (principal, cal_id, id) NOT IN (\n SELECT DISTINCT principal, cal_id, object_id FROM calendarobjectchangelog\n )\n ;\n ",
|
||||||
|
"describe": {
|
||||||
|
"columns": [
|
||||||
|
{
|
||||||
|
"name": "principal",
|
||||||
|
"ordinal": 0,
|
||||||
|
"type_info": "Text"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "cal_id",
|
||||||
|
"ordinal": 1,
|
||||||
|
"type_info": "Text"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "id",
|
||||||
|
"ordinal": 2,
|
||||||
|
"type_info": "Text"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "deleted: bool",
|
||||||
|
"ordinal": 3,
|
||||||
|
"type_info": "Integer"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"parameters": {
|
||||||
|
"Right": 0
|
||||||
|
},
|
||||||
|
"nullable": [
|
||||||
|
false,
|
||||||
|
false,
|
||||||
|
false,
|
||||||
|
false
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"hash": "053c17f3b54ae3e153137926115486eb19a801bd73a74230bcf72a9a7254824a"
|
||||||
|
}
|
||||||
38
.sqlx/query-c138b1143ac04af4930266ffae0990e82005911c11a683ad565e92335e085f4d.json
generated
Normal file
38
.sqlx/query-c138b1143ac04af4930266ffae0990e82005911c11a683ad565e92335e085f4d.json
generated
Normal file
@@ -0,0 +1,38 @@
|
|||||||
|
{
|
||||||
|
"db_name": "SQLite",
|
||||||
|
"query": "\n SELECT principal, addressbook_id, id, (deleted_at IS NOT NULL) AS \"deleted: bool\"\n FROM addressobjects\n WHERE (principal, addressbook_id, id) NOT IN (\n SELECT DISTINCT principal, addressbook_id, object_id FROM addressobjectchangelog\n )\n ;\n ",
|
||||||
|
"describe": {
|
||||||
|
"columns": [
|
||||||
|
{
|
||||||
|
"name": "principal",
|
||||||
|
"ordinal": 0,
|
||||||
|
"type_info": "Text"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "addressbook_id",
|
||||||
|
"ordinal": 1,
|
||||||
|
"type_info": "Text"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "id",
|
||||||
|
"ordinal": 2,
|
||||||
|
"type_info": "Text"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "deleted: bool",
|
||||||
|
"ordinal": 3,
|
||||||
|
"type_info": "Integer"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"parameters": {
|
||||||
|
"Right": 0
|
||||||
|
},
|
||||||
|
"nullable": [
|
||||||
|
false,
|
||||||
|
false,
|
||||||
|
false,
|
||||||
|
false
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"hash": "c138b1143ac04af4930266ffae0990e82005911c11a683ad565e92335e085f4d"
|
||||||
|
}
|
||||||
422
Cargo.lock
generated
422
Cargo.lock
generated
File diff suppressed because it is too large
Load Diff
@@ -2,7 +2,7 @@
|
|||||||
members = ["crates/*"]
|
members = ["crates/*"]
|
||||||
|
|
||||||
[workspace.package]
|
[workspace.package]
|
||||||
version = "0.10.3"
|
version = "0.10.5"
|
||||||
rust-version = "1.91"
|
rust-version = "1.91"
|
||||||
edition = "2024"
|
edition = "2024"
|
||||||
description = "A CalDAV server"
|
description = "A CalDAV server"
|
||||||
@@ -133,7 +133,7 @@ syn = { version = "2.0", features = ["full"] }
|
|||||||
quote = "1.0"
|
quote = "1.0"
|
||||||
proc-macro2 = "1.0"
|
proc-macro2 = "1.0"
|
||||||
heck = "0.5"
|
heck = "0.5"
|
||||||
darling = "0.21"
|
darling = "0.23"
|
||||||
reqwest = { version = "0.12", features = [
|
reqwest = { version = "0.12", features = [
|
||||||
"rustls-tls",
|
"rustls-tls",
|
||||||
"charset",
|
"charset",
|
||||||
@@ -148,6 +148,7 @@ ece = { version = "2.3", default-features = false, features = [
|
|||||||
] }
|
] }
|
||||||
openssl = { version = "0.10", features = ["vendored"] }
|
openssl = { version = "0.10", features = ["vendored"] }
|
||||||
async-std = { version = "1.13", features = ["attributes"] }
|
async-std = { version = "1.13", features = ["attributes"] }
|
||||||
|
similar-asserts = "1.7"
|
||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
rustical_store.workspace = true
|
rustical_store.workspace = true
|
||||||
|
|||||||
@@ -45,3 +45,4 @@ tower-http.workspace = true
|
|||||||
strum.workspace = true
|
strum.workspace = true
|
||||||
strum_macros.workspace = true
|
strum_macros.workspace = true
|
||||||
vtimezones-rs.workspace = true
|
vtimezones-rs.workspace = true
|
||||||
|
similar-asserts.workspace = true
|
||||||
|
|||||||
@@ -26,16 +26,18 @@ pub async fn get_objects_calendar_multiget<C: CalendarStore>(
|
|||||||
let mut not_found = vec![];
|
let mut not_found = vec![];
|
||||||
|
|
||||||
for href in &cal_query.href {
|
for href in &cal_query.href {
|
||||||
if let Some(filename) = href.strip_prefix(path) {
|
if let Ok(href) = percent_encoding::percent_decode_str(href).decode_utf8()
|
||||||
|
&& let Some(filename) = href.strip_prefix(path)
|
||||||
|
{
|
||||||
let filename = filename.trim_start_matches('/');
|
let filename = filename.trim_start_matches('/');
|
||||||
if let Some(object_id) = filename.strip_suffix(".ics") {
|
if let Some(object_id) = filename.strip_suffix(".ics") {
|
||||||
match store.get_object(principal, cal_id, object_id, false).await {
|
match store.get_object(principal, cal_id, object_id, false).await {
|
||||||
Ok(object) => result.push(object),
|
Ok(object) => result.push(object),
|
||||||
Err(rustical_store::Error::NotFound) => not_found.push(href.to_owned()),
|
Err(rustical_store::Error::NotFound) => not_found.push(href.to_string()),
|
||||||
Err(err) => return Err(err.into()),
|
Err(err) => return Err(err.into()),
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
not_found.push(href.to_owned());
|
not_found.push(href.to_string());
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
not_found.push(href.to_owned());
|
not_found.push(href.to_owned());
|
||||||
|
|||||||
@@ -39,9 +39,7 @@ async fn test_propfind() {
|
|||||||
.unwrap()
|
.unwrap()
|
||||||
.trim()
|
.trim()
|
||||||
.replace("\r\n", "\n");
|
.replace("\r\n", "\n");
|
||||||
println!("{output}");
|
similar_asserts::assert_eq!(output, expected_output);
|
||||||
println!("{}, {} \n\n\n", output.len(), expected_output.len());
|
|
||||||
assert_eq!(output, expected_output);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -34,7 +34,9 @@ pub async fn get_objects_addressbook_multiget<AS: AddressbookStore>(
|
|||||||
let mut not_found = vec![];
|
let mut not_found = vec![];
|
||||||
|
|
||||||
for href in &addressbook_multiget.href {
|
for href in &addressbook_multiget.href {
|
||||||
if let Some(filename) = href.strip_prefix(path) {
|
if let Ok(href) = percent_encoding::percent_decode_str(href).decode_utf8()
|
||||||
|
&& let Some(filename) = href.strip_prefix(path)
|
||||||
|
{
|
||||||
let filename = filename.trim_start_matches('/');
|
let filename = filename.trim_start_matches('/');
|
||||||
if let Some(object_id) = filename.strip_suffix(".vcf") {
|
if let Some(object_id) = filename.strip_suffix(".vcf") {
|
||||||
match store
|
match store
|
||||||
@@ -42,11 +44,11 @@ pub async fn get_objects_addressbook_multiget<AS: AddressbookStore>(
|
|||||||
.await
|
.await
|
||||||
{
|
{
|
||||||
Ok(object) => result.push(object),
|
Ok(object) => result.push(object),
|
||||||
Err(rustical_store::Error::NotFound) => not_found.push(href.to_owned()),
|
Err(rustical_store::Error::NotFound) => not_found.push(href.to_string()),
|
||||||
Err(err) => return Err(err.into()),
|
Err(err) => return Err(err.into()),
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
not_found.push(href.to_owned());
|
not_found.push(href.to_string());
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
not_found.push(href.to_owned());
|
not_found.push(href.to_owned());
|
||||||
|
|||||||
@@ -9,7 +9,7 @@ use rustical_store::{
|
|||||||
};
|
};
|
||||||
use sqlx::{Acquire, Executor, Sqlite, SqlitePool, Transaction};
|
use sqlx::{Acquire, Executor, Sqlite, SqlitePool, Transaction};
|
||||||
use tokio::sync::mpsc::Sender;
|
use tokio::sync::mpsc::Sender;
|
||||||
use tracing::{error, instrument};
|
use tracing::{error, instrument, warn};
|
||||||
|
|
||||||
#[derive(Debug, Clone)]
|
#[derive(Debug, Clone)]
|
||||||
struct AddressObjectRow {
|
struct AddressObjectRow {
|
||||||
@@ -32,6 +32,60 @@ pub struct SqliteAddressbookStore {
|
|||||||
}
|
}
|
||||||
|
|
||||||
impl SqliteAddressbookStore {
|
impl SqliteAddressbookStore {
|
||||||
|
// Commit "orphaned" objects to the changelog table
|
||||||
|
pub async fn repair_orphans(&self) -> Result<(), Error> {
|
||||||
|
struct Row {
|
||||||
|
principal: String,
|
||||||
|
addressbook_id: String,
|
||||||
|
id: String,
|
||||||
|
deleted: bool,
|
||||||
|
}
|
||||||
|
|
||||||
|
let mut tx = self
|
||||||
|
.db
|
||||||
|
.begin_with(BEGIN_IMMEDIATE)
|
||||||
|
.await
|
||||||
|
.map_err(crate::Error::from)?;
|
||||||
|
|
||||||
|
let rows = sqlx::query_as!(
|
||||||
|
Row,
|
||||||
|
r#"
|
||||||
|
SELECT principal, addressbook_id, id, (deleted_at IS NOT NULL) AS "deleted: bool"
|
||||||
|
FROM addressobjects
|
||||||
|
WHERE (principal, addressbook_id, id) NOT IN (
|
||||||
|
SELECT DISTINCT principal, addressbook_id, object_id FROM addressobjectchangelog
|
||||||
|
)
|
||||||
|
;
|
||||||
|
"#,
|
||||||
|
)
|
||||||
|
.fetch_all(&mut *tx)
|
||||||
|
.await
|
||||||
|
.map_err(crate::Error::from)?;
|
||||||
|
|
||||||
|
for row in rows {
|
||||||
|
let operation = if row.deleted {
|
||||||
|
ChangeOperation::Delete
|
||||||
|
} else {
|
||||||
|
ChangeOperation::Add
|
||||||
|
};
|
||||||
|
warn!(
|
||||||
|
"Commiting orphaned addressbook object ({},{},{}), deleted={}",
|
||||||
|
&row.principal, &row.addressbook_id, &row.id, &row.deleted
|
||||||
|
);
|
||||||
|
log_object_operation(
|
||||||
|
&mut tx,
|
||||||
|
&row.principal,
|
||||||
|
&row.addressbook_id,
|
||||||
|
&row.id,
|
||||||
|
operation,
|
||||||
|
)
|
||||||
|
.await?;
|
||||||
|
}
|
||||||
|
tx.commit().await.map_err(crate::Error::from)?;
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
async fn _get_addressbook<'e, E: Executor<'e, Database = Sqlite>>(
|
async fn _get_addressbook<'e, E: Executor<'e, Database = Sqlite>>(
|
||||||
executor: E,
|
executor: E,
|
||||||
principal: &str,
|
principal: &str,
|
||||||
@@ -90,9 +144,9 @@ impl SqliteAddressbookStore {
|
|||||||
|
|
||||||
async fn _update_addressbook<'e, E: Executor<'e, Database = Sqlite>>(
|
async fn _update_addressbook<'e, E: Executor<'e, Database = Sqlite>>(
|
||||||
executor: E,
|
executor: E,
|
||||||
principal: String,
|
principal: &str,
|
||||||
id: String,
|
id: &str,
|
||||||
addressbook: Addressbook,
|
addressbook: &Addressbook,
|
||||||
) -> Result<(), rustical_store::Error> {
|
) -> Result<(), rustical_store::Error> {
|
||||||
let result = sqlx::query!(
|
let result = sqlx::query!(
|
||||||
r#"UPDATE addressbooks SET principal = ?, id = ?, displayname = ?, description = ?, push_topic = ?
|
r#"UPDATE addressbooks SET principal = ?, id = ?, displayname = ?, description = ?, push_topic = ?
|
||||||
@@ -116,7 +170,7 @@ impl SqliteAddressbookStore {
|
|||||||
|
|
||||||
async fn _insert_addressbook<'e, E: Executor<'e, Database = Sqlite>>(
|
async fn _insert_addressbook<'e, E: Executor<'e, Database = Sqlite>>(
|
||||||
executor: E,
|
executor: E,
|
||||||
addressbook: Addressbook,
|
addressbook: &Addressbook,
|
||||||
) -> Result<(), rustical_store::Error> {
|
) -> Result<(), rustical_store::Error> {
|
||||||
sqlx::query!(
|
sqlx::query!(
|
||||||
r#"INSERT INTO addressbooks (principal, id, displayname, description, push_topic)
|
r#"INSERT INTO addressbooks (principal, id, displayname, description, push_topic)
|
||||||
@@ -283,9 +337,9 @@ impl SqliteAddressbookStore {
|
|||||||
|
|
||||||
async fn _put_object<'e, E: Executor<'e, Database = Sqlite>>(
|
async fn _put_object<'e, E: Executor<'e, Database = Sqlite>>(
|
||||||
executor: E,
|
executor: E,
|
||||||
principal: String,
|
principal: &str,
|
||||||
addressbook_id: String,
|
addressbook_id: &str,
|
||||||
object: AddressObject,
|
object: &AddressObject,
|
||||||
overwrite: bool,
|
overwrite: bool,
|
||||||
) -> Result<(), rustical_store::Error> {
|
) -> Result<(), rustical_store::Error> {
|
||||||
let (object_id, vcf) = (object.get_id(), object.get_vcf());
|
let (object_id, vcf) = (object.get_id(), object.get_vcf());
|
||||||
@@ -397,7 +451,7 @@ impl AddressbookStore for SqliteAddressbookStore {
|
|||||||
id: String,
|
id: String,
|
||||||
addressbook: Addressbook,
|
addressbook: Addressbook,
|
||||||
) -> Result<(), rustical_store::Error> {
|
) -> Result<(), rustical_store::Error> {
|
||||||
Self::_update_addressbook(&self.db, principal, id, addressbook).await
|
Self::_update_addressbook(&self.db, &principal, &id, &addressbook).await
|
||||||
}
|
}
|
||||||
|
|
||||||
#[instrument]
|
#[instrument]
|
||||||
@@ -405,7 +459,7 @@ impl AddressbookStore for SqliteAddressbookStore {
|
|||||||
&self,
|
&self,
|
||||||
addressbook: Addressbook,
|
addressbook: Addressbook,
|
||||||
) -> Result<(), rustical_store::Error> {
|
) -> Result<(), rustical_store::Error> {
|
||||||
Self::_insert_addressbook(&self.db, addressbook).await
|
Self::_insert_addressbook(&self.db, &addressbook).await
|
||||||
}
|
}
|
||||||
|
|
||||||
#[instrument]
|
#[instrument]
|
||||||
@@ -521,14 +575,7 @@ impl AddressbookStore for SqliteAddressbookStore {
|
|||||||
|
|
||||||
let object_id = object.get_id().to_owned();
|
let object_id = object.get_id().to_owned();
|
||||||
|
|
||||||
Self::_put_object(
|
Self::_put_object(&mut *tx, &principal, &addressbook_id, &object, overwrite).await?;
|
||||||
&mut *tx,
|
|
||||||
principal.clone(),
|
|
||||||
addressbook_id.clone(),
|
|
||||||
object,
|
|
||||||
overwrite,
|
|
||||||
)
|
|
||||||
.await?;
|
|
||||||
|
|
||||||
let sync_token = log_object_operation(
|
let sync_token = log_object_operation(
|
||||||
&mut tx,
|
&mut tx,
|
||||||
@@ -628,7 +675,7 @@ impl AddressbookStore for SqliteAddressbookStore {
|
|||||||
.await?
|
.await?
|
||||||
.push_topic,
|
.push_topic,
|
||||||
}) {
|
}) {
|
||||||
error!("Push notification about deleted addressbook failed: {err}");
|
error!("Push notification about restored addressbook object failed: {err}");
|
||||||
}
|
}
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
@@ -659,21 +706,44 @@ impl AddressbookStore for SqliteAddressbookStore {
|
|||||||
return Err(Error::AlreadyExists);
|
return Err(Error::AlreadyExists);
|
||||||
}
|
}
|
||||||
if existing.is_none() {
|
if existing.is_none() {
|
||||||
Self::_insert_addressbook(&mut *tx, addressbook.clone()).await?;
|
Self::_insert_addressbook(&mut *tx, &addressbook).await?;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
let mut sync_token = None;
|
||||||
for object in objects {
|
for object in objects {
|
||||||
Self::_put_object(
|
Self::_put_object(
|
||||||
&mut *tx,
|
&mut *tx,
|
||||||
addressbook.principal.clone(),
|
&addressbook.principal,
|
||||||
addressbook.id.clone(),
|
&addressbook.id,
|
||||||
object,
|
&object,
|
||||||
false,
|
false,
|
||||||
)
|
)
|
||||||
.await?;
|
.await?;
|
||||||
|
|
||||||
|
sync_token = Some(
|
||||||
|
log_object_operation(
|
||||||
|
&mut tx,
|
||||||
|
&addressbook.principal,
|
||||||
|
&addressbook.id,
|
||||||
|
object.get_id(),
|
||||||
|
ChangeOperation::Add,
|
||||||
|
)
|
||||||
|
.await?,
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
tx.commit().await.map_err(crate::Error::from)?;
|
tx.commit().await.map_err(crate::Error::from)?;
|
||||||
|
if let Some(sync_token) = sync_token
|
||||||
|
&& let Err(err) = self.sender.try_send(CollectionOperation {
|
||||||
|
data: CollectionOperationInfo::Content { sync_token },
|
||||||
|
topic: self
|
||||||
|
.get_addressbook(&addressbook.principal, &addressbook.id, true)
|
||||||
|
.await?
|
||||||
|
.push_topic,
|
||||||
|
})
|
||||||
|
{
|
||||||
|
error!("Push notification about imported addressbook failed: {err}");
|
||||||
|
}
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -685,7 +755,7 @@ async fn log_object_operation(
|
|||||||
addressbook_id: &str,
|
addressbook_id: &str,
|
||||||
object_id: &str,
|
object_id: &str,
|
||||||
operation: ChangeOperation,
|
operation: ChangeOperation,
|
||||||
) -> Result<String, sqlx::Error> {
|
) -> Result<String, Error> {
|
||||||
struct Synctoken {
|
struct Synctoken {
|
||||||
synctoken: i64,
|
synctoken: i64,
|
||||||
}
|
}
|
||||||
@@ -700,7 +770,8 @@ async fn log_object_operation(
|
|||||||
addressbook_id
|
addressbook_id
|
||||||
)
|
)
|
||||||
.fetch_one(&mut **tx)
|
.fetch_one(&mut **tx)
|
||||||
.await?;
|
.await
|
||||||
|
.map_err(crate::Error::from)?;
|
||||||
|
|
||||||
sqlx::query!(
|
sqlx::query!(
|
||||||
r#"
|
r#"
|
||||||
@@ -714,6 +785,7 @@ async fn log_object_operation(
|
|||||||
operation
|
operation
|
||||||
)
|
)
|
||||||
.execute(&mut **tx)
|
.execute(&mut **tx)
|
||||||
.await?;
|
.await
|
||||||
|
.map_err(crate::Error::from)?;
|
||||||
Ok(format_synctoken(synctoken))
|
Ok(format_synctoken(synctoken))
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -11,7 +11,7 @@ use rustical_store::{CollectionOperation, CollectionOperationInfo};
|
|||||||
use sqlx::types::chrono::NaiveDateTime;
|
use sqlx::types::chrono::NaiveDateTime;
|
||||||
use sqlx::{Acquire, Executor, Sqlite, SqlitePool, Transaction};
|
use sqlx::{Acquire, Executor, Sqlite, SqlitePool, Transaction};
|
||||||
use tokio::sync::mpsc::Sender;
|
use tokio::sync::mpsc::Sender;
|
||||||
use tracing::{error, instrument};
|
use tracing::{error, instrument, warn};
|
||||||
|
|
||||||
#[derive(Debug, Clone)]
|
#[derive(Debug, Clone)]
|
||||||
struct CalendarObjectRow {
|
struct CalendarObjectRow {
|
||||||
@@ -94,6 +94,53 @@ pub struct SqliteCalendarStore {
|
|||||||
}
|
}
|
||||||
|
|
||||||
impl SqliteCalendarStore {
|
impl SqliteCalendarStore {
|
||||||
|
// Commit "orphaned" objects to the changelog table
|
||||||
|
pub async fn repair_orphans(&self) -> Result<(), Error> {
|
||||||
|
struct Row {
|
||||||
|
principal: String,
|
||||||
|
cal_id: String,
|
||||||
|
id: String,
|
||||||
|
deleted: bool,
|
||||||
|
}
|
||||||
|
|
||||||
|
let mut tx = self
|
||||||
|
.db
|
||||||
|
.begin_with(BEGIN_IMMEDIATE)
|
||||||
|
.await
|
||||||
|
.map_err(crate::Error::from)?;
|
||||||
|
|
||||||
|
let rows = sqlx::query_as!(
|
||||||
|
Row,
|
||||||
|
r#"
|
||||||
|
SELECT principal, cal_id, id, (deleted_at IS NOT NULL) AS "deleted: bool"
|
||||||
|
FROM calendarobjects
|
||||||
|
WHERE (principal, cal_id, id) NOT IN (
|
||||||
|
SELECT DISTINCT principal, cal_id, object_id FROM calendarobjectchangelog
|
||||||
|
)
|
||||||
|
;
|
||||||
|
"#,
|
||||||
|
)
|
||||||
|
.fetch_all(&mut *tx)
|
||||||
|
.await
|
||||||
|
.map_err(crate::Error::from)?;
|
||||||
|
|
||||||
|
for row in rows {
|
||||||
|
let operation = if row.deleted {
|
||||||
|
ChangeOperation::Delete
|
||||||
|
} else {
|
||||||
|
ChangeOperation::Add
|
||||||
|
};
|
||||||
|
warn!(
|
||||||
|
"Commiting orphaned calendar object ({},{},{}), deleted={}",
|
||||||
|
&row.principal, &row.cal_id, &row.id, &row.deleted
|
||||||
|
);
|
||||||
|
log_object_operation(&mut tx, &row.principal, &row.cal_id, &row.id, operation).await?;
|
||||||
|
}
|
||||||
|
tx.commit().await.map_err(crate::Error::from)?;
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
async fn _get_calendar<'e, E: Executor<'e, Database = Sqlite>>(
|
async fn _get_calendar<'e, E: Executor<'e, Database = Sqlite>>(
|
||||||
executor: E,
|
executor: E,
|
||||||
principal: &str,
|
principal: &str,
|
||||||
@@ -351,9 +398,9 @@ impl SqliteCalendarStore {
|
|||||||
#[instrument]
|
#[instrument]
|
||||||
async fn _put_object<'e, E: Executor<'e, Database = Sqlite>>(
|
async fn _put_object<'e, E: Executor<'e, Database = Sqlite>>(
|
||||||
executor: E,
|
executor: E,
|
||||||
principal: String,
|
principal: &str,
|
||||||
cal_id: String,
|
cal_id: &str,
|
||||||
object: CalendarObject,
|
object: &CalendarObject,
|
||||||
overwrite: bool,
|
overwrite: bool,
|
||||||
) -> Result<(), Error> {
|
) -> Result<(), Error> {
|
||||||
let (object_id, uid, ics) = (object.get_id(), object.get_uid(), object.get_ics());
|
let (object_id, uid, ics) = (object.get_id(), object.get_uid(), object.get_ics());
|
||||||
@@ -600,18 +647,35 @@ impl CalendarStore for SqliteCalendarStore {
|
|||||||
Self::_insert_calendar(&mut *tx, calendar.clone()).await?;
|
Self::_insert_calendar(&mut *tx, calendar.clone()).await?;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
let mut sync_token = None;
|
||||||
for object in objects {
|
for object in objects {
|
||||||
Self::_put_object(
|
Self::_put_object(&mut *tx, &calendar.principal, &calendar.id, &object, false).await?;
|
||||||
&mut *tx,
|
|
||||||
calendar.principal.clone(),
|
sync_token = Some(
|
||||||
calendar.id.clone(),
|
log_object_operation(
|
||||||
object,
|
&mut tx,
|
||||||
false,
|
&calendar.principal,
|
||||||
)
|
&calendar.id,
|
||||||
.await?;
|
object.get_id(),
|
||||||
|
ChangeOperation::Add,
|
||||||
|
)
|
||||||
|
.await?,
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
tx.commit().await.map_err(crate::Error::from)?;
|
tx.commit().await.map_err(crate::Error::from)?;
|
||||||
|
|
||||||
|
if let Some(sync_token) = sync_token
|
||||||
|
&& let Err(err) = self.sender.try_send(CollectionOperation {
|
||||||
|
data: CollectionOperationInfo::Content { sync_token },
|
||||||
|
topic: self
|
||||||
|
.get_calendar(&calendar.principal, &calendar.id, true)
|
||||||
|
.await?
|
||||||
|
.push_topic,
|
||||||
|
})
|
||||||
|
{
|
||||||
|
error!("Push notification about imported calendar failed: {err}");
|
||||||
|
}
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -689,14 +753,7 @@ impl CalendarStore for SqliteCalendarStore {
|
|||||||
return Err(Error::ReadOnly);
|
return Err(Error::ReadOnly);
|
||||||
}
|
}
|
||||||
|
|
||||||
Self::_put_object(
|
Self::_put_object(&mut *tx, &principal, &cal_id, &object, overwrite).await?;
|
||||||
&mut *tx,
|
|
||||||
principal.clone(),
|
|
||||||
cal_id.clone(),
|
|
||||||
object,
|
|
||||||
overwrite,
|
|
||||||
)
|
|
||||||
.await?;
|
|
||||||
|
|
||||||
let sync_token = log_object_operation(
|
let sync_token = log_object_operation(
|
||||||
&mut tx,
|
&mut tx,
|
||||||
@@ -774,7 +831,7 @@ impl CalendarStore for SqliteCalendarStore {
|
|||||||
data: CollectionOperationInfo::Content { sync_token },
|
data: CollectionOperationInfo::Content { sync_token },
|
||||||
topic: self.get_calendar(principal, cal_id, true).await?.push_topic,
|
topic: self.get_calendar(principal, cal_id, true).await?.push_topic,
|
||||||
}) {
|
}) {
|
||||||
error!("Push notification about deleted calendar failed: {err}");
|
error!("Push notification about restored calendar object failed: {err}");
|
||||||
}
|
}
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
@@ -795,6 +852,7 @@ impl CalendarStore for SqliteCalendarStore {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Logs an operation to the events
|
// Logs an operation to the events
|
||||||
|
// TODO: Log multiple updates
|
||||||
async fn log_object_operation(
|
async fn log_object_operation(
|
||||||
tx: &mut Transaction<'_, Sqlite>,
|
tx: &mut Transaction<'_, Sqlite>,
|
||||||
principal: &str,
|
principal: &str,
|
||||||
|
|||||||
@@ -69,7 +69,9 @@ async fn get_data_stores(
|
|||||||
let (send, recv) = tokio::sync::mpsc::channel(1000);
|
let (send, recv) = tokio::sync::mpsc::channel(1000);
|
||||||
|
|
||||||
let addressbook_store = Arc::new(SqliteAddressbookStore::new(db.clone(), send.clone()));
|
let addressbook_store = Arc::new(SqliteAddressbookStore::new(db.clone(), send.clone()));
|
||||||
|
addressbook_store.repair_orphans().await?;
|
||||||
let cal_store = Arc::new(SqliteCalendarStore::new(db.clone(), send));
|
let cal_store = Arc::new(SqliteCalendarStore::new(db.clone(), send));
|
||||||
|
cal_store.repair_orphans().await?;
|
||||||
let subscription_store = Arc::new(SqliteStore::new(db.clone()));
|
let subscription_store = Arc::new(SqliteStore::new(db.clone()));
|
||||||
let principal_store = Arc::new(SqlitePrincipalStore::new(db));
|
let principal_store = Arc::new(SqlitePrincipalStore::new(db));
|
||||||
(
|
(
|
||||||
|
|||||||
Reference in New Issue
Block a user