diff options
author | Quentin Dufour <quentin@deuxfleurs.fr> | 2024-03-26 15:08:04 +0100 |
---|---|---|
committer | Quentin Dufour <quentin@deuxfleurs.fr> | 2024-03-26 15:08:04 +0100 |
commit | bc0f897803cbb9b7537010e9d4714a2a0b2a6872 (patch) | |
tree | 3371045ed249bf668340d9596fd67a71e9189ec2 | |
parent | ed47855ef1a6c9d10d48080367ff8b280530e362 (diff) | |
download | aerogramme-bc0f897803cbb9b7537010e9d4714a2a0b2a6872.tar.gz aerogramme-bc0f897803cbb9b7537010e9d4714a2a0b2a6872.zip |
Calendar Namespace
-rw-r--r-- | aero-collections/src/calendar/mod.rs | 15 | ||||
-rw-r--r-- | aero-collections/src/calendar/namespace.rs | 302 | ||||
-rw-r--r-- | aero-collections/src/mail/mailbox.rs | 2 | ||||
-rw-r--r-- | aero-collections/src/user.rs | 6 |
4 files changed, 310 insertions, 15 deletions
diff --git a/aero-collections/src/calendar/mod.rs b/aero-collections/src/calendar/mod.rs index 708e1f1..d2217b8 100644 --- a/aero-collections/src/calendar/mod.rs +++ b/aero-collections/src/calendar/mod.rs @@ -1,5 +1,20 @@ pub mod namespace; +use anyhow::Result; + +use aero_user::login::Credentials; + +use crate::unique_ident::*; + pub struct Calendar { a: u64, } + +impl Calendar { + pub(crate) async fn open( + creds: &Credentials, + id: UniqueIdent, + ) -> Result<Self> { + todo!(); + } +} diff --git a/aero-collections/src/calendar/namespace.rs b/aero-collections/src/calendar/namespace.rs index cf8a159..2fbc364 100644 --- a/aero-collections/src/calendar/namespace.rs +++ b/aero-collections/src/calendar/namespace.rs @@ -1,47 +1,327 @@ -use anyhow::Result; +use anyhow::{bail, Result}; use std::collections::{HashMap, BTreeMap}; use std::sync::{Weak, Arc}; use serde::{Deserialize, Serialize}; +use aero_bayou::timestamp::now_msec; use aero_user::storage; +use aero_user::cryptoblob::{open_deserialize, seal_serialize}; -use crate::unique_ident::UniqueIdent; +use crate::unique_ident::{gen_ident, UniqueIdent}; use crate::user::User; use super::Calendar; pub(crate) const CAL_LIST_PK: &str = "calendars"; pub(crate) const CAL_LIST_SK: &str = "list"; +pub(crate) const MAIN_CAL: &str = "Personal"; +pub(crate) const MAX_CALNAME_CHARS: usize = 32; pub(crate) struct CalendarNs(std::sync::Mutex<HashMap<UniqueIdent, Weak<Calendar>>>); + impl CalendarNs { + /// Create a new calendar namespace pub fn new() -> Self { Self(std::sync::Mutex::new(HashMap::new())) } - pub fn list(&self) { - todo!(); + /// Open a calendar by name + pub async fn open(&self, user: &Arc<User>, name: &str) -> Result<Option<Arc<Calendar>>> { + let (list, _ct) = CalendarList::load(user).await?; + + match list.get(name) { + None => Ok(None), + Some(ident) => Ok(Some(self.open_by_id(user, ident).await?)), + } + } + + /// Open a calendar by unique id + /// Check user.rs::open_mailbox_by_id to understand this function + pub async fn open_by_id(&self, user: &Arc<User>, id: UniqueIdent) -> Result<Arc<Calendar>> { + { + let cache = self.0.lock().unwrap(); + if let Some(cal) = cache.get(&id).and_then(Weak::upgrade) { + return Ok(cal); + } + } + + let cal = Arc::new(Calendar::open(&user.creds, id).await?); + + let mut cache = self.0.lock().unwrap(); + if let Some(concurrent_cal) = cache.get(&id).and_then(Weak::upgrade) { + drop(cal); // we worked for nothing but at least we didn't starve someone else + Ok(concurrent_cal) + } else { + cache.insert(id, Arc::downgrade(&cal)); + Ok(cal) + } + } + + /// List calendars + pub async fn list(&self, user: &Arc<User>) -> Result<Vec<String>> { + CalendarList::load(user).await.map(|(list, _)| list.names()) + } + + /// Delete a calendar from the index + pub async fn delete(&self, user: &Arc<User>, name: &str) -> Result<()> { + // We currently assume that main cal is a bit specific + if name == MAIN_CAL { + bail!("Cannot delete main calendar"); + } + + let (mut list, ct) = CalendarList::load(user).await?; + if list.has(name) { + //@TODO: actually delete calendar content + list.bind(name, None); + list.save(user, ct).await?; + Ok(()) + } else { + bail!("Calendar {} does not exist", name); + } + } + + /// Rename a calendar in the index + pub async fn rename(&self, user: &Arc<User>, old: &str, new: &str) -> Result<()> { + if old == MAIN_CAL { + bail!("Renaming main calendar is not supported currently"); + } + if !new.chars().all(char::is_alphanumeric) { + bail!("Unsupported characters in new calendar name, only alphanumeric characters are allowed currently"); + } + if new.len() > MAX_CALNAME_CHARS { + bail!("Calendar name can't contain more than 32 characters"); + } + + let (mut list, ct) = CalendarList::load(user).await?; + list.rename(old, new)?; + list.save(user, ct).await?; + + Ok(()) + } + + /// Create calendar + pub async fn create(&self, user: &Arc<User>, name: &str) -> Result<()> { + if name == MAIN_CAL { + bail!("Main calendar is automatically created, can't create it manually"); + } + if !name.chars().all(char::is_alphanumeric) { + bail!("Unsupported characters in new calendar name, only alphanumeric characters are allowed"); + } + if name.len() > MAX_CALNAME_CHARS { + bail!("Calendar name can't contain more than 32 characters"); + } + + let (mut list, ct) = CalendarList::load(user).await?; + match list.create(name) { + CalendarExists::Existed(_) => bail!("Calendar {} already exists", name), + CalendarExists::Created(_) => (), + } + list.save(user, ct).await?; + + Ok(()) + } + + /// Has calendar + pub async fn has(&self, user: &Arc<User>, name: &str) -> Result<bool> { + CalendarList::load(user).await.map(|(list, _)| list.has(name)) } } +// ------ +// ------ From this point, implementation is hidden from the rest of the crate +// ------ + #[derive(Serialize, Deserialize)] -pub(crate) struct CalendarList(BTreeMap<String, CalendarListEntry>); +struct CalendarList(BTreeMap<String, CalendarListEntry>); #[derive(Serialize, Deserialize, Clone, Copy, Debug)] -pub(crate) struct CalendarListEntry { +struct CalendarListEntry { id_lww: (u64, Option<UniqueIdent>), } impl CalendarList { - pub(crate) async fn load(user: &Arc<User>) -> Result<(Self, Option<storage::RowRef>)> { - todo!(); + // ---- Index persistence related functions + + /// Load from storage + async fn load(user: &Arc<User>) -> Result<(Self, Option<storage::RowRef>)> { + let row_ref = storage::RowRef::new(CAL_LIST_PK, CAL_LIST_SK); + let (mut list, row) = match user + .storage + .row_fetch(&storage::Selector::Single(&row_ref)) + .await + { + Err(storage::StorageError::NotFound) => (Self::new(), None), + Err(e) => return Err(e.into()), + Ok(rv) => { + let mut list = Self::new(); + let (row_ref, row_vals) = match rv.into_iter().next() { + Some(row_val) => (row_val.row_ref, row_val.value), + None => (row_ref, vec![]), + }; + + for v in row_vals { + if let storage::Alternative::Value(vbytes) = v { + let list2 = open_deserialize::<CalendarList>(&vbytes, &user.creds.keys.master)?; + list.merge(list2); + } + } + (list, Some(row_ref)) + } + }; + + // Create default calendars (currently only one calendar is created) + let is_default_cal_missing = [MAIN_CAL] + .iter() + .map(|calname| list.create(calname)) + .fold(false, |acc, r| { + acc || matches!(r, CalendarExists::Created(..)) + }); + + // Save the index if we created a new calendar + if is_default_cal_missing { + list.save(user, row.clone()).await?; + } + + Ok((list, row)) } - pub(crate) async fn save(user: &Arc<User>, ct: Option<storage::RowRef>) -> Result<()> { - todo!(); + /// Save an updated index + async fn save(&self, user: &Arc<User>, ct: Option<storage::RowRef>) -> Result<()> { + let list_blob = seal_serialize(self, &user.creds.keys.master)?; + let rref = ct.unwrap_or(storage::RowRef::new(CAL_LIST_PK, CAL_LIST_SK)); + let row_val = storage::RowVal::new(rref, list_blob); + user.storage.row_insert(vec![row_val]).await?; + Ok(()) } - pub(crate) fn new() -> Self { + // ----- Index manipulation functions + + /// Ensure that a given calendar exists + /// (Don't forget to save if it returns CalendarExists::Created) + fn create(&mut self, name: &str) -> CalendarExists { + if let Some(CalendarListEntry { + id_lww: (_, Some(id)) + }) = self.0.get(name) + { + return CalendarExists::Existed(*id); + } + + let id = gen_ident(); + self.bind(name, Some(id)).unwrap(); + CalendarExists::Created(id) + } + + /// Get a list of all calendar names + fn names(&self) -> Vec<String> { + self.0 + .iter() + .filter(|(_, v)| v.id_lww.1.is_some()) + .map(|(k, _)| k.to_string()) + .collect() + } + + /// For a given calendar name, get its Unique Identifier + fn get(&self, name: &str) -> Option<UniqueIdent> { + self.0.get(name).map(|CalendarListEntry { + id_lww: (_, ident), + }| *ident).flatten() + } + + /// Check if a given calendar name exists + fn has(&self, name: &str) -> bool { + self.get(name).is_some() + } + + /// Rename a calendar + fn rename(&mut self, old: &str, new: &str) -> Result<()> { + if self.has(new) { + bail!("Calendar {} already exists", new); + } + let ident = match self.get(old) { + None => bail!("Calendar {} does not exist", old), + Some(ident) => ident, + }; + + self.bind(old, None); + self.bind(new, Some(ident)); + + Ok(()) + } + + // ----- Internal logic + + /// New is not publicly exposed, use `load` instead + fn new() -> Self { Self(BTreeMap::new()) } + + /// Low level index updating logic (used to add/rename/delete) an entry + fn bind(&mut self, name: &str, id: Option<UniqueIdent>) -> Option<()> { + let (ts, id) = match self.0.get_mut(name) { + None => { + if id.is_none() { + // User wants to delete entry with given name (passed id is None) + // Entry does not exist (get_mut is None) + // Nothing to do + return None; + } else { + // User wants entry with given name to be present (id is Some) + // Entry does not exist + // Initialize entry + (now_msec(), id) + } + } + Some(CalendarListEntry { + id_lww, + }) => { + if id_lww.1 == id { + // Entry is already equals to the requested id (Option<UniqueIdent) + // Nothing to do + return None; + } else { + // Entry does not equal to what we know internally + // We update the Last Write Win CRDT here with the new id value + ( + std::cmp::max(id_lww.0 + 1, now_msec()), + id, + ) + } + } + }; + + // If we did not return here, that's because we have to update + // something in our internal index. + self.0.insert( + name.into(), + CalendarListEntry { id_lww: (ts, id) }, + ); + Some(()) + } + + // Merge 2 calendar lists by applying a LWW logic on each element + fn merge(&mut self, list2: Self) { + for (k, v) in list2.0.into_iter() { + if let Some(e) = self.0.get_mut(&k) { + e.merge(&v); + } else { + self.0.insert(k, v); + } + } + } +} + +impl CalendarListEntry { + fn merge(&mut self, other: &Self) { + // Simple CRDT merge rule + if other.id_lww.0 > self.id_lww.0 + || (other.id_lww.0 == self.id_lww.0 && other.id_lww.1 > self.id_lww.1) + { + self.id_lww = other.id_lww; + } + } +} + +pub(crate) enum CalendarExists { + Created(UniqueIdent), + Existed(UniqueIdent), } diff --git a/aero-collections/src/mail/mailbox.rs b/aero-collections/src/mail/mailbox.rs index 25aacf5..f797be6 100644 --- a/aero-collections/src/mail/mailbox.rs +++ b/aero-collections/src/mail/mailbox.rs @@ -8,8 +8,8 @@ use aero_user::storage::{self, BlobRef, BlobVal, RowRef, RowVal, Selector, Store use aero_bayou::Bayou; use aero_bayou::timestamp::now_msec; -use crate::mail::uidindex::*; use crate::unique_ident::*; +use crate::mail::uidindex::*; use crate::mail::IMF; pub struct Mailbox { diff --git a/aero-collections/src/user.rs b/aero-collections/src/user.rs index 0c6b931..9ed342f 100644 --- a/aero-collections/src/user.rs +++ b/aero-collections/src/user.rs @@ -14,7 +14,7 @@ use crate::mail::mailbox::Mailbox; use crate::mail::uidindex::ImapUidvalidity; use crate::unique_ident::UniqueIdent; use crate::mail::namespace::{MAILBOX_HIERARCHY_DELIMITER, INBOX, DRAFTS, ARCHIVE, SENT, TRASH, MAILBOX_LIST_PK, MAILBOX_LIST_SK,MailboxList,CreatedMailbox}; -use crate::calendar::Calendar; +use crate::calendar::namespace::CalendarNs; //@FIXME User should be totally rewriten // to extract the local mailbox list @@ -29,7 +29,7 @@ pub struct User { pub creds: Credentials, pub storage: storage::Store, pub mailboxes: std::sync::Mutex<HashMap<UniqueIdent, Weak<Mailbox>>>, - pub calendars: std::sync::Mutex<HashMap<UniqueIdent, Weak<Calendar>>>, + pub calendars: CalendarNs, // Handle on worker processing received email // (moving emails from the mailqueue to the user's INBOX) @@ -186,7 +186,7 @@ impl User { storage, tx_inbox_id, mailboxes: std::sync::Mutex::new(HashMap::new()), - calendars: std::sync::Mutex::new(HashMap::new()), + calendars: CalendarNs::new(), }); // Ensure INBOX exists (done inside load_mailbox_list) |