use super::{StoreError, Undo}; use chrono::{DateTime, Utc}; use sled::{Db, Transactional, Tree}; use std::{fmt, io::Cursor}; use uuid::Uuid; #[derive(Clone, Copy, Debug, serde::Deserialize, serde::Serialize)] pub enum Visibility { Public, Unlisted, Followers, } #[derive(Clone, Debug)] pub struct Submission { id: Uuid, profile_id: Uuid, title: String, title_source: Option, description: Option, description_source: Option, files: Vec, published: Option>, updated: Option>, visibility: Visibility, local_only: bool, logged_in_only: bool, sensitive: bool, } #[derive(Debug)] pub struct SubmissionChanges { id: Uuid, title: Option, title_source: Option, description: Option, description_source: Option, visibility: Option, published: Option>, updated: Option>, local_only: Option, logged_in_only: Option, sensitive: Option, original_files: Vec, files: Vec, } #[derive(Clone, Debug)] pub struct Store { submission_tree: Tree, profile_tree: Tree, profile_drafted_tree: Tree, published_tree: Tree, profile_published_tree: Tree, count_tree: Tree, } #[derive(Debug, serde::Deserialize, serde::Serialize)] struct StoredSubmission { id: Uuid, profile_id: Uuid, title: String, #[serde(skip_serializing_if = "Option::is_none")] title_source: Option, #[serde(skip_serializing_if = "Option::is_none")] description: Option, #[serde(skip_serializing_if = "Option::is_none")] description_source: Option, files: Vec, #[serde(skip_serializing_if = "Option::is_none")] published: Option>, #[serde(skip_serializing_if = "Option::is_none")] updated: Option>, visibility: Visibility, #[serde(default)] local_only: bool, #[serde(default)] logged_in_only: bool, #[serde(default)] sensitive: bool, drafted_at: DateTime, } impl Submission { pub fn update(&self) -> SubmissionChanges { SubmissionChanges { id: self.id, title: None, title_source: None, description: None, description_source: None, visibility: None, published: self.published, updated: None, local_only: None, logged_in_only: None, sensitive: None, original_files: self.files.clone(), files: self.files.clone(), } } pub fn id(&self) -> Uuid { self.id } pub fn profile_id(&self) -> Uuid { self.profile_id } pub fn title(&self) -> &str { &self.title } pub fn title_source(&self) -> Option<&str> { self.title_source.as_deref() } pub fn description(&self) -> Option<&str> { self.description.as_ref().map(|d| d.as_str()) } pub fn description_source(&self) -> Option<&str> { self.description_source.as_deref() } pub fn files(&self) -> &[Uuid] { &self.files } pub fn published(&self) -> Option> { self.published } pub fn updated(&self) -> Option> { self.updated } pub fn visibility(&self) -> Visibility { self.visibility } pub fn is_public(&self) -> bool { matches!(self.visibility, Visibility::Public) } pub fn is_unlisted(&self) -> bool { matches!(self.visibility, Visibility::Unlisted) } pub fn is_followers_only(&self) -> bool { matches!(self.visibility, Visibility::Followers) } pub fn is_local_only(&self) -> bool { self.local_only } pub fn is_logged_in_only(&self) -> bool { self.logged_in_only } pub fn is_sensitive(&self) -> bool { self.sensitive } } impl SubmissionChanges { pub(crate) fn title(&mut self, title: &str) -> &mut Self { self.title = Some(title.to_owned()); self } pub(crate) fn title_source(&mut self, title_source: &str) -> &mut Self { self.title_source = Some(title_source.to_owned()); self } pub(crate) fn description(&mut self, description: &str) -> &mut Self { self.description = Some(description.to_owned()); self } pub(crate) fn description_source(&mut self, description_source: &str) -> &mut Self { self.description_source = Some(description_source.to_owned()); self } pub(crate) fn visibility(&mut self, visibility: Visibility) -> &mut Self { if self.visibility.is_none() { self.visibility = Some(visibility); } self } pub(crate) fn published(&mut self, time: Option>) -> &mut Self { if self.published.is_none() { self.published = time.or_else(|| Some(Utc::now())); } self } pub(crate) fn updated(&mut self, time: DateTime) -> &mut Self { if self.published.is_some() { self.updated = Some(time); } self } pub(crate) fn local_only(&mut self, local_only: bool) -> &mut Self { if self.published.is_none() { self.local_only = Some(local_only); } self } pub(crate) fn logged_in_only(&mut self, logged_in_only: bool) -> &mut Self { if self.published.is_none() { self.logged_in_only = Some(logged_in_only); } self } pub(crate) fn sensitive(&mut self, sensitive: bool) -> &mut Self { self.sensitive = Some(sensitive); self } pub(crate) fn add_file(&mut self, file_id: Uuid) -> &mut Self { self.files.push(file_id); self } pub(crate) fn delete_file(&mut self, file_id: Uuid) -> &mut Self { self.files.retain(|id| *id != file_id); self } pub(crate) fn any_changes(&self) -> bool { self.title.is_some() || self.description.is_some() || self.published.is_some() || self.updated.is_some() || self.visibility.is_some() || self.local_only.is_some() || self.logged_in_only.is_some() || self.sensitive.is_some() || self.original_files != self.files } } impl Store { pub fn build(db: &Db) -> Result { Ok(Store { submission_tree: db.open_tree("profiles/submissions")?, profile_tree: db.open_tree("profiles/submissions/profile")?, profile_drafted_tree: db.open_tree("/profiles/submissions/profile/drafted")?, published_tree: db.open_tree("/profiles/submission/published")?, profile_published_tree: db.open_tree("/profiles/submission/profile/published")?, count_tree: db.open_tree("/profiles/submissions/count")?, }) } pub fn create( &self, profile_id: Uuid, title: &str, visibility: Visibility, ) -> Result { let mut id; let mut stored_submission; let now = Utc::now().into(); let mut stored_submission_vec = vec![]; while { stored_submission_vec.clear(); let writer = Cursor::new(&mut stored_submission_vec); id = Uuid::new_v4(); stored_submission = StoredSubmission { id, profile_id, title: title.to_owned(), title_source: None, description: None, description_source: None, files: vec![], published: None, updated: None, visibility, local_only: false, logged_in_only: false, drafted_at: now, sensitive: false, }; serde_json::to_writer(writer, &stored_submission)?; self.submission_tree .compare_and_swap( id_submission_key(id).as_bytes(), None as Option<&[u8]>, Some(stored_submission_vec.as_slice()), )? .is_err() } {} let res = [ &self.profile_tree, &self.profile_drafted_tree, &self.count_tree, ] .transaction(move |trees| { let profile_tree = &trees[0]; let profile_drafted_tree = &trees[1]; let count_tree = &trees[2]; profile_tree.insert( profile_id_key(profile_id, id).as_bytes(), id.to_string().as_bytes(), )?; profile_drafted_tree.insert( profile_id_drafted_submission_key(profile_id, now, id).as_bytes(), id.to_string().as_bytes(), )?; super::count( count_tree, &profile_id_submission_count_key(profile_id), |c| c.saturating_add(1), )?; Ok(()) }); if let Err(e) = res { self.submission_tree .remove(id_submission_key(id).as_bytes())?; return Err(e.into()); } Ok(stored_submission.into()) } pub fn count(&self, profile_id: Uuid) -> Result { match self .count_tree .get(profile_id_submission_count_key(profile_id))? { Some(ivec) => Ok(String::from_utf8_lossy(&ivec) .parse::() .expect("Count is valid")), None => Ok(0), } } pub fn update(&self, changes: &SubmissionChanges) -> Result { let stored_submission_ivec = match self.submission_tree.get(id_submission_key(changes.id))? { Some(ivec) => ivec, None => return Err(StoreError::Missing), }; let mut stored_submission: StoredSubmission = serde_json::from_slice(&stored_submission_ivec)?; if let Some(updated) = changes.updated { if let Some(previously_updated) = stored_submission .updated .or_else(|| stored_submission.published) { if updated < previously_updated { return Err(StoreError::Outdated); } } } let already_published = stored_submission.published.is_some(); if let Some(title) = &changes.title { stored_submission.title = title.to_owned(); } if let Some(title_source) = &changes.title_source { stored_submission.title_source = Some(title_source.clone()); } if let Some(description) = &changes.description { stored_submission.description = Some(description.to_owned()); } if let Some(description_source) = &changes.description_source { stored_submission.description_source = Some(description_source.clone()); } if let Some(visibility) = changes.visibility { stored_submission.visibility = visibility; } if let Some(local_only) = changes.local_only { stored_submission.local_only = local_only; } if let Some(logged_in_only) = changes.logged_in_only { stored_submission.logged_in_only = logged_in_only; } if let Some(sensitive) = changes.sensitive { stored_submission.sensitive = sensitive; } if stored_submission.published.is_some() { if let Some(updated) = changes.updated { stored_submission.updated = Some(updated); } } stored_submission.published = changes.published; stored_submission.files = changes.files.clone(); let stored_submission_vec = serde_json::to_vec(&stored_submission)?; if self .submission_tree .compare_and_swap( id_submission_key(changes.id), Some(&stored_submission_ivec), Some(stored_submission_vec.as_slice()), )? .is_err() { return Err(StoreError::DoubleStore); } if !already_published { if let Some(published) = changes.published { self.profile_drafted_tree .remove(profile_id_drafted_submission_key( stored_submission.profile_id, stored_submission.drafted_at, stored_submission.id, ))?; self.published_tree.insert( published_submission_key(published, stored_submission.id), stored_submission.id.to_string().as_bytes(), )?; self.profile_published_tree.insert( published_profile_submission_key( stored_submission.profile_id, published, stored_submission.id, ), stored_submission.id.to_string().as_bytes(), )?; } } Ok(stored_submission.into()) } fn extract(&self, submission_id: Uuid, f: F) -> Option where F: FnOnce(StoredSubmission) -> Option, { let ivec = self .submission_tree .get(id_submission_key(submission_id)) .ok()??; let stored_submission: StoredSubmission = serde_json::from_slice(&ivec).ok()?; (f)(stored_submission) } pub fn published(&self) -> impl DoubleEndedIterator { self.published_tree .scan_prefix(published_prefix()) .values() .filter_map(|res| res.ok()) .filter_map(uuid_from_ivec) .rev() } fn published_date_range( &self, range: impl std::ops::RangeBounds, ) -> impl DoubleEndedIterator where K: AsRef<[u8]>, { self.published_tree .range(range) .values() .filter_map(|res| res.ok()) .filter_map(uuid_from_ivec) } pub fn published_newer_than( &self, submission_id: Uuid, ) -> impl DoubleEndedIterator { let this = self.clone(); self.extract(submission_id, |stored_submission| { stored_submission.published }) .into_iter() .flat_map(move |published| { let range_entry = published_submission_range_start(published); let range_entry = range_entry.as_bytes().to_vec(); this.published_date_range(range_entry..) }) } pub fn published_older_than( &self, submission_id: Uuid, ) -> impl DoubleEndedIterator { let this = self.clone(); self.extract(submission_id, |stored_submission| { stored_submission.published }) .into_iter() .flat_map(move |published| { let range_entry = published_submission_range_start(published); let range_entry = range_entry.as_bytes().to_vec(); this.published_date_range(..range_entry) }) .rev() } pub fn drafted_for_profile(&self, profile_id: Uuid) -> impl DoubleEndedIterator { self.profile_drafted_tree .scan_prefix(profile_id_drafted_prefix(profile_id)) .values() .filter_map(|res| res.ok()) .filter_map(uuid_from_ivec) .rev() } fn drafted_date_range_for_profile( &self, range: impl std::ops::RangeBounds, ) -> impl DoubleEndedIterator where K: AsRef<[u8]>, { self.profile_drafted_tree .range(range) .values() .filter_map(|res| res.ok()) .filter_map(uuid_from_ivec) } pub fn drafted_newer_than_for_profile( &self, id: Uuid, ) -> impl DoubleEndedIterator { let this = self.clone(); self.extract(id, |s| Some((s.profile_id, s.drafted_at))) .into_iter() .flat_map(move |(profile_id, drafted)| { let range_end = profile_id_drafted_submission_range_end(profile_id); let range_entry = profile_id_drafted_submission_range_entry(profile_id, drafted); this.drafted_date_range_for_profile(range_entry..range_end) }) } pub fn drafted_older_than_for_profile( &self, id: Uuid, ) -> impl DoubleEndedIterator { let this = self.clone(); self.extract(id, |s| Some((s.profile_id, s.drafted_at))) .into_iter() .flat_map(move |(profile_id, drafted)| { let range_start = profile_id_drafted_submission_range_beginning(profile_id); let range_entry = profile_id_drafted_submission_range_entry(profile_id, drafted); this.drafted_date_range_for_profile(range_start..range_entry) }) .rev() } pub fn published_for_profile(&self, profile_id: Uuid) -> impl DoubleEndedIterator { self.profile_published_tree .scan_prefix(profile_id_published_prefix(profile_id)) .values() .filter_map(|res| res.ok()) .filter_map(uuid_from_ivec) .rev() } fn published_date_range_for_profile( &self, range: impl std::ops::RangeBounds, ) -> impl DoubleEndedIterator where K: AsRef<[u8]>, { self.profile_published_tree .range(range) .values() .filter_map(|res| res.ok()) .filter_map(uuid_from_ivec) } pub fn published_newer_than_for_profile( &self, id: Uuid, ) -> impl DoubleEndedIterator { let this = self.clone(); self.extract(id, |s| s.published.map(|p| (s.profile_id, p))) .into_iter() .flat_map(move |(profile_id, published)| { let range_end = profile_id_publshed_submission_range_end(profile_id); let range_entry = profile_id_publshed_submission_range_entry(profile_id, published); this.published_date_range_for_profile(range_entry..range_end) }) } pub fn published_older_than_for_profile( &self, id: Uuid, ) -> impl DoubleEndedIterator { let this = self.clone(); self.extract(id, |s| s.published.map(|p| (s.profile_id, p))) .into_iter() .flat_map(move |(profile_id, published)| { let range_start = profile_id_publshed_submission_range_beginning(profile_id); let range_entry = profile_id_publshed_submission_range_entry(profile_id, published); this.published_date_range_for_profile(range_start..range_entry) }) .rev() } pub fn by_id(&self, id: Uuid) -> Result, StoreError> { let stored_submission_ivec = match self.submission_tree.get(id_submission_key(id))? { Some(ivec) => ivec, None => return Ok(None), }; let stored_submission: StoredSubmission = serde_json::from_slice(&stored_submission_ivec)?; Ok(Some(stored_submission.into())) } pub fn delete(&self, submission_id: Uuid) -> Result>, StoreError> { let stored_submission_ivec = match self.submission_tree.get(id_submission_key(submission_id))? { Some(ivec) => ivec, None => return Ok(None), }; let stored_submission: StoredSubmission = serde_json::from_slice(&stored_submission_ivec)?; let id = submission_id; let profile_id = stored_submission.profile_id; let drafted_at = stored_submission.drafted_at; let published = stored_submission.published; [ &self.submission_tree, &self.profile_tree, &self.profile_drafted_tree, &self.published_tree, &self.profile_published_tree, &self.count_tree, ] .transaction(move |trees| { let submission_tree = &trees[0]; let profile_tree = &trees[1]; let profile_drafted_tree = &trees[2]; let published_tree = &trees[3]; let profile_published_tree = &trees[4]; let count_tree = &trees[5]; submission_tree.remove(id_submission_key(id).as_bytes())?; profile_tree.remove(profile_id_key(profile_id, id).as_bytes())?; profile_drafted_tree .remove(profile_id_drafted_submission_key(profile_id, drafted_at, id).as_bytes())?; if let Some(published) = published { published_tree.remove(published_submission_key(published, id).as_bytes())?; profile_published_tree.remove( published_profile_submission_key(profile_id, published, id).as_bytes(), )?; } super::count( count_tree, &profile_id_submission_count_key(profile_id), |c| c.saturating_sub(1), )?; Ok(()) })?; Ok(Some(Undo(stored_submission.into()))) } } fn uuid_from_ivec(ivec: sled::IVec) -> Option { String::from_utf8_lossy(&ivec).parse().ok() } // Used to map id -> Submission fn id_submission_key(id: Uuid) -> String { format!("/submission/{}/data", id) } // Used to map profile_id -> id fn profile_id_key(profile_id: Uuid, id: Uuid) -> String { format!("/profile/{}/submission/{}", profile_id, id) } fn profile_id_published_prefix(profile_id: Uuid) -> String { format!("/profile/{}/published", profile_id) } fn profile_id_drafted_prefix(profile_id: Uuid) -> String { format!("/profile/{}/drafted", profile_id) } // Used to fetch submissions for a given profile in a user-recognizalbe order fn profile_id_drafted_submission_key( profile_id: Uuid, drafted_at: DateTime, id: Uuid, ) -> String { format!( "/profile/{}/drafted/{}/submission/{}", profile_id, drafted_at.to_rfc3339(), id ) } fn profile_id_drafted_submission_range_beginning(profile_id: Uuid) -> String { format!("/profile/{}/drafted/", profile_id) } fn profile_id_drafted_submission_range_entry( profile_id: Uuid, drafted_at: DateTime, ) -> String { format!( "/profile/{}/drafted/{}", profile_id, drafted_at.to_rfc3339() ) } fn profile_id_drafted_submission_range_end(profile_id: Uuid) -> String { format!("/profile/{}/draftee/", profile_id) } fn published_profile_submission_key( profile_id: Uuid, published: DateTime, id: Uuid, ) -> String { format!( "/profile/{}/published/{}/submission/{}", profile_id, published.to_rfc3339(), id ) } fn published_submission_key(published: DateTime, id: Uuid) -> String { format!("/published/{}/submission/{}", published.to_rfc3339(), id) } fn published_prefix() -> String { "/published".to_owned() } fn published_submission_range_start(published: DateTime) -> String { format!("/published/{}", published.to_rfc3339()) } fn profile_id_publshed_submission_range_beginning(profile_id: Uuid) -> String { format!("/profile/{}/published/", profile_id) } fn profile_id_publshed_submission_range_entry( profile_id: Uuid, published: DateTime, ) -> String { format!( "/profile/{}/published/{}", profile_id, published.to_rfc3339() ) } fn profile_id_publshed_submission_range_end(profile_id: Uuid) -> String { format!("/profile/{}/publishee/", profile_id) } fn profile_id_submission_count_key(profile_id: Uuid) -> String { format!("/profile/{}/count", profile_id) } impl From for Submission { fn from(ss: StoredSubmission) -> Self { Submission { id: ss.id, profile_id: ss.profile_id, title: ss.title, title_source: ss.title_source, description: ss.description, description_source: ss.description_source, files: ss.files, published: ss.published, updated: ss.updated, visibility: ss.visibility, local_only: ss.local_only, logged_in_only: ss.logged_in_only, sensitive: ss.sensitive, } } } impl fmt::Display for Visibility { fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { match self { Visibility::Public => write!(f, "Public"), Visibility::Unlisted => write!(f, "Unlisted"), Visibility::Followers => write!(f, "Followers"), } } }