mirror of
https://github.com/lennart-k/rustical.git
synced 2026-01-31 13:48:19 +00:00
Compare commits
19 Commits
4adf1818d4
...
b9c2a4cc27
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
b9c2a4cc27 | ||
|
|
c91205558e | ||
|
|
cd9e3ed8d6 | ||
|
|
200d5e7170 | ||
|
|
5cb538d3fb | ||
|
|
d9da123ff4 | ||
|
|
eba2f0da9f | ||
|
|
291bd967da | ||
|
|
002814a564 | ||
|
|
ba13aaa703 | ||
|
|
7a02bfeffc | ||
|
|
1b69148d6f | ||
|
|
f4de80c6b9 | ||
|
|
7a1ec3e351 | ||
|
|
eb7bdd0018 | ||
|
|
8e583e24cb | ||
|
|
5e5017a185 | ||
|
|
3c87191f69 | ||
|
|
d1947a159b |
2
.github/workflows/docker-publish.yml
vendored
2
.github/workflows/docker-publish.yml
vendored
@@ -2,7 +2,7 @@ name: Docker
|
||||
|
||||
on:
|
||||
push:
|
||||
branches: ["main", "dev"]
|
||||
branches: ["main"]
|
||||
release:
|
||||
types: ["published"]
|
||||
|
||||
|
||||
@@ -1,12 +0,0 @@
|
||||
{
|
||||
"db_name": "SQLite",
|
||||
"query": "DELETE FROM calendarobjectchangelog WHERE (principal, cal_id, object_id) = (?, ?, ?);",
|
||||
"describe": {
|
||||
"columns": [],
|
||||
"parameters": {
|
||||
"Right": 3
|
||||
},
|
||||
"nullable": []
|
||||
},
|
||||
"hash": "146e23ae4e0eaae4d65ac7563c67d4f295ccc2534dcc4b3bd710de773ed137f9"
|
||||
}
|
||||
@@ -1,12 +0,0 @@
|
||||
{
|
||||
"db_name": "SQLite",
|
||||
"query": "UPDATE calendarobjects SET ics = ? WHERE (principal, cal_id, id) = (?, ?, ?);",
|
||||
"describe": {
|
||||
"columns": [],
|
||||
"parameters": {
|
||||
"Right": 4
|
||||
},
|
||||
"nullable": []
|
||||
},
|
||||
"hash": "354decac84758c88280f60fbf0f93dddc6c7ff92ac7b8ba44049d31df3c680e3"
|
||||
}
|
||||
@@ -1,38 +0,0 @@
|
||||
{
|
||||
"db_name": "SQLite",
|
||||
"query": "SELECT principal, cal_id, id, ics FROM calendarobjects WHERE ics LIKE '%VERSION:4.0%';",
|
||||
"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": "ics",
|
||||
"ordinal": 3,
|
||||
"type_info": "Text"
|
||||
}
|
||||
],
|
||||
"parameters": {
|
||||
"Right": 0
|
||||
},
|
||||
"nullable": [
|
||||
false,
|
||||
false,
|
||||
false,
|
||||
false
|
||||
]
|
||||
},
|
||||
"hash": "bdaa4bee8b01d0e3773e34672ed4805d1e71d24888f2227045afd90bf080fc23"
|
||||
}
|
||||
30
Cargo.lock
generated
30
Cargo.lock
generated
@@ -2870,9 +2870,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "quick-xml"
|
||||
version = "0.39.0"
|
||||
version = "0.38.4"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "f2e3bf4aa9d243beeb01a7b3bc30b77cfe2c44e24ec02d751a7104a53c2c49a1"
|
||||
checksum = "b66c2058c55a409d601666cffe35f04333cf1013010882cec174a7467cd4e21c"
|
||||
dependencies = [
|
||||
"memchr",
|
||||
]
|
||||
@@ -3317,7 +3317,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "rustical"
|
||||
version = "0.11.17"
|
||||
version = "0.11.10"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"argon2",
|
||||
@@ -3328,7 +3328,6 @@ dependencies = [
|
||||
"figment",
|
||||
"headers",
|
||||
"http",
|
||||
"ical",
|
||||
"insta",
|
||||
"opentelemetry",
|
||||
"opentelemetry-otlp",
|
||||
@@ -3363,7 +3362,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "rustical_caldav"
|
||||
version = "0.11.17"
|
||||
version = "0.11.10"
|
||||
dependencies = [
|
||||
"async-std",
|
||||
"async-trait",
|
||||
@@ -3405,7 +3404,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "rustical_carddav"
|
||||
version = "0.11.17"
|
||||
version = "0.11.10"
|
||||
dependencies = [
|
||||
"async-trait",
|
||||
"axum",
|
||||
@@ -3439,7 +3438,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "rustical_dav"
|
||||
version = "0.11.17"
|
||||
version = "0.11.10"
|
||||
dependencies = [
|
||||
"async-trait",
|
||||
"axum",
|
||||
@@ -3465,7 +3464,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "rustical_dav_push"
|
||||
version = "0.11.17"
|
||||
version = "0.11.10"
|
||||
dependencies = [
|
||||
"async-trait",
|
||||
"axum",
|
||||
@@ -3490,7 +3489,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "rustical_frontend"
|
||||
version = "0.11.17"
|
||||
version = "0.11.10"
|
||||
dependencies = [
|
||||
"askama",
|
||||
"askama_web",
|
||||
@@ -3526,7 +3525,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "rustical_ical"
|
||||
version = "0.11.17"
|
||||
version = "0.11.10"
|
||||
dependencies = [
|
||||
"axum",
|
||||
"chrono",
|
||||
@@ -3545,7 +3544,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "rustical_oidc"
|
||||
version = "0.11.17"
|
||||
version = "0.11.10"
|
||||
dependencies = [
|
||||
"async-trait",
|
||||
"axum",
|
||||
@@ -3561,7 +3560,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "rustical_store"
|
||||
version = "0.11.17"
|
||||
version = "0.11.10"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"async-trait",
|
||||
@@ -3594,7 +3593,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "rustical_store_sqlite"
|
||||
version = "0.11.17"
|
||||
version = "0.11.10"
|
||||
dependencies = [
|
||||
"async-trait",
|
||||
"chrono",
|
||||
@@ -3604,7 +3603,6 @@ dependencies = [
|
||||
"password-auth",
|
||||
"password-hash",
|
||||
"pbkdf2",
|
||||
"regex",
|
||||
"rstest",
|
||||
"rustical_ical",
|
||||
"rustical_store",
|
||||
@@ -3619,7 +3617,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "rustical_xml"
|
||||
version = "0.11.17"
|
||||
version = "0.11.10"
|
||||
dependencies = [
|
||||
"quick-xml",
|
||||
"thiserror 2.0.17",
|
||||
@@ -5441,7 +5439,7 @@ checksum = "9edde0db4769d2dc68579893f2306b26c6ecfbe0ef499b013d731b7b9247e0b9"
|
||||
|
||||
[[package]]
|
||||
name = "xml_derive"
|
||||
version = "0.11.17"
|
||||
version = "0.11.10"
|
||||
dependencies = [
|
||||
"darling 0.23.0",
|
||||
"heck",
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
members = ["crates/*"]
|
||||
|
||||
[workspace.package]
|
||||
version = "0.11.17"
|
||||
version = "0.11.10"
|
||||
rust-version = "1.92"
|
||||
edition = "2024"
|
||||
description = "A CalDAV server"
|
||||
@@ -73,7 +73,7 @@ tokio = { version = "1.48", features = [
|
||||
url = "2.5"
|
||||
base64 = "0.22"
|
||||
thiserror = "2.0"
|
||||
quick-xml = { version = "0.39" }
|
||||
quick-xml = { version = "0.38" }
|
||||
rust-embed = "8.9"
|
||||
tower-sessions = "0.14"
|
||||
futures-core = "0.3"
|
||||
@@ -160,7 +160,6 @@ rustical_store_sqlite.workspace = true
|
||||
rustical_caldav.workspace = true
|
||||
rustical_carddav.workspace = true
|
||||
rustical_frontend.workspace = true
|
||||
ical.workspace = true
|
||||
toml.workspace = true
|
||||
serde.workspace = true
|
||||
tokio.workspace = true
|
||||
|
||||
@@ -36,7 +36,7 @@ COPY --from=planner /rustical/recipe.json recipe.json
|
||||
RUN cargo chef cook --release --target "$(cat /tmp/rust_target)"
|
||||
|
||||
COPY . .
|
||||
RUN cargo install --locked --target "$(cat /tmp/rust_target)" --path .
|
||||
RUN cargo install --target "$(cat /tmp/rust_target)" --path .
|
||||
|
||||
FROM scratch
|
||||
COPY --from=builder /usr/local/cargo/bin/rustical /usr/local/bin/rustical
|
||||
|
||||
@@ -45,7 +45,7 @@ impl<PN: XmlDeserialize> XmlDeserialize for PropElement<PN> {
|
||||
// start of a child element
|
||||
Event::Start(start) | Event::Empty(start) => {
|
||||
let empty = matches!(event, Event::Empty(_));
|
||||
let (ns, name) = reader.resolver().resolve_element(start.name());
|
||||
let (ns, name) = reader.resolve_element(start.name());
|
||||
let ns = match ns {
|
||||
ResolveResult::Bound(ns) => Some(NamespaceOwned::from(ns)),
|
||||
ResolveResult::Unknown(_ns) => todo!("handle error"),
|
||||
|
||||
@@ -37,4 +37,3 @@ pbkdf2.workspace = true
|
||||
rustical_ical.workspace = true
|
||||
rstest = { workspace = true, optional = true }
|
||||
sha2.workspace = true
|
||||
regex.workspace = true
|
||||
|
||||
@@ -4,7 +4,6 @@ use async_trait::async_trait;
|
||||
use chrono::TimeDelta;
|
||||
use derive_more::derive::Constructor;
|
||||
use ical::types::CalDateTime;
|
||||
use regex::Regex;
|
||||
use rustical_ical::{CalendarObject, CalendarObjectType};
|
||||
use rustical_store::calendar_store::CalendarQuery;
|
||||
use rustical_store::synctoken::format_synctoken;
|
||||
@@ -146,83 +145,6 @@ impl SqliteCalendarStore {
|
||||
}
|
||||
}
|
||||
|
||||
/// In the past exports generated objects with invalid VERSION:4.0
|
||||
/// This repair sets them to VERSION:2.0
|
||||
#[allow(clippy::missing_panics_doc)]
|
||||
pub async fn repair_invalid_version_4_0(&self) -> Result<(), Error> {
|
||||
struct Row {
|
||||
principal: String,
|
||||
cal_id: String,
|
||||
id: String,
|
||||
ics: String,
|
||||
}
|
||||
|
||||
let mut tx = self
|
||||
.db
|
||||
.begin_with(BEGIN_IMMEDIATE)
|
||||
.await
|
||||
.map_err(crate::Error::from)?;
|
||||
|
||||
#[allow(clippy::missing_panics_doc)]
|
||||
let version_pattern = Regex::new(r"(?mi)^VERSION:4.0").unwrap();
|
||||
|
||||
let repairs: Vec<Row> = sqlx::query_as!(
|
||||
Row,
|
||||
r#"SELECT principal, cal_id, id, ics FROM calendarobjects WHERE ics LIKE '%VERSION:4.0%';"#
|
||||
)
|
||||
.fetch_all(&mut *tx)
|
||||
.await
|
||||
.map_err(crate::Error::from)?
|
||||
.into_iter()
|
||||
.filter_map(|mut row| {
|
||||
version_pattern.find(&row.ics)?;
|
||||
let new_ics = version_pattern.replace(&row.ics, "VERSION:2.0");
|
||||
// Safeguard that we really only changed the version
|
||||
assert_eq!(row.ics.len(), new_ics.len());
|
||||
row.ics = new_ics.to_string();
|
||||
Some(row)
|
||||
})
|
||||
.collect();
|
||||
|
||||
if repairs.is_empty() {
|
||||
return Ok(());
|
||||
}
|
||||
warn!(
|
||||
"Found {} calendar objects with invalid VERSION:4.0. Repairing by setting to VERSION:2.0",
|
||||
repairs.len()
|
||||
);
|
||||
|
||||
for repair in &repairs {
|
||||
// calendarobjectchangelog is used by sync-collection to fetch changes
|
||||
// By deleting entries we will later regenerate new entries such that clients will notice
|
||||
// the objects have changed
|
||||
warn!(
|
||||
"Repairing VERSION for {}/{}/{}.ics",
|
||||
repair.principal, repair.cal_id, repair.id
|
||||
);
|
||||
sqlx::query!(
|
||||
"DELETE FROM calendarobjectchangelog WHERE (principal, cal_id, object_id) = (?, ?, ?);",
|
||||
repair.principal, repair.cal_id, repair.id
|
||||
).execute(&mut *tx).await
|
||||
.map_err(crate::Error::from)?;
|
||||
|
||||
sqlx::query!(
|
||||
"UPDATE calendarobjects SET ics = ? WHERE (principal, cal_id, id) = (?, ?, ?);",
|
||||
repair.ics,
|
||||
repair.principal,
|
||||
repair.cal_id,
|
||||
repair.id
|
||||
)
|
||||
.execute(&mut *tx)
|
||||
.await
|
||||
.map_err(crate::Error::from)?;
|
||||
}
|
||||
|
||||
tx.commit().await.map_err(crate::Error::from)?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
// Commit "orphaned" objects to the changelog table
|
||||
pub async fn repair_orphans(&self) -> Result<(), Error> {
|
||||
struct Row {
|
||||
|
||||
@@ -136,7 +136,7 @@ impl NamedStruct {
|
||||
#(#builder_field_inits),*
|
||||
};
|
||||
|
||||
let (ns, name) = reader.resolver().resolve_element(start.name());
|
||||
let (ns, name) = reader.resolve_element(start.name());
|
||||
#(#tagname_field_branches);*
|
||||
#(#namespace_field_branches);*
|
||||
|
||||
@@ -161,7 +161,7 @@ impl NamedStruct {
|
||||
// start of a child element
|
||||
Event::Start(start) | Event::Empty(start) => {
|
||||
let empty = matches!(event, Event::Empty(_));
|
||||
let (ns, name) = reader.resolver().resolve_element(start.name());
|
||||
let (ns, name) = reader.resolve_element(start.name());
|
||||
match (ns, name.as_ref()) {
|
||||
#(#named_field_branches),*
|
||||
#(#untagged_field_branches),*
|
||||
|
||||
@@ -42,7 +42,7 @@ impl<T: XmlRootTag + XmlDeserialize> XmlDocument for T {
|
||||
match event {
|
||||
Event::Decl(_) | Event::Comment(_) => { /* ignore this */ }
|
||||
Event::Start(start) | Event::Empty(start) => {
|
||||
let (ns, name) = reader.resolver().resolve_element(start.name());
|
||||
let (ns, name) = reader.resolve_element(start.name());
|
||||
let matches = match (Self::root_ns(), &ns, name) {
|
||||
// Wrong tag
|
||||
(_, _, name) if name.as_ref() != Self::root_tag().as_bytes() => false,
|
||||
|
||||
@@ -17,7 +17,6 @@ pub fn cmd_gen_config(_args: GenConfigArgs) -> anyhow::Result<()> {
|
||||
http: HttpConfig::default(),
|
||||
data_store: DataStoreConfig::Sqlite(SqliteDataStoreConfig {
|
||||
db_url: "/var/lib/rustical/db.sqlite3".to_owned(),
|
||||
run_repairs: true,
|
||||
}),
|
||||
tracing: TracingConfig::default(),
|
||||
frontend: FrontendConfig {
|
||||
|
||||
@@ -26,8 +26,6 @@ impl Default for HttpConfig {
|
||||
#[serde(deny_unknown_fields)]
|
||||
pub struct SqliteDataStoreConfig {
|
||||
pub db_url: String,
|
||||
#[serde(default = "default_true")]
|
||||
pub run_repairs: bool,
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize, Serialize)]
|
||||
|
||||
24
src/main.rs
24
src/main.rs
@@ -25,7 +25,7 @@ use std::sync::Arc;
|
||||
use tokio::sync::mpsc::Receiver;
|
||||
use tower::Layer;
|
||||
use tower_http::normalize_path::NormalizePathLayer;
|
||||
use tracing::{info, warn};
|
||||
use tracing::info;
|
||||
|
||||
mod app;
|
||||
mod commands;
|
||||
@@ -34,9 +34,6 @@ mod config;
|
||||
pub mod integration_tests;
|
||||
mod setup_tracing;
|
||||
|
||||
mod migration_0_12;
|
||||
use migration_0_12::{validate_address_objects_0_12, validate_calendar_objects_0_12};
|
||||
|
||||
#[derive(Parser, Debug)]
|
||||
#[command(author, version, about, long_about = None)]
|
||||
struct Args {
|
||||
@@ -70,22 +67,15 @@ async fn get_data_stores(
|
||||
Receiver<CollectionOperation>,
|
||||
)> {
|
||||
Ok(match &config {
|
||||
DataStoreConfig::Sqlite(SqliteDataStoreConfig {
|
||||
db_url,
|
||||
run_repairs,
|
||||
}) => {
|
||||
DataStoreConfig::Sqlite(SqliteDataStoreConfig { db_url }) => {
|
||||
let db = create_db_pool(db_url, migrate).await?;
|
||||
// Channel to watch for changes (for DAV Push)
|
||||
let (send, recv) = tokio::sync::mpsc::channel(1000);
|
||||
|
||||
let addressbook_store = Arc::new(SqliteAddressbookStore::new(db.clone(), send.clone()));
|
||||
let cal_store = Arc::new(SqliteCalendarStore::new(db.clone(), send));
|
||||
if *run_repairs {
|
||||
info!("Running repair tasks");
|
||||
addressbook_store.repair_orphans().await?;
|
||||
cal_store.repair_invalid_version_4_0().await?;
|
||||
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 principal_store = Arc::new(SqlitePrincipalStore::new(db));
|
||||
(
|
||||
@@ -125,14 +115,6 @@ async fn main() -> Result<()> {
|
||||
let (addr_store, cal_store, subscription_store, principal_store, update_recv) =
|
||||
get_data_stores(!args.no_migrations, &config.data_store).await?;
|
||||
|
||||
warn!(
|
||||
"Validating calendar data against the next-version ical parser.
|
||||
In the next major release these will be rejected and cause errors.
|
||||
If any errors occur, please open an issue so they can be fixed before the next major release."
|
||||
);
|
||||
validate_calendar_objects_0_12(principal_store.as_ref(), cal_store.as_ref()).await?;
|
||||
validate_address_objects_0_12(principal_store.as_ref(), addr_store.as_ref()).await?;
|
||||
|
||||
let mut tasks = vec![];
|
||||
|
||||
if config.dav_push.enabled {
|
||||
|
||||
@@ -1,76 +0,0 @@
|
||||
use ical::parser::{ical::IcalObjectParser, vcard::VcardParser};
|
||||
use rustical_store::{AddressbookStore, CalendarStore, auth::AuthenticationProvider};
|
||||
use tracing::{error, info};
|
||||
|
||||
pub async fn validate_calendar_objects_0_12(
|
||||
principal_store: &impl AuthenticationProvider,
|
||||
cal_store: &impl CalendarStore,
|
||||
) -> Result<(), rustical_store::Error> {
|
||||
let mut success = true;
|
||||
for principal in principal_store.get_principals().await? {
|
||||
for calendar in cal_store.get_calendars(&principal.id).await? {
|
||||
for (object_id, object) in cal_store
|
||||
.get_objects(&calendar.principal, &calendar.id)
|
||||
.await?
|
||||
{
|
||||
if let Err(err) =
|
||||
IcalObjectParser::from_slice(object.get_ics().as_bytes()).expect_one()
|
||||
{
|
||||
success = false;
|
||||
error!(
|
||||
"An error occured parsing a calendar object: principal={principal}, calendar={calendar}, object_id={object_id}: {err}",
|
||||
principal = principal.id,
|
||||
calendar = calendar.id,
|
||||
);
|
||||
println!("{}", object.get_ics());
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
if success {
|
||||
info!("Your calendar data seems to be valid in the next major version.");
|
||||
} else {
|
||||
error!(
|
||||
"Not all calendar objects will be successfully parsed in the next major version (v0.12).
|
||||
This will not cause issues in this version, but please comment under the tracking issue on GitHub:
|
||||
https://github.com/lennart-k/rustical/issues/165"
|
||||
);
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub async fn validate_address_objects_0_12(
|
||||
principal_store: &impl AuthenticationProvider,
|
||||
addr_store: &impl AddressbookStore,
|
||||
) -> Result<(), rustical_store::Error> {
|
||||
let mut success = true;
|
||||
for principal in principal_store.get_principals().await? {
|
||||
for addressbook in addr_store.get_addressbooks(&principal.id).await? {
|
||||
for (object_id, object) in addr_store
|
||||
.get_objects(&addressbook.principal, &addressbook.id)
|
||||
.await?
|
||||
{
|
||||
if let Err(err) = VcardParser::from_slice(object.get_vcf().as_bytes()).expect_one()
|
||||
{
|
||||
success = false;
|
||||
error!(
|
||||
"An error occured parsing an address object: principal={principal}, addressbook={addressbook}, object_id={object_id}: {err}",
|
||||
principal = principal.id,
|
||||
addressbook = addressbook.id,
|
||||
);
|
||||
println!("{}", object.get_vcf());
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
if success {
|
||||
info!("Your addressbook data seems to be valid in the next major version.");
|
||||
} else {
|
||||
error!(
|
||||
"Not all address objects will be successfully parsed in the next major version (v0.12).
|
||||
This will not cause issues in this version, but please comment under the tracking issue on GitHub:
|
||||
https://github.com/lennart-k/rustical/issues/165"
|
||||
);
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
Reference in New Issue
Block a user