diff --git a/crates/frontend/public/assets/style.css b/crates/frontend/public/assets/style.css
index 2cd0e3d..95ee5dc 100644
--- a/crates/frontend/public/assets/style.css
+++ b/crates/frontend/public/assets/style.css
@@ -202,14 +202,16 @@ ul.collection-list {
background: color-mix(in srgb, var(--background-color), var(--dilute-color) 5%);
display: grid;
min-height: 80px;
+ height: fit-content;
grid-template-areas:
". . color-chip"
"title comps color-chip"
"description description color-chip"
"subscription-url subscription-url color-chip"
+ "metadata metadata color-chip"
"actions actions color-chip"
". . color-chip";
- grid-template-rows: 12px auto auto auto auto 12px;
+ grid-template-rows: 12px auto auto auto auto auto 12px;
grid-template-columns: min-content auto 80px;
row-gap: 4px;
color: inherit;
@@ -251,6 +253,11 @@ ul.collection-list {
white-space: nowrap;
}
+ .metadata {
+ grid-area: metadata;
+ white-space: nowrap;
+ }
+
.subscription-url {
grid-area: subscription-url;
white-space: nowrap;
diff --git a/crates/frontend/public/templates/components/sections/addressbooks_section.html b/crates/frontend/public/templates/components/sections/addressbooks_section.html
index 94245c7..1371ce8 100644
--- a/crates/frontend/public/templates/components/sections/addressbooks_section.html
+++ b/crates/frontend/public/templates/components/sections/addressbooks_section.html
@@ -1,6 +1,6 @@
{{user.id }}'s Addressbooks
- {% for addressbook in addressbooks %}
+ {% for (meta, addressbook) in addressbooks %}
-
@@ -18,6 +18,9 @@
+
+ {{ meta.len }} objects, {{ meta.deleted_len }} deleted objects
+
{% else %}
@@ -27,7 +30,7 @@
{%if !deleted_addressbooks.is_empty() %}
Deleted Addressbooks
- {% for addressbook in deleted_addressbooks %}
+ {% for (meta, addressbook) in deleted_addressbooks %}
-
@@ -44,6 +47,9 @@
+
+ {{ meta.len }} objects, {{ meta.deleted_len }} deleted objects
+
{% endfor %}
diff --git a/crates/frontend/public/templates/components/sections/calendars_section.html b/crates/frontend/public/templates/components/sections/calendars_section.html
index 50a1c04..93096ae 100644
--- a/crates/frontend/public/templates/components/sections/calendars_section.html
+++ b/crates/frontend/public/templates/components/sections/calendars_section.html
@@ -1,6 +1,6 @@
{{ user.id }}'s Calendars
- {% for calendar in calendars %}
+ {% for (meta, calendar) in calendars %}
{% let color = calendar.color.to_owned().unwrap_or("transparent".to_owned()) %}
-
@@ -27,6 +27,9 @@
{% endif %}
+
+ {{ meta.len }} objects, {{ meta.deleted_len }} deleted objects
+
@@ -37,7 +40,7 @@
{%if !deleted_calendars.is_empty() %}
Deleted Calendars
- {% for calendar in deleted_calendars %}
+ {% for (meta, calendar) in deleted_calendars %}
{% let color = calendar.color.to_owned().unwrap_or("transparent".to_owned()) %}
-
@@ -60,6 +63,9 @@
+
+ {{ meta.len }} objects, {{ meta.deleted_len }} deleted objects
+
diff --git a/crates/frontend/src/routes/addressbooks.rs b/crates/frontend/src/routes/addressbooks.rs
index 2e41a6f..f0707fc 100644
--- a/crates/frontend/src/routes/addressbooks.rs
+++ b/crates/frontend/src/routes/addressbooks.rs
@@ -4,7 +4,7 @@ use askama::Template;
use askama_web::WebTemplate;
use axum::{Extension, extract::Path, response::IntoResponse};
use http::StatusCode;
-use rustical_store::{Addressbook, AddressbookStore, auth::Principal};
+use rustical_store::{Addressbook, AddressbookStore, CollectionMetadata, auth::Principal};
use crate::pages::user::{Section, UserPage};
@@ -18,8 +18,8 @@ impl Section for AddressbooksSection {
#[template(path = "components/sections/addressbooks_section.html")]
pub struct AddressbooksSection {
pub user: Principal,
- pub addressbooks: Vec,
- pub deleted_addressbooks: Vec,
+ pub addressbooks: Vec<(CollectionMetadata, Addressbook)>,
+ pub deleted_addressbooks: Vec<(CollectionMetadata, Addressbook)>,
}
pub async fn route_addressbooks(
@@ -41,11 +41,33 @@ pub async fn route_addressbooks(
deleted_addressbooks.extend(addr_store.get_deleted_addressbooks(group).await.unwrap());
}
+ let mut addressbook_infos = vec![];
+ for addressbook in addressbooks {
+ addressbook_infos.push((
+ addr_store
+ .addressbook_metadata(&addressbook.principal, &addressbook.id)
+ .await
+ .unwrap(),
+ addressbook,
+ ));
+ }
+
+ let mut deleted_addressbook_infos = vec![];
+ for addressbook in deleted_addressbooks {
+ deleted_addressbook_infos.push((
+ addr_store
+ .addressbook_metadata(&addressbook.principal, &addressbook.id)
+ .await
+ .unwrap(),
+ addressbook,
+ ));
+ }
+
UserPage {
section: AddressbooksSection {
user: user.clone(),
- addressbooks,
- deleted_addressbooks,
+ addressbooks: addressbook_infos,
+ deleted_addressbooks: deleted_addressbook_infos,
},
user,
}
diff --git a/crates/frontend/src/routes/calendars.rs b/crates/frontend/src/routes/calendars.rs
index 7cebc29..b440858 100644
--- a/crates/frontend/src/routes/calendars.rs
+++ b/crates/frontend/src/routes/calendars.rs
@@ -5,7 +5,7 @@ use askama::Template;
use askama_web::WebTemplate;
use axum::{Extension, extract::Path, response::IntoResponse};
use http::StatusCode;
-use rustical_store::{Calendar, CalendarStore, auth::Principal};
+use rustical_store::{Calendar, CalendarStore, CollectionMetadata, auth::Principal};
impl Section for CalendarsSection {
fn name() -> &'static str {
@@ -17,8 +17,8 @@ impl Section for CalendarsSection {
#[template(path = "components/sections/calendars_section.html")]
pub struct CalendarsSection {
pub user: Principal,
- pub calendars: Vec,
- pub deleted_calendars: Vec,
+ pub calendars: Vec<(CollectionMetadata, Calendar)>,
+ pub deleted_calendars: Vec<(CollectionMetadata, Calendar)>,
}
pub async fn route_calendars(
@@ -35,16 +35,38 @@ pub async fn route_calendars(
calendars.extend(cal_store.get_calendars(group).await.unwrap());
}
+ let mut calendar_infos = vec![];
+ for calendar in calendars {
+ calendar_infos.push((
+ cal_store
+ .calendar_metadata(&calendar.principal, &calendar.id)
+ .await
+ .unwrap(),
+ calendar,
+ ));
+ }
+
let mut deleted_calendars = vec![];
for group in user.memberships() {
deleted_calendars.extend(cal_store.get_deleted_calendars(group).await.unwrap());
}
+ let mut deleted_calendar_infos = vec![];
+ for calendar in deleted_calendars {
+ deleted_calendar_infos.push((
+ cal_store
+ .calendar_metadata(&calendar.principal, &calendar.id)
+ .await
+ .unwrap(),
+ calendar,
+ ));
+ }
+
UserPage {
section: CalendarsSection {
user: user.clone(),
- calendars,
- deleted_calendars,
+ calendars: calendar_infos,
+ deleted_calendars: deleted_calendar_infos,
},
user,
}
diff --git a/crates/store/src/addressbook_store.rs b/crates/store/src/addressbook_store.rs
index 5453703..1fff3ee 100644
--- a/crates/store/src/addressbook_store.rs
+++ b/crates/store/src/addressbook_store.rs
@@ -1,4 +1,4 @@
-use crate::{Error, addressbook::Addressbook};
+use crate::{CollectionMetadata, Error, addressbook::Addressbook};
use async_trait::async_trait;
use rustical_ical::AddressObject;
@@ -35,6 +35,12 @@ pub trait AddressbookStore: Send + Sync + 'static {
synctoken: i64,
) -> Result<(Vec, Vec, i64), Error>;
+ async fn addressbook_metadata(
+ &self,
+ principal: &str,
+ addressbook_id: &str,
+ ) -> Result;
+
async fn get_objects(
&self,
principal: &str,
diff --git a/crates/store/src/calendar_store.rs b/crates/store/src/calendar_store.rs
index ffed079..c01c217 100644
--- a/crates/store/src/calendar_store.rs
+++ b/crates/store/src/calendar_store.rs
@@ -1,4 +1,4 @@
-use crate::{Calendar, error::Error};
+use crate::{Calendar, CollectionMetadata, error::Error};
use async_trait::async_trait;
use chrono::NaiveDate;
use rustical_ical::CalendarObject;
@@ -53,6 +53,12 @@ pub trait CalendarStore: Send + Sync + 'static {
self.get_objects(principal, cal_id).await
}
+ async fn calendar_metadata(
+ &self,
+ principal: &str,
+ cal_id: &str,
+ ) -> Result;
+
async fn get_objects(
&self,
principal: &str,
diff --git a/crates/store/src/combined_calendar_store.rs b/crates/store/src/combined_calendar_store.rs
index b86c527..841bfb8 100644
--- a/crates/store/src/combined_calendar_store.rs
+++ b/crates/store/src/combined_calendar_store.rs
@@ -135,6 +135,20 @@ impl CalendarStore for CombinedCalendarSto
}
}
+ #[inline]
+ async fn calendar_metadata(
+ &self,
+ principal: &str,
+ cal_id: &str,
+ ) -> Result {
+ if cal_id.starts_with(BIRTHDAYS_PREFIX) {
+ self.birthday_store
+ .calendar_metadata(principal, cal_id)
+ .await
+ } else {
+ self.cal_store.calendar_metadata(principal, cal_id).await
+ }
+ }
#[inline]
async fn get_objects(
&self,
diff --git a/crates/store/src/contact_birthday_store.rs b/crates/store/src/contact_birthday_store.rs
index ef24e57..cbfde40 100644
--- a/crates/store/src/contact_birthday_store.rs
+++ b/crates/store/src/contact_birthday_store.rs
@@ -16,7 +16,7 @@ fn birthday_calendar(addressbook: Addressbook) -> Calendar {
id: format!("{}{}", BIRTHDAYS_PREFIX, addressbook.id),
displayname: addressbook
.displayname
- .map(|name| format!("{} birthdays", name)),
+ .map(|name| format!("{name} birthdays")),
order: 0,
description: None,
color: None,
@@ -104,6 +104,17 @@ impl CalendarStore for ContactBirthdayStore {
Ok((objects, deleted_objects, new_synctoken))
}
+ async fn calendar_metadata(
+ &self,
+ principal: &str,
+ cal_id: &str,
+ ) -> Result {
+ let cal_id = cal_id
+ .strip_prefix(BIRTHDAYS_PREFIX)
+ .ok_or(Error::NotFound)?;
+ self.0.addressbook_metadata(principal, cal_id).await
+ }
+
async fn get_objects(
&self,
principal: &str,
diff --git a/crates/store/src/lib.rs b/crates/store/src/lib.rs
index 36e9a50..d761193 100644
--- a/crates/store/src/lib.rs
+++ b/crates/store/src/lib.rs
@@ -37,3 +37,11 @@ pub struct CollectionOperation {
pub topic: String,
pub data: CollectionOperationInfo,
}
+
+#[derive(Default, Debug, Clone)]
+pub struct CollectionMetadata {
+ pub len: usize,
+ pub deleted_len: usize,
+ pub size: u64,
+ pub deleted_size: u64,
+}
diff --git a/crates/store/src/synctoken.rs b/crates/store/src/synctoken.rs
index 5d1b46e..67425bf 100644
--- a/crates/store/src/synctoken.rs
+++ b/crates/store/src/synctoken.rs
@@ -1,7 +1,7 @@
const SYNC_NAMESPACE: &str = "github.com/lennart-k/rustical/ns/";
pub fn format_synctoken(synctoken: i64) -> String {
- format!("{}{}", SYNC_NAMESPACE, synctoken)
+ format!("{SYNC_NAMESPACE}{synctoken}")
}
pub fn parse_synctoken(synctoken: &str) -> Option {
diff --git a/crates/store_sqlite/src/addressbook_store.rs b/crates/store_sqlite/src/addressbook_store.rs
index 1566be9..eec49e6 100644
--- a/crates/store_sqlite/src/addressbook_store.rs
+++ b/crates/store_sqlite/src/addressbook_store.rs
@@ -3,8 +3,8 @@ use async_trait::async_trait;
use derive_more::derive::Constructor;
use rustical_ical::AddressObject;
use rustical_store::{
- Addressbook, AddressbookStore, CollectionOperation, CollectionOperationInfo, Error,
- synctoken::format_synctoken,
+ Addressbook, AddressbookStore, CollectionMetadata, CollectionOperation,
+ CollectionOperationInfo, Error, synctoken::format_synctoken,
};
use sqlx::{Acquire, Executor, Sqlite, SqlitePool, Transaction};
use tokio::sync::mpsc::Sender;
@@ -223,6 +223,28 @@ impl SqliteAddressbookStore {
Ok((objects, deleted_objects, new_synctoken))
}
+ async fn _list_objects<'e, E: Executor<'e, Database = Sqlite>>(
+ executor: E,
+ principal: &str,
+ addressbook_id: &str,
+ ) -> Result, rustical_store::Error> {
+ struct ObjectEntry {
+ length: u64,
+ deleted: bool,
+ }
+ Ok(sqlx::query_as!(
+ ObjectEntry,
+ "SELECT length(vcf) AS 'length!: u64', deleted_at AS 'deleted!: bool' FROM addressobjects WHERE principal = ? AND addressbook_id = ?",
+ principal,
+ addressbook_id
+ )
+ .fetch_all(executor)
+ .await.map_err(crate::Error::from)?
+ .into_iter()
+ .map(|row| (row.length, row.deleted))
+ .collect())
+ }
+
async fn _get_objects<'e, E: Executor<'e, Database = Sqlite>>(
executor: E,
principal: &str,
@@ -442,6 +464,29 @@ impl AddressbookStore for SqliteAddressbookStore {
Self::_sync_changes(&self.db, principal, addressbook_id, synctoken).await
}
+ #[instrument]
+ async fn addressbook_metadata(
+ &self,
+ principal: &str,
+ addressbook_id: &str,
+ ) -> Result {
+ let mut sizes = vec![];
+ let mut deleted_sizes = vec![];
+ for (size, deleted) in Self::_list_objects(&self.db, principal, addressbook_id).await? {
+ if deleted {
+ deleted_sizes.push(size)
+ } else {
+ sizes.push(size)
+ }
+ }
+ Ok(CollectionMetadata {
+ len: sizes.len(),
+ deleted_len: deleted_sizes.len(),
+ size: sizes.iter().sum(),
+ deleted_size: deleted_sizes.iter().sum(),
+ })
+ }
+
#[instrument]
async fn get_objects(
&self,
diff --git a/crates/store_sqlite/src/calendar_store.rs b/crates/store_sqlite/src/calendar_store.rs
index 218919f..5b02786 100644
--- a/crates/store_sqlite/src/calendar_store.rs
+++ b/crates/store_sqlite/src/calendar_store.rs
@@ -5,7 +5,7 @@ use derive_more::derive::Constructor;
use rustical_ical::{CalDateTime, CalendarObject, CalendarObjectType};
use rustical_store::calendar_store::CalendarQuery;
use rustical_store::synctoken::format_synctoken;
-use rustical_store::{Calendar, CalendarStore, Error};
+use rustical_store::{Calendar, CalendarStore, CollectionMetadata, Error};
use rustical_store::{CollectionOperation, CollectionOperationInfo};
use sqlx::types::chrono::NaiveDateTime;
use sqlx::{Acquire, Executor, Sqlite, SqlitePool, Transaction};
@@ -242,6 +242,28 @@ impl SqliteCalendarStore {
Ok(())
}
+ async fn _list_objects<'e, E: Executor<'e, Database = Sqlite>>(
+ executor: E,
+ principal: &str,
+ cal_id: &str,
+ ) -> Result, rustical_store::Error> {
+ struct ObjectEntry {
+ length: u64,
+ deleted: bool,
+ }
+ Ok(sqlx::query_as!(
+ ObjectEntry,
+ "SELECT length(ics) AS 'length!: u64', deleted_at AS 'deleted!: bool' FROM calendarobjects WHERE principal = ? AND cal_id = ?",
+ principal,
+ cal_id
+ )
+ .fetch_all(executor)
+ .await.map_err(crate::Error::from)?
+ .into_iter()
+ .map(|row| (row.length, row.deleted))
+ .collect())
+ }
+
async fn _get_objects<'e, E: Executor<'e, Database = Sqlite>>(
executor: E,
principal: &str,
@@ -552,6 +574,28 @@ impl CalendarStore for SqliteCalendarStore {
Self::_calendar_query(&self.db, principal, cal_id, query).await
}
+ async fn calendar_metadata(
+ &self,
+ principal: &str,
+ cal_id: &str,
+ ) -> Result {
+ let mut sizes = vec![];
+ let mut deleted_sizes = vec![];
+ for (size, deleted) in Self::_list_objects(&self.db, principal, cal_id).await? {
+ if deleted {
+ deleted_sizes.push(size)
+ } else {
+ sizes.push(size)
+ }
+ }
+ Ok(CollectionMetadata {
+ len: sizes.len(),
+ deleted_len: deleted_sizes.len(),
+ size: sizes.iter().sum(),
+ deleted_size: deleted_sizes.iter().sum(),
+ })
+ }
+
#[instrument]
async fn get_objects(
&self,