This commit is contained in:
2025-01-30 00:50:49 +01:00
parent d6972e604e
commit 83ad4ca784
10 changed files with 346 additions and 274 deletions

View File

@@ -37,7 +37,7 @@ type-toppings = { version = "0.2.1", features = ["result"] }
wasm-bindgen = "=0.2.99" wasm-bindgen = "=0.2.99"
web-sys = { version = "0.3.76", features = ["File", "FileList"] } web-sys = { version = "0.3.76", features = ["File", "FileList"] }
xdg = { version = "2.5", optional = true } xdg = { version = "2.5", optional = true }
uuid = { version = "1.12", optional = true, features = ["serde", "v4"] } uuid = { version = "1.12", features = ["serde", "v4"] }
redb = { version = "2.4", optional = true } redb = { version = "2.4", optional = true }
bincode = { version = "1.3", optional = true } bincode = { version = "1.3", optional = true }
@@ -48,7 +48,6 @@ version = "1"
hydrate = ["leptos/hydrate"] hydrate = ["leptos/hydrate"]
ssr = [ ssr = [
"dep:axum", "dep:axum",
"dep:uuid",
"dep:redb", "dep:redb",
"dep:bincode", "dep:bincode",
"dep:tokio", "dep:tokio",

View File

@@ -1,8 +1,94 @@
//! Shared models between server and client code. //! Shared models between server and client code.
pub use v1::Hold;
pub use v1::HoldPosition;
pub use v1::HoldRole;
pub use v1::Image;
pub use v2::Method;
pub use v2::Problem;
pub use v2::ProblemId;
pub use v2::Root;
pub use v2::Wall;
pub use v2::WallId;
pub mod v2 {
use super::v1;
use serde::Deserialize; use serde::Deserialize;
use serde::Serialize; use serde::Serialize;
use std::collections::BTreeMap; use std::collections::BTreeMap;
use std::collections::BTreeSet;
#[derive(Serialize, Deserialize, Debug)]
pub struct Root {
pub walls: BTreeSet<WallId>,
}
#[derive(Serialize, Deserialize, Debug, Clone)]
pub struct Wall {
pub uid: WallId,
pub rows: u64,
pub cols: u64,
pub holds: BTreeMap<v1::HoldPosition, v1::Hold>,
pub problems: BTreeSet<ProblemId>,
}
#[derive(Serialize, Deserialize, Debug, PartialEq, Eq, PartialOrd, Ord, Clone, Copy)]
pub struct WallId(pub uuid::Uuid);
impl WallId {
pub fn new() -> Self {
Self(uuid::Uuid::new_v4())
}
}
#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Eq, PartialOrd, Ord)]
pub struct Problem {
pub uid: ProblemId,
pub name: String,
pub set_by: String,
pub holds: BTreeMap<v1::HoldPosition, v1::HoldRole>,
pub method: Method,
pub date_added: chrono::DateTime<chrono::Utc>,
}
#[derive(Serialize, Deserialize, Debug, PartialEq, Eq, PartialOrd, Ord, Clone, Copy)]
pub struct ProblemId(pub uuid::Uuid);
impl ProblemId {
pub fn new() -> Self {
Self(uuid::Uuid::new_v4())
}
}
#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Eq, PartialOrd, Ord)]
pub enum Method {
FeetFollowHands,
Footless,
FootlessPlusKickboard,
}
}
pub mod v1 {
use serde::Deserialize;
use serde::Serialize;
use smart_default::SmartDefault;
use std::collections::BTreeMap;
use std::collections::BTreeSet;
const STATE_VERSION: u64 = 1;
#[derive(Serialize, Deserialize, Clone, Debug, SmartDefault)]
pub struct PersistentState {
/// State schema version
#[default(STATE_VERSION)]
pub version: u64,
pub wall: Wall,
pub problems: Problems,
}
#[derive(Serialize, Deserialize, Clone, Debug, Default)]
pub struct Problems {
pub problems: BTreeSet<Problem>,
}
#[derive(Serialize, Deserialize, Clone, Debug)] #[derive(Serialize, Deserialize, Clone, Debug)]
pub struct Wall { pub struct Wall {
@@ -59,13 +145,14 @@ pub enum HoldRole {
End, End,
} }
#[derive(Serialize, Deserialize, Clone, Debug, Default)] #[derive(Serialize, Deserialize, Clone, Debug)]
pub struct Hold { pub struct Hold {
pub position: HoldPosition, pub position: HoldPosition,
pub image: Option<Image>, pub image: Option<Image>,
} }
#[derive(Serialize, Deserialize, Clone, Debug, Default)] #[derive(Serialize, Deserialize, Clone, Debug)]
pub struct Image { pub struct Image {
pub filename: String, pub filename: String,
} }
}

View File

@@ -10,6 +10,7 @@ use leptos::reactive::graph::ReactiveNode;
use serde::Deserialize; use serde::Deserialize;
use serde::Serialize; use serde::Serialize;
use std::ops::Deref; use std::ops::Deref;
use std::sync::Arc;
#[component] #[component]
pub fn Wall() -> impl leptos::IntoView { pub fn Wall() -> impl leptos::IntoView {
@@ -125,15 +126,31 @@ pub struct InitialData {
} }
#[server] #[server]
async fn load_initial_data() -> Result<RonCodec<InitialData>, ServerFnError> { #[tracing::instrument(skip_all, err)]
todo!() async fn load_initial_data(wall_id: models::WallId) -> Result<RonCodec<InitialData>, ServerFnError> {
// let state = expect_context::<State>(); let db = expect_context::<Arc<redb::Database>>();
// let wall = state.persistent.with(|s| s.wall.clone()).await; #[derive(Debug, derive_more::Error, derive_more::Display)]
// Ok(RonCodec::new(InitialData { wall })) enum Error {
#[display("Wall not found: {_0:?}")]
NotFound(#[error(not(source))] models::WallId),
}
let wall = tokio::task::spawn_blocking(move || -> Result<models::Wall, ServerFnError> {
let read_txn = db.begin_read()?;
let walls_table = read_txn.open_table(crate::server::db::current::TABLE_WALLS)?;
let wall = walls_table.get(wall_id)?.ok_or(Error::NotFound(wall_id))?.value();
Ok(wall)
})
.await??;
Ok(RonCodec::new(InitialData { wall }))
} }
#[server] #[server]
#[tracing::instrument(skip_all, err)]
async fn get_random_problem() -> Result<RonCodec<Option<models::Problem>>, ServerFnError> { async fn get_random_problem() -> Result<RonCodec<Option<models::Problem>>, ServerFnError> {
todo!() todo!()
// use rand::seq::IteratorRandom; // use rand::seq::IteratorRandom;

View File

@@ -4,7 +4,6 @@ use cli::Cli;
use config::Config; use config::Config;
use confik::Configuration; use confik::Configuration;
use confik::EnvSource; use confik::EnvSource;
use state::PersistentState;
use std::path::Path; use std::path::Path;
use std::sync::Arc; use std::sync::Arc;
use tower_http::services::ServeDir; use tower_http::services::ServeDir;
@@ -13,10 +12,9 @@ use type_toppings::ResultExt;
mod cli; mod cli;
pub mod config; pub mod config;
mod db; pub mod db;
mod migrations; mod migrations;
pub mod operations; pub mod operations;
pub mod state;
pub const STATE_FILE: &str = "datastore/private/state.ron"; pub const STATE_FILE: &str = "datastore/private/state.ron";
@@ -128,5 +126,5 @@ pub enum Error {
Confik(confik::Error), Confik(confik::Error),
Database(redb::DatabaseError), Database(redb::Error),
} }

View File

@@ -1,14 +1,16 @@
use bincode::Bincode;
use redb::Database; use redb::Database;
use redb::DatabaseError; use redb::TableDefinition;
use serde::Deserialize;
use serde::Serialize;
use std::path::PathBuf; use std::path::PathBuf;
mod bincode; mod bincode;
pub mod db_models;
pub const DB_FILE: &str = "datastore/private/ascend.redb"; pub const DB_FILE: &str = "datastore/private/ascend.redb";
#[tracing::instrument(err)] #[tracing::instrument(skip_all, err)]
pub fn create() -> Result<Database, DatabaseError> { pub fn create() -> Result<Database, redb::Error> {
let file = PathBuf::from(DB_FILE); let file = PathBuf::from(DB_FILE);
// Create parent dirs // Create parent dirs
@@ -19,3 +21,45 @@ pub fn create() -> Result<Database, DatabaseError> {
let db = Database::create(file)?; let db = Database::create(file)?;
Ok(db) Ok(db)
} }
#[tracing::instrument(skip_all)]
pub fn get_version(db: &Database) -> Result<Option<Version>, redb::Error> {
let txn = db.begin_read()?;
let version = txn.open_table(TABLE_VERSION)?.get(())?.map(|v| v.value());
Ok(version)
}
pub const TABLE_VERSION: TableDefinition<(), Bincode<Version>> = TableDefinition::new("version");
#[derive(Serialize, Deserialize, Debug, derive_more::Display)]
#[display("{version}")]
pub struct Version {
pub version: u64,
}
impl Version {
pub fn current() -> Version {
Version { version: current::VERSION }
}
}
pub use v2 as current;
pub mod v2 {
use crate::models;
use crate::server::db::bincode::Bincode;
use redb::TableDefinition;
pub const VERSION: u64 = 2;
pub const TABLE_ROOT: TableDefinition<(), Bincode<models::v2::Root>> = TableDefinition::new("root");
pub const TABLE_WALLS: TableDefinition<Bincode<models::v2::WallId>, Bincode<models::v2::Wall>> = TableDefinition::new("walls");
pub const TABLE_PROBLEMS: TableDefinition<Bincode<(models::v2::WallId, models::v2::ProblemId)>, Bincode<models::v2::Problem>> =
TableDefinition::new("problems");
}
pub mod v1 {
use crate::models;
use crate::server::db::bincode::Bincode;
use redb::TableDefinition;
pub const TABLE_ROOT: TableDefinition<(), Bincode<models::v1::PersistentState>> = TableDefinition::new("root");
}

View File

@@ -1,105 +0,0 @@
use super::bincode::Bincode;
use redb::TableDefinition;
use serde::Deserialize;
use serde::Serialize;
pub use v2::Hold;
pub use v2::HoldPosition;
pub use v2::HoldRole;
pub use v2::Image;
pub use v2::Method;
pub use v2::Problem;
pub use v2::ProblemId;
pub use v2::Root;
pub use v2::TABLE_ROOT;
pub use v2::TABLE_WALLS;
pub use v2::Wall;
pub use v2::WallId;
pub const TABLE_VERSION: TableDefinition<(), Bincode<Version>> = TableDefinition::new("version");
#[derive(Serialize, Deserialize, Debug)]
pub struct Version {
pub version: u64,
}
pub mod v2 {
use crate::server::db::bincode::Bincode;
use redb::TableDefinition;
use serde::Deserialize;
use serde::Serialize;
use std::collections::BTreeMap;
use std::collections::BTreeSet;
pub const TABLE_ROOT: TableDefinition<(), Bincode<Root>> = TableDefinition::new("root");
pub const TABLE_WALLS: TableDefinition<Bincode<WallId>, Bincode<Wall>> = TableDefinition::new("walls");
#[derive(Serialize, Deserialize, Debug)]
pub struct Root {
pub walls: BTreeSet<WallId>,
}
#[derive(Serialize, Deserialize, Debug)]
pub struct Wall {
pub uid: WallId,
pub rows: u64,
pub cols: u64,
pub holds: BTreeMap<HoldPosition, Hold>,
}
#[derive(Serialize, Deserialize, Debug, PartialEq, Eq, PartialOrd, Ord, Clone, Copy)]
pub struct WallId(pub uuid::Uuid);
#[derive(Serialize, Deserialize, Debug)]
pub struct Problem {
pub uid: ProblemId,
pub name: String,
pub set_by: String,
pub holds: BTreeMap<HoldPosition, HoldRole>,
pub method: Method,
pub date_added: chrono::NaiveDate,
}
#[derive(Serialize, Deserialize, Debug, PartialEq, Eq, PartialOrd, Ord, Clone, Copy)]
pub struct ProblemId(pub uuid::Uuid);
#[derive(Serialize, Deserialize, Debug)]
pub enum Method {
FeetFollowHands,
Footless,
FootlessPlusKickboard,
}
#[derive(Serialize, Deserialize, Debug)]
pub enum HoldRole {
Start,
Normal,
Zone,
End,
}
#[derive(Serialize, Deserialize, Debug, PartialEq, Eq, PartialOrd, Ord)]
pub struct HoldPosition {
pub row: u64,
pub col: u64,
}
#[derive(Serialize, Deserialize, Debug)]
pub struct Hold {
pub position: HoldPosition,
pub image: Option<Image>,
}
#[derive(Serialize, Deserialize, Debug)]
pub struct Image {
pub filename: String,
}
}
pub mod v1 {
use crate::server::db::bincode::Bincode;
use crate::server::state::PersistentState;
use redb::TableDefinition;
pub type Root = PersistentState;
pub const TABLE_ROOT: TableDefinition<(), Bincode<Root>> = TableDefinition::new("root");
}

View File

@@ -1,5 +1,5 @@
use super::state::PersistentState; use super::db;
use leptos::prelude::StorageAccess; use crate::models;
use redb::Database; use redb::Database;
use redb::ReadableTable; use redb::ReadableTable;
use redb::ReadableTableMetadata; use redb::ReadableTableMetadata;
@@ -7,34 +7,17 @@ use std::collections::BTreeSet;
use std::path::PathBuf; use std::path::PathBuf;
use type_toppings::ResultExt; use type_toppings::ResultExt;
#[tracing::instrument] #[tracing::instrument(skip_all, err)]
pub async fn run_migrations(db: &Database) -> Result<(), Box<dyn std::error::Error>> { pub async fn run_migrations(db: &Database) -> Result<(), Box<dyn std::error::Error>> {
migrate_state_file().await?;
migrate_from_ron_to_redb(db).await?; migrate_from_ron_to_redb(db).await?;
init_at_current_version(db).await?;
migrate_to_v2(db).await?; migrate_to_v2(db).await?;
Ok(()) Ok(())
} }
/// State file moved to datastore/private
#[tracing::instrument(err)]
async fn migrate_state_file() -> Result<(), Box<dyn std::error::Error>> {
let m = PathBuf::from("state.ron");
if m.try_exists().expect_or_report_with(|| format!("Failed to read {}", m.display())) {
tracing::warn!("MIGRATING");
let p = PathBuf::from(super::STATE_FILE);
tokio::fs::create_dir_all(p.parent().unwrap()).await?;
tokio::fs::rename(m, &p).await?;
}
Ok(())
}
/// Use redb DB instead of Ron state file /// Use redb DB instead of Ron state file
#[tracing::instrument(err)] #[tracing::instrument(skip_all, err)]
async fn migrate_from_ron_to_redb(db: &Database) -> Result<(), Box<dyn std::error::Error>> { async fn migrate_from_ron_to_redb(db: &Database) -> Result<(), Box<dyn std::error::Error>> {
use super::db::db_models::TABLE_VERSION;
use super::db::db_models::Version;
use super::db::db_models::v1;
let ron_state_file_path = PathBuf::from(super::STATE_FILE); let ron_state_file_path = PathBuf::from(super::STATE_FILE);
if ron_state_file_path if ron_state_file_path
@@ -43,27 +26,27 @@ async fn migrate_from_ron_to_redb(db: &Database) -> Result<(), Box<dyn std::erro
{ {
tracing::warn!("MIGRATING"); tracing::warn!("MIGRATING");
let ron_state: PersistentState = { let ron_state: models::v1::PersistentState = {
let content = tokio::fs::read_to_string(&ron_state_file_path).await?; let content = tokio::fs::read_to_string(&ron_state_file_path).await?;
ron::from_str(&content)? ron::from_str(&content)?
}; };
let write_txn = db.begin_write()?; let write_txn = db.begin_write()?;
{ {
let mut version_table = write_txn.open_table(TABLE_VERSION)?; let mut version_table = write_txn.open_table(db::TABLE_VERSION)?;
assert!(version_table.is_empty()?); assert!(version_table.is_empty()?);
version_table.insert((), Version { version: 1 }); version_table.insert((), db::Version { version: 1 })?;
let mut root_table = write_txn.open_table(v1::TABLE_ROOT)?; let mut root_table = write_txn.open_table(db::v1::TABLE_ROOT)?;
assert!(root_table.is_empty()?); assert!(root_table.is_empty()?);
let root = v1::Root { let persistent_state = models::v1::PersistentState {
version: ron_state.version, version: ron_state.version,
wall: ron_state.wall, wall: ron_state.wall,
problems: ron_state.problems, problems: ron_state.problems,
}; };
root_table.insert((), root)?; root_table.insert((), persistent_state)?;
} }
write_txn.commit()?; write_txn.commit()?;
@@ -74,63 +57,103 @@ async fn migrate_from_ron_to_redb(db: &Database) -> Result<(), Box<dyn std::erro
Ok(()) Ok(())
} }
#[tracing::instrument(err)] // TODO: Move out, is not really a migration
#[tracing::instrument(skip_all, err)]
async fn init_at_current_version(db: &Database) -> Result<(), Box<dyn std::error::Error>> {
let txn = db.begin_write()?;
{
let mut version_table = txn.open_table(db::TABLE_VERSION)?;
let is_missing_version = version_table.get(())?.is_none();
if is_missing_version {
let v = db::Version::current();
tracing::warn!("INITIALIZING DATABASE AT VERSION {v}");
version_table.insert((), v)?;
// Root table
{
let mut table = txn.open_table(db::current::TABLE_ROOT)?;
assert!(table.is_empty()?);
table.insert((), models::Root { walls: BTreeSet::new() })?;
}
// Walls table
{
// Opening the table creates the table
let table = txn.open_table(db::current::TABLE_WALLS)?;
assert!(table.is_empty()?);
}
// Problems table
{
// Opening the table creates the table
let table = txn.open_table(db::current::TABLE_PROBLEMS)?;
assert!(table.is_empty()?);
}
}
}
txn.commit()?;
Ok(())
}
#[tracing::instrument(skip_all, err)]
async fn migrate_to_v2(db: &Database) -> Result<(), Box<dyn std::error::Error>> { async fn migrate_to_v2(db: &Database) -> Result<(), Box<dyn std::error::Error>> {
use super::db::db_models::TABLE_VERSION; use super::db;
use super::db::db_models::Version;
use super::db::db_models::v1;
use super::db::db_models::v2;
let txn = db.begin_write()?; let txn = db.begin_write()?;
{ {
let mut version_table = txn.open_table(TABLE_VERSION)?; let mut version_table = txn.open_table(db::TABLE_VERSION)?;
let version = version_table.get(())?.unwrap().value().version; let version = version_table.get(())?.unwrap().value().version;
if version == 1 { if version == 1 {
tracing::warn!("MIGRATING"); tracing::warn!("MIGRATING");
version_table.insert((), Version { version: 2 })?; version_table.insert((), db::Version { version: 2 })?;
let root_table_v1 = txn.open_table(v1::TABLE_ROOT)?; let root_table_v1 = txn.open_table(db::v1::TABLE_ROOT)?;
let root_v1 = root_table_v1.get(())?.unwrap().value(); let root_v1 = root_table_v1.get(())?.unwrap().value();
txn.delete_table(v1::TABLE_ROOT)?; txn.delete_table(db::v1::TABLE_ROOT)?;
let v1::Root { version: _, wall, problems } = root_v1; let models::v1::PersistentState { version: _, wall, problems } = root_v1;
// we'll reimport them instead of a lossy conversion.
drop(problems);
let mut walls = BTreeSet::new(); let mut walls = BTreeSet::new();
let wall_uid = v2::WallId(uuid::Uuid::new_v4()); let wall_uid = models::v2::WallId(uuid::Uuid::new_v4());
let holds = wall let holds = wall
.holds .holds
.into_iter() .into_iter()
.map(|(hold_position, hold)| { .map(|(hold_position, hold)| {
( (
v2::HoldPosition { models::v1::HoldPosition {
row: hold_position.row, row: hold_position.row,
col: hold_position.col, col: hold_position.col,
}, },
v2::Hold { models::v1::Hold {
position: v2::HoldPosition { position: models::v1::HoldPosition {
row: hold.position.row, row: hold.position.row,
col: hold.position.col, col: hold.position.col,
}, },
image: hold.image.map(|i| v2::Image { filename: i.filename }), image: hold.image.map(|i| models::v1::Image { filename: i.filename }),
}, },
) )
}) })
.collect(); .collect();
let wall_v2 = v2::Wall { let wall_v2 = models::v2::Wall {
uid: wall_uid, uid: wall_uid,
rows: wall.rows, rows: wall.rows,
cols: wall.cols, cols: wall.cols,
holds, holds,
problems: BTreeSet::new(),
}; };
walls.insert(wall_v2.uid); walls.insert(wall_v2.uid);
let root_v2 = v2::Root { walls }; let root_v2 = models::v2::Root { walls };
let mut root_table_v2 = txn.open_table(v2::TABLE_ROOT)?; let mut root_table_v2 = txn.open_table(db::v2::TABLE_ROOT)?;
root_table_v2.insert((), root_v2)?; root_table_v2.insert((), root_v2)?;
let mut walls_table = txn.open_table(v2::TABLE_WALLS)?; let mut walls_table = txn.open_table(db::v2::TABLE_WALLS)?;
walls_table.insert(wall_v2.uid, wall_v2)?; walls_table.insert(wall_v2.uid, wall_v2)?;
} }
} }

View File

@@ -4,11 +4,14 @@ use crate::models;
use crate::models::HoldPosition; use crate::models::HoldPosition;
use crate::models::HoldRole; use crate::models::HoldRole;
use crate::server::config::Config; use crate::server::config::Config;
use crate::server::db;
use redb::Database; use redb::Database;
use redb::ReadableTable;
use std::collections::BTreeMap; use std::collections::BTreeMap;
use std::sync::Arc;
#[tracing::instrument] #[tracing::instrument(skip_all)]
pub(crate) async fn import_mini_moonboard_problems(config: &Config, db: &Database) -> Result<(), Error> { pub(crate) async fn import_mini_moonboard_problems(config: &Config, db: Arc<Database>, wall_id: models::WallId) -> Result<(), Error> {
use moonboard_parser::mini_moonboard; use moonboard_parser::mini_moonboard;
let mut problems = Vec::new(); let mut problems = Vec::new();
@@ -21,9 +24,9 @@ pub(crate) async fn import_mini_moonboard_problems(config: &Config, db: &Databas
let set_by = "mini-mb-2020-parser"; let set_by = "mini-mb-2020-parser";
let mini_moonboard = mini_moonboard::parse(file_path.as_std_path()).await?; let mini_moonboard = mini_moonboard::parse(file_path.as_std_path()).await?;
for problem in mini_moonboard.problems { for mini_mb_problem in mini_moonboard.problems {
let mut holds = BTreeMap::<HoldPosition, HoldRole>::new(); let mut holds = BTreeMap::<HoldPosition, HoldRole>::new();
for mv in problem.moves { for mv in mini_mb_problem.moves {
let row = mv.description.row(); let row = mv.description.row();
let col = mv.description.column(); let col = mv.description.column();
let hold_position = HoldPosition { row, col }; let hold_position = HoldPosition { row, col };
@@ -37,29 +40,47 @@ pub(crate) async fn import_mini_moonboard_problems(config: &Config, db: &Databas
holds.insert(hold_position, role); holds.insert(hold_position, role);
} }
// TODO: let name = mini_mb_problem.name;
// let name = problem.name; let method = match mini_mb_problem.method {
mini_moonboard::Method::FeetFollowHands => models::Method::FeetFollowHands,
mini_moonboard::Method::Footless => models::Method::Footless,
mini_moonboard::Method::FootlessPlusKickboard => models::Method::FootlessPlusKickboard,
};
// let method = match problem.method { let problem_id = models::ProblemId::new();
// mini_moonboard::Method::FeetFollowHands => models::Method::FeetFollowHands,
// mini_moonboard::Method::Footless => models::Method::Footless,
// mini_moonboard::Method::FootlessPlusKickboard => models::Method::FootlessPlusKickboard,
// };
// let problem = models::Problem::new(name, set_by.to_owned(), holds, method); let problem = models::Problem {
// problems.push(problem); uid: problem_id,
name,
let problem = models::Problem { holds }; set_by: set_by.to_owned(),
holds,
method,
date_added: chrono::Utc::now(),
};
problems.push(problem); problems.push(problem);
} }
state tokio::task::spawn_blocking(move || -> Result<(), redb::Error> {
.persistent let write_txn = db.begin_write()?;
.update(|s| { {
s.problems.problems.extend(problems); let mut walls_table = write_txn.open_table(db::current::TABLE_WALLS)?;
let mut problems_table = write_txn.open_table(db::current::TABLE_PROBLEMS)?;
let mut wall = walls_table.get(wall_id)?.unwrap().value();
wall.problems.extend(problems.iter().map(|p| p.uid));
walls_table.insert(wall_id, wall)?;
for problem in problems {
let key = (wall_id, problem.uid);
problems_table.insert(key, problem)?;
}
}
write_txn.commit()?;
Ok(())
}) })
.await?; .await??;
Ok(()) Ok(())
} }
@@ -67,4 +88,6 @@ pub(crate) async fn import_mini_moonboard_problems(config: &Config, db: &Databas
#[derive(Debug, derive_more::Error, derive_more::Display, derive_more::From)] #[derive(Debug, derive_more::Error, derive_more::Display, derive_more::From)]
pub enum Error { pub enum Error {
Parser(moonboard_parser::Error), Parser(moonboard_parser::Error),
Redb(redb::Error),
Tokio(tokio::task::JoinError),
} }

View File

@@ -1,25 +0,0 @@
//! Server state
const STATE_VERSION: u64 = 1;
use crate::models;
use crate::models::Wall;
use serde::Deserialize;
use serde::Serialize;
use smart_default::SmartDefault;
use std::collections::BTreeSet;
#[derive(Serialize, Deserialize, Clone, Debug, SmartDefault)]
pub struct PersistentState {
/// State schema version
#[default(STATE_VERSION)]
pub version: u64,
pub wall: Wall,
pub problems: Problems,
}
#[derive(Serialize, Deserialize, Clone, Debug, Default)]
pub struct Problems {
pub problems: BTreeSet<models::Problem>,
}

11
todo.md Normal file
View File

@@ -0,0 +1,11 @@
- save images with a uuid
- downscale images
- associate routes with wall
- group routes by pattern (pattern family has shift/mirror variations)
- generate pattern families of variations when importing problems
- implement pattern challenge (start an "adventure mode" based on a pattern family)
- Record problem success (enum: flash, send, no-send)
- implement routes page to show all routes for a given wall
- implement favorite routes feature
- use wall id in URL.
- decide on routes vs problems terminology