hyaenidae/profiles/src/store/server.rs
asonix 22d8f85fba Profiles: Move bbcode, html sanitizing to Changes types
Handle Updated timestamp for profiles, servers, comments
2021-02-08 21:14:51 -06:00

382 lines
11 KiB
Rust

use crate::{store::StoreError, State};
use chrono::{DateTime, Utc};
use hyaenidae_content::{bbcode, html, strip};
use sled::{Db, Transactional, Tree};
use uuid::Uuid;
#[derive(Clone, Debug)]
pub struct Server {
id: Uuid,
domain: String,
created: DateTime<Utc>,
title: Option<String>,
title_source: Option<String>,
description: Option<String>,
description_source: Option<String>,
published: Option<DateTime<Utc>>,
updated: Option<DateTime<Utc>>,
}
#[derive(Debug)]
pub enum ServerChangesKind {
Create { domain: String },
Update { id: Uuid },
}
#[derive(Debug)]
pub struct ServerChanges<'a> {
state: &'a State,
kind: ServerChangesKind,
title: Option<String>,
title_source: Option<String>,
description: Option<String>,
description_source: Option<String>,
published: Option<DateTime<Utc>>,
updated: Option<DateTime<Utc>>,
}
#[derive(Clone)]
pub struct Store {
created_id: Tree,
domain_id: Tree,
id_domain: Tree,
id_created: Tree,
id_title: Tree,
id_title_source: Tree,
id_description: Tree,
id_description_source: Tree,
id_published: Tree,
id_updated: Tree,
self_server: Tree,
}
impl Server {
pub fn id(&self) -> Uuid {
self.id
}
pub fn domain(&self) -> &str {
&self.domain
}
pub fn title(&self) -> Option<&str> {
self.title.as_deref()
}
pub fn title_source(&self) -> Option<&str> {
self.title_source.as_deref()
}
pub fn description(&self) -> Option<&str> {
self.description.as_deref()
}
pub fn description_source(&self) -> Option<&str> {
self.description_source.as_deref()
}
pub fn created(&self) -> DateTime<Utc> {
self.created
}
pub(crate) fn updated(&self) -> Option<DateTime<Utc>> {
self.updated
}
pub(crate) fn changes<'a>(&self, state: &'a State) -> ServerChanges<'a> {
ServerChanges {
state,
kind: ServerChangesKind::Update { id: self.id },
title: None,
title_source: None,
description: None,
description_source: None,
published: self.published,
updated: None,
}
}
}
impl<'a> ServerChanges<'a> {
fn new(state: &'a State, domain: String) -> Self {
ServerChanges {
state,
kind: ServerChangesKind::Create { domain },
title: None,
title_source: None,
description: None,
description_source: None,
published: None,
updated: None,
}
}
pub(crate) fn title(&mut self, title: &str) -> &mut Self {
self.title = Some(strip(title));
self
}
pub(crate) fn title_source(&mut self, title_source: &str) -> &mut Self {
self.title_source = Some(title_source.to_owned());
self.title(title_source)
}
pub(crate) fn description(&mut self, description: &str) -> &mut Self {
self.description = Some(html(description));
self
}
pub(crate) fn description_source(&mut self, description_source: &str) -> &mut Self {
self.description_source = Some(description_source.to_owned());
self.description(&bbcode(description_source, |v| v))
}
pub(crate) fn published(&mut self, published: DateTime<Utc>) -> &mut Self {
if self.published.is_none() {
self.published = Some(published);
}
self
}
pub(crate) fn updated(&mut self, updated: DateTime<Utc>) -> &mut Self {
if self.published.is_some() {
self.updated = Some(updated);
}
self
}
pub(crate) fn any_changes(&self) -> bool {
self.title.is_some() || self.description.is_some()
}
pub(crate) fn save(self) -> Result<Server, StoreError> {
self.state.store.servers.save(&self)
}
}
impl Store {
pub(super) fn build(db: &Db) -> Result<Self, sled::Error> {
let created_id = db.open_tree("/profiles/servers/created_id")?;
let domain_id = db.open_tree("/profiles/servers/domain_id")?;
let id_domain = db.open_tree("/profiles/servers/id_domain")?;
let id_created = db.open_tree("/profiles/servers/id_created")?;
let id_title = db.open_tree("/profiles/servers/id_title")?;
let id_title_source = db.open_tree("/profiles/servers/id_title_source")?;
let id_description = db.open_tree("/profiles/servers/id_description")?;
let id_description_source = db.open_tree("/profiles/servers/id_description_source")?;
let id_published = db.open_tree("/profiles/servers/id_published")?;
let id_updated = db.open_tree("/profiles/servers/id_updated")?;
let self_server = db.open_tree("/profiles/servers/self_server")?;
Ok(Store {
created_id,
domain_id,
id_domain,
id_created,
id_title,
id_title_source,
id_description,
id_description_source,
id_published,
id_updated,
self_server,
})
}
pub(crate) fn self_exists(&self) -> bool {
self.self_server.iter().next().is_some()
}
pub(crate) fn set_self(&self, id: Uuid) -> Result<(), StoreError> {
self.self_server.insert("self", id.as_bytes())?;
Ok(())
}
pub(crate) fn is_self(&self, id: Uuid) -> Result<bool, StoreError> {
Ok(self
.self_server
.get("self")?
.and_then(uuid_from_ivec)
.map(|self_id| self_id == id)
.unwrap_or(false))
}
pub(crate) fn create<'a>(&self, state: &'a State, domain: String) -> ServerChanges<'a> {
ServerChanges::new(state, domain)
}
pub(crate) fn save(&self, changes: &ServerChanges) -> Result<Server, StoreError> {
match &changes.kind {
ServerChangesKind::Create { domain } => {
let id = self.do_create(domain, Utc::now())?;
self.do_update(id, changes)
}
ServerChangesKind::Update { id } => self.do_update(*id, changes),
}
}
fn do_create(&self, domain: &str, created: DateTime<Utc>) -> Result<Uuid, StoreError> {
let mut id;
while {
id = Uuid::new_v4();
self.id_domain
.compare_and_swap(
id.as_bytes(),
None as Option<&[u8]>,
Some(domain.as_bytes()),
)?
.is_err()
} {}
let domain_clone = domain.to_owned();
let res = [&self.created_id, &self.domain_id, &self.id_created].transaction(move |trees| {
let created_id = &trees[0];
let domain_id = &trees[1];
let id_created = &trees[2];
created_id.insert(created.to_rfc3339().as_bytes(), id.as_bytes())?;
domain_id.insert(domain_clone.as_bytes(), id.as_bytes())?;
id_created.insert(id.as_bytes(), created.to_rfc3339().as_bytes())?;
Ok(())
});
if let Err(e) = res {
self.id_domain.remove(id.as_bytes())?;
return Err(e.into());
}
Ok(id)
}
fn do_update(&self, id: Uuid, changes: &ServerChanges) -> Result<Server, StoreError> {
let domain = match self.id_domain.get(id.as_bytes())?.map(string_from_ivec) {
Some(domain) => domain,
None => return Err(StoreError::Missing),
};
let created = match self.id_created.get(id.as_bytes())?.and_then(date_from_ivec) {
Some(date) => date,
None => return Err(StoreError::Missing),
};
let stored_updated = self.id_updated.get(id.as_bytes())?.and_then(date_from_ivec);
let stored_published = self
.id_published
.get(id.as_bytes())?
.and_then(date_from_ivec);
if let Some(updated) = changes.updated {
if let Some(previously_updated) = stored_updated.or_else(|| stored_published) {
if updated < previously_updated {
return Err(StoreError::Outdated);
}
}
}
if let Some(title) = &changes.title {
self.id_title.insert(id.as_bytes(), title.as_bytes())?;
}
if let Some(title_source) = &changes.title_source {
self.id_title_source
.insert(id.as_bytes(), title_source.as_bytes())?;
}
if let Some(description) = &changes.description {
self.id_description
.insert(id.as_bytes(), description.as_bytes())?;
}
if let Some(description_source) = &changes.description_source {
self.id_description_source
.insert(id.as_bytes(), description_source.as_bytes())?;
}
if let Some(published) = changes.published {
self.id_published
.insert(id.as_bytes(), published.to_rfc3339().as_bytes())?;
}
if let Some(updated) = changes.updated {
self.id_updated
.insert(id.as_bytes(), updated.to_rfc3339().as_bytes())?;
}
Ok(Server {
id,
domain,
created,
title: changes.title.clone(),
title_source: changes.title_source.clone(),
description: changes.description.clone(),
description_source: changes.description_source.clone(),
published: changes.published,
updated: changes.updated,
})
}
pub fn get_self(&self) -> Result<Option<Uuid>, StoreError> {
Ok(self.self_server.get("self")?.and_then(uuid_from_ivec))
}
pub fn by_id(&self, id: Uuid) -> Result<Option<Server>, StoreError> {
let domain = self.id_domain.get(id.as_bytes())?.map(string_from_ivec);
let created = self.id_created.get(id.as_bytes())?.and_then(date_from_ivec);
let title = self.id_title.get(id.as_bytes())?.map(string_from_ivec);
let title_source = self
.id_title_source
.get(id.as_bytes())?
.map(string_from_ivec);
let description = self
.id_description
.get(id.as_bytes())?
.map(string_from_ivec);
let description_source = self
.id_description_source
.get(id.as_bytes())?
.map(string_from_ivec);
let updated = self.id_updated.get(id.as_bytes())?.and_then(date_from_ivec);
let published = self
.id_published
.get(id.as_bytes())?
.and_then(date_from_ivec);
Ok(domain.and_then(|domain| {
created.map(|created| Server {
id,
domain,
title,
title_source,
description,
description_source,
created,
updated,
published,
})
}))
}
pub fn by_domain(&self, domain: &str) -> Result<Option<Uuid>, StoreError> {
Ok(self.domain_id.get(domain)?.and_then(uuid_from_ivec))
}
pub fn known(&self) -> impl DoubleEndedIterator<Item = Uuid> {
self.created_id
.iter()
.values()
.filter_map(|res| res.ok())
.filter_map(uuid_from_ivec)
}
}
fn string_from_ivec(ivec: sled::IVec) -> String {
String::from_utf8_lossy(&ivec).to_string()
}
fn uuid_from_ivec(ivec: sled::IVec) -> Option<Uuid> {
Uuid::from_slice(&ivec).ok()
}
fn date_from_ivec(ivec: sled::IVec) -> Option<DateTime<Utc>> {
String::from_utf8_lossy(&ivec).parse().ok()
}