hyaenidae/profiles/src/store/submission.rs

828 lines
25 KiB
Rust

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<String>,
description: Option<String>,
description_source: Option<String>,
files: Vec<Uuid>,
published: Option<DateTime<Utc>>,
updated: Option<DateTime<Utc>>,
visibility: Visibility,
local_only: bool,
logged_in_only: bool,
sensitive: bool,
}
#[derive(Debug)]
pub struct SubmissionChanges {
id: Uuid,
title: Option<String>,
title_source: Option<String>,
description: Option<String>,
description_source: Option<String>,
visibility: Option<Visibility>,
published: Option<DateTime<Utc>>,
updated: Option<DateTime<Utc>>,
local_only: Option<bool>,
logged_in_only: Option<bool>,
sensitive: Option<bool>,
original_files: Vec<Uuid>,
files: Vec<Uuid>,
}
#[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<String>,
#[serde(skip_serializing_if = "Option::is_none")]
description: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
description_source: Option<String>,
files: Vec<Uuid>,
#[serde(skip_serializing_if = "Option::is_none")]
published: Option<DateTime<Utc>>,
#[serde(skip_serializing_if = "Option::is_none")]
updated: Option<DateTime<Utc>>,
visibility: Visibility,
#[serde(default)]
local_only: bool,
#[serde(default)]
logged_in_only: bool,
#[serde(default)]
sensitive: bool,
drafted_at: DateTime<Utc>,
}
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<DateTime<Utc>> {
self.published
}
pub fn updated(&self) -> Option<DateTime<Utc>> {
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<DateTime<Utc>>) -> &mut Self {
if self.published.is_none() {
self.published = time.or_else(|| Some(Utc::now()));
}
self
}
pub(crate) fn updated(&mut self, time: DateTime<Utc>) -> &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<Self, sled::Error> {
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<Submission, StoreError> {
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<u64, StoreError> {
match self
.count_tree
.get(profile_id_submission_count_key(profile_id))?
{
Some(ivec) => Ok(String::from_utf8_lossy(&ivec)
.parse::<u64>()
.expect("Count is valid")),
None => Ok(0),
}
}
pub fn update(&self, changes: &SubmissionChanges) -> Result<Submission, StoreError> {
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<T, F>(&self, submission_id: Uuid, f: F) -> Option<T>
where
F: FnOnce(StoredSubmission) -> Option<T>,
{
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<Item = Uuid> {
self.published_tree
.scan_prefix(published_prefix())
.values()
.filter_map(|res| res.ok())
.filter_map(uuid_from_ivec)
.rev()
}
fn published_date_range<K>(
&self,
range: impl std::ops::RangeBounds<K>,
) -> impl DoubleEndedIterator<Item = Uuid>
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<Item = Uuid> {
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<Item = Uuid> {
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<Item = Uuid> {
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<K>(
&self,
range: impl std::ops::RangeBounds<K>,
) -> impl DoubleEndedIterator<Item = Uuid>
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<Item = Uuid> {
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<Item = Uuid> {
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<Item = Uuid> {
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<K>(
&self,
range: impl std::ops::RangeBounds<K>,
) -> impl DoubleEndedIterator<Item = Uuid>
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<Item = Uuid> {
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<Item = Uuid> {
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<Option<Submission>, 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<Option<Undo<Submission>>, 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<Uuid> {
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<Utc>,
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<Utc>,
) -> 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<Utc>,
id: Uuid,
) -> String {
format!(
"/profile/{}/published/{}/submission/{}",
profile_id,
published.to_rfc3339(),
id
)
}
fn published_submission_key(published: DateTime<Utc>, id: Uuid) -> String {
format!("/published/{}/submission/{}", published.to_rfc3339(), id)
}
fn published_prefix() -> String {
"/published".to_owned()
}
fn published_submission_range_start(published: DateTime<Utc>) -> 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<Utc>,
) -> 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<StoredSubmission> 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"),
}
}
}