382 lines
11 KiB
Rust
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()
|
|
}
|