diff --git a/Cargo.lock b/Cargo.lock index d9f64fe..aedbd72 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -26,6 +26,21 @@ dependencies = [ "memchr", ] +[[package]] +name = "android-tzdata" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e999941b234f3131b00bc13c22d06e8c5ff726d1b6318ac7eb276997bbb4fef0" + +[[package]] +name = "android_system_properties" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "819e7219dbd41043ac279b19830f2efc897156490d7fd6ea916720117ee66311" +dependencies = [ + "libc", +] + [[package]] name = "anstream" version = "0.6.18" @@ -105,7 +120,9 @@ name = "ascend" version = "0.0.0" dependencies = [ "axum", + "bincode", "camino", + "chrono", "clap", "confik", "console_error_panic_hook", @@ -117,6 +134,7 @@ dependencies = [ "leptos_router", "moonboard-parser", "rand", + "redb", "ron", "serde", "serde_json", @@ -129,6 +147,7 @@ dependencies = [ "tracing-subscriber", "tracing-subscriber-wasm", "type-toppings", + "uuid", "wasm-bindgen", "web-sys", "xdg", @@ -320,12 +339,36 @@ dependencies = [ "serde", ] +[[package]] +name = "cc" +version = "1.2.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "13208fcbb66eaeffe09b99fffbe1af420f00a7b35aa99ad683dfc1aa76145229" +dependencies = [ + "shlex", +] + [[package]] name = "cfg-if" version = "1.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd" +[[package]] +name = "chrono" +version = "0.4.39" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7e36cc9d416881d2e24f9a963be5fb1cd90966419ac844274161d10488b3e825" +dependencies = [ + "android-tzdata", + "iana-time-zone", + "js-sys", + "num-traits", + "serde", + "wasm-bindgen", + "windows-targets", +] + [[package]] name = "ciborium" version = "0.2.2" @@ -510,6 +553,12 @@ dependencies = [ "unicode-segmentation", ] +[[package]] +name = "core-foundation-sys" +version = "0.8.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "773648b94d0e5d620f64f280777445740e61fe701025087ec8b57f45c791888b" + [[package]] name = "crossbeam-utils" version = "0.8.21" @@ -1174,6 +1223,29 @@ dependencies = [ "tower-service", ] +[[package]] +name = "iana-time-zone" +version = "0.1.61" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "235e081f3925a06703c2d0117ea8b91f042756fd6e7a6e5d901e8ca1a996b220" +dependencies = [ + "android_system_properties", + "core-foundation-sys", + "iana-time-zone-haiku", + "js-sys", + "wasm-bindgen", + "windows-core", +] + +[[package]] +name = "iana-time-zone-haiku" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f31827a206f56af32e590ba56d5d2d085f558508192593743f16b2306495269f" +dependencies = [ + "cc", +] + [[package]] name = "icu_collections" version = "1.5.0" @@ -1776,6 +1848,15 @@ dependencies = [ "winapi", ] +[[package]] +name = "num-traits" +version = "0.2.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "071dfc062690e90b734c0b2273ce72ad0ffa95f0c74596bc250dcfd960262841" +dependencies = [ + "autocfg", +] + [[package]] name = "num_cpus" version = "1.16.0" @@ -2087,6 +2168,15 @@ dependencies = [ "syn", ] +[[package]] +name = "redb" +version = "2.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ea0a72cd7140de9fc3e318823b883abf819c20d478ec89ce880466dc2ef263c6" +dependencies = [ + "libc", +] + [[package]] name = "redox_syscall" version = "0.5.8" @@ -2370,6 +2460,12 @@ dependencies = [ "lazy_static", ] +[[package]] +name = "shlex" +version = "1.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0fda2ff0d084019ba4d7c6f371c95d8fd75ce3524c3cb8fb653a3023f6323e64" + [[package]] name = "slab" version = "0.4.9" @@ -2906,6 +3002,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b3758f5e68192bb96cc8f9b7e2c2cfdabb435499a28499a42f8f984092adad4b" dependencies = [ "getrandom", + "serde", ] [[package]] @@ -3048,7 +3145,7 @@ version = "0.1.9" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "cf221c93e13a30d793f7645a0e7762c55d169dbb0a49671918a2319d289b10bb" dependencies = [ - "windows-sys 0.59.0", + "windows-sys 0.52.0", ] [[package]] @@ -3057,6 +3154,15 @@ version = "0.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f" +[[package]] +name = "windows-core" +version = "0.52.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "33ab640c8d7e35bf8ba19b884ba838ceb4fba93a4e8c65a9059d08afcfc683d9" +dependencies = [ + "windows-targets", +] + [[package]] name = "windows-sys" version = "0.52.0" diff --git a/crates/ascend/Cargo.toml b/crates/ascend/Cargo.toml index 974d558..6d2c5e8 100644 --- a/crates/ascend/Cargo.toml +++ b/crates/ascend/Cargo.toml @@ -9,33 +9,37 @@ publish = false crate-type = ["cdylib", "rlib"] [dependencies] -moonboard-parser = { workspace = true, optional = true } axum = { version = "0.7", optional = true } +camino = { version = "1.1", optional = true } +chrono = { version = "0.4.39", features = ["now", "serde"] } +clap = { version = "4.5.7", features = ["derive"] } +confik = { version = "0.12", optional = true, features = ["camino"] } console_error_panic_hook = "0.1" +derive_more = { version = "1", features = ["display", "error", "from"] } +http = "1" leptos = { version = "0.7.4", features = ["tracing"] } -server_fn = { version = "0.7.4", features = ["cbor"] } leptos_axum = { version = "0.7", optional = true } leptos_meta = { version = "0.7" } leptos_router = { version = "0.7.0" } +moonboard-parser = { workspace = true, optional = true } +rand = { version = "0.8", optional = true } +ron = { version = "0.8" } +serde = { version = "1", features = ["derive"] } +server_fn = { version = "0.7.4", features = ["cbor"] } +smart-default = "0.7.1" tokio = { version = "1", features = ["rt-multi-thread"], optional = true } tower = { version = "0.4", optional = true } tower-http = { version = "0.5", features = ["fs"], optional = true } -wasm-bindgen = "=0.2.99" -http = "1" -serde = { version = "1", features = ["derive"] } -derive_more = { version = "1", features = ["display", "error", "from"] } -clap = { version = "4.5.7", features = ["derive"] } -camino = { version = "1.1", optional = true } -type-toppings = { version = "0.2.1", features = ["result"] } tracing = { version = "0.1" } tracing-subscriber = { version = "0.3.18", features = ["env-filter"] } tracing-subscriber-wasm = "0.1.0" -ron = { version = "0.8" } -rand = { version = "0.8", optional = true } +type-toppings = { version = "0.2.1", features = ["result"] } +wasm-bindgen = "=0.2.99" web-sys = { version = "0.3.76", features = ["File", "FileList"] } -smart-default = "0.7.1" -confik = { version = "0.12", optional = true, features = ["camino"] } xdg = { version = "2.5", optional = true } +uuid = { version = "1.12", optional = true, features = ["serde", "v4"] } +redb = { version = "2.4", optional = true } +bincode = { version = "1.3", optional = true } [dev-dependencies.serde_json] version = "1" @@ -44,6 +48,9 @@ version = "1" hydrate = ["leptos/hydrate"] ssr = [ "dep:axum", + "dep:uuid", + "dep:redb", + "dep:bincode", "dep:tokio", "dep:rand", "dep:tower", diff --git a/crates/ascend/src/pages/edit_wall.rs b/crates/ascend/src/pages/edit_wall.rs index 079ea1f..ceb0fac 100644 --- a/crates/ascend/src/pages/edit_wall.rs +++ b/crates/ascend/src/pages/edit_wall.rs @@ -151,12 +151,9 @@ pub struct Image { #[server] async fn load_initial_data() -> Result, ServerFnError> { - use crate::server::state::State; - - let state = expect_context::(); - - let wall = state.persistent.with(|s| s.wall.clone()).await; - Ok(RonCodec::new(InitialData { wall })) + todo!() + // let wall = state.persistent.with(|s| s.wall.clone()).await; + // Ok(RonCodec::new(InitialData { wall })) } #[server(name = SetImage, input = Cbor)] @@ -164,25 +161,24 @@ async fn load_initial_data() -> Result, ServerFnError> { async fn set_image(hold_position: HoldPosition, image: Image) -> Result { tracing::info!("Setting image, {}, {} bytes", image.file_name, image.file_contents.len()); - use crate::server::state::State; - // TODO: Fix file extension presumption, and possibly use uuid let filename = format!("row{}_col{}.jpg", hold_position.row, hold_position.col); tokio::fs::create_dir_all("datastore/public/holds").await?; tokio::fs::write(format!("datastore/public/holds/{filename}"), image.file_contents).await?; - let state = expect_context::(); - state - .persistent - .update(|s| { - if let Some(hold) = s.wall.holds.get_mut(&hold_position) { - hold.image = Some(models::Image { filename }); - } - }) - .await?; + todo!() + // let state = expect_context::(); + // state + // .persistent + // .update(|s| { + // if let Some(hold) = s.wall.holds.get_mut(&hold_position) { + // hold.image = Some(models::Image { filename }); + // } + // }) + // .await?; - // Return updated hold - let hold = state.persistent.with(|s| s.wall.holds.get(&hold_position).cloned().unwrap()).await; + // // Return updated hold + // let hold = state.persistent.with(|s| s.wall.holds.get(&hold_position).cloned().unwrap()).await; - Ok(hold) + // Ok(hold) } diff --git a/crates/ascend/src/pages/routes.rs b/crates/ascend/src/pages/routes.rs index 94208aa..82eeced 100644 --- a/crates/ascend/src/pages/routes.rs +++ b/crates/ascend/src/pages/routes.rs @@ -2,9 +2,11 @@ use crate::codec::ron::RonCodec; use crate::components::header::HeaderItem; use crate::components::header::HeaderItems; use crate::components::header::StyledHeader; +use crate::models; use leptos::prelude::*; use serde::Deserialize; use serde::Serialize; +use std::collections::BTreeSet; use std::ops::Deref; #[component] @@ -56,31 +58,39 @@ fn Ready(data: InitialData) -> impl leptos::IntoView { } #[derive(Debug, Serialize, Deserialize, Clone)] -pub struct InitialData {} +pub struct InitialData { + problems: BTreeSet, +} #[server] async fn load_initial_data() -> Result, ServerFnError> { - // use crate::server::state::State; + todo!() // let state = expect_context::(); - // TODO: provide info on current routes set + // let problems = state + // .persistent + // .with(|s| { + // let problems = &s.problems.problems; + // problems.clone() + // }) + // .await; - Ok(RonCodec::new(InitialData {})) + // Ok(RonCodec::new(InitialData { problems })) } #[server(name = ImportFromMiniMoonboard)] #[tracing::instrument] async fn import_from_mini_moonboard() -> Result<(), ServerFnError> { use crate::server::config::Config; - use crate::server::state::State; - tracing::info!("Importing mini moonboard problems"); + todo!() + // tracing::info!("Importing mini moonboard problems"); - let config = expect_context::(); - let state = expect_context::(); + // let config = expect_context::(); + // let state = expect_context::(); - crate::server::operations::import_mini_moonboard_problems(&config, &state).await?; + // crate::server::operations::import_mini_moonboard_problems(&config, &state).await?; - // TODO: Return information about what was done - Ok(()) + // // TODO: Return information about what was done + // Ok(()) } diff --git a/crates/ascend/src/pages/wall.rs b/crates/ascend/src/pages/wall.rs index 3624947..c302493 100644 --- a/crates/ascend/src/pages/wall.rs +++ b/crates/ascend/src/pages/wall.rs @@ -80,7 +80,13 @@ fn Ready(data: InitialData) -> impl leptos::IntoView {
{cells}
-
} @@ -120,31 +126,30 @@ pub struct InitialData { #[server] async fn load_initial_data() -> Result, ServerFnError> { - use crate::server::state::State; + todo!() + // let state = expect_context::(); - let state = expect_context::(); - - let wall = state.persistent.with(|s| s.wall.clone()).await; - Ok(RonCodec::new(InitialData { wall })) + // let wall = state.persistent.with(|s| s.wall.clone()).await; + // Ok(RonCodec::new(InitialData { wall })) } #[server] async fn get_random_problem() -> Result>, ServerFnError> { - use crate::server::state::State; - use rand::seq::IteratorRandom; + todo!() + // use rand::seq::IteratorRandom; - let state = expect_context::(); + // let state = expect_context::(); - let problem = state - .persistent - .with(|s| { - let problems = &s.problems.problems; - let rng = &mut rand::thread_rng(); - problems.iter().choose(rng).cloned() - }) - .await; + // let problem = state + // .persistent + // .with(|s| { + // let problems = &s.problems.problems; + // let rng = &mut rand::thread_rng(); + // problems.iter().choose(rng).cloned() + // }) + // .await; - tracing::debug!("Returning randomized problem: {problem:?}"); + // tracing::debug!("Returning randomized problem: {problem:?}"); - Ok(RonCodec::new(problem)) + // Ok(RonCodec::new(problem)) } diff --git a/crates/ascend/src/server.rs b/crates/ascend/src/server.rs index c28e5e8..8b618d8 100644 --- a/crates/ascend/src/server.rs +++ b/crates/ascend/src/server.rs @@ -4,20 +4,18 @@ use cli::Cli; use config::Config; use confik::Configuration; use confik::EnvSource; -use persistence::Persistent; use state::PersistentState; -use state::State; use std::path::Path; -use std::path::PathBuf; +use std::sync::Arc; use tower_http::services::ServeDir; use tracing::level_filters::LevelFilter; use type_toppings::ResultExt; mod cli; pub mod config; +mod db; mod migrations; pub mod operations; -pub mod persistence; pub mod state; pub const STATE_FILE: &str = "datastore/private/state.ron"; @@ -43,12 +41,6 @@ pub async fn main() { match cli.command { Command::Serve => serve(cli).await.unwrap_or_report(), - Command::ResetState => { - let s = PersistentState::default(); - let p = Path::new(STATE_FILE); - tracing::info!("Resetting state to default: {}", p.display()); - Persistent::persist(p, &s).await.unwrap_or_report(); - } } } @@ -57,32 +49,34 @@ async fn serve(cli: Cli) -> Result<(), Error> { use crate::app::App; use crate::app::shell; use axum::Router; - use leptos::prelude::*; use leptos_axum::LeptosRoutes; use leptos_axum::generate_route_list; - migrations::run_migrations().await; + tracing::debug!("Creating DB"); + let db = Arc::new(db::create()?); + + migrations::run_migrations(&db).await.map_err(Error::Migration)?; // Setting get_configuration(None) means we'll be using cargo-leptos's env values // For deployment these variables are: // // Alternately a file can be specified such as Some("Cargo.toml") // The file would need to be included with the executable when moved to deployment - let leptos_conf_file = get_configuration(None).unwrap_or_report(); + let leptos_conf_file = leptos::config::get_configuration(None).unwrap_or_report(); let leptos_options = leptos_conf_file.leptos_options; let addr = leptos_options.site_addr; let routes = generate_route_list(App); let config = load_config(cli)?; - let server_state = load_state().await?; + tracing::debug!("Creating app router"); let app = Router::new() .leptos_routes_with_context( &leptos_options, routes, move || { - provide_context(server_state.clone()); - provide_context(config.clone()) + leptos::prelude::provide_context(Arc::clone(&db)); + leptos::prelude::provide_context(config.clone()) }, { let leptos_options = leptos_options.clone(); @@ -93,7 +87,9 @@ async fn serve(cli: Cli) -> Result<(), Error> { .fallback(leptos_axum::file_and_error_handler(shell)) .with_state(leptos_options); + tracing::debug!("Binding TCP listener"); let listener = tokio::net::TcpListener::bind(&addr).await?; + tracing::info!("Listening on http://{addr}"); axum::serve(listener, app.into_make_service()).await?; @@ -106,22 +102,6 @@ fn file_service(path: impl AsRef) -> ServeDir { ServeDir::new(path) } -#[tracing::instrument] -async fn load_state() -> Result { - tracing::info!("Loading state"); - - let p = PathBuf::from(STATE_FILE); - - let persistent = if p.try_exists()? { - Persistent::::load(&p).await? - } else { - tracing::info!("No state found at {STATE_FILE}, creating default state"); - Persistent::::new(PersistentState::default(), p) - }; - - Ok(State { persistent }) -} - fn load_config(cli: Cli) -> Result { let mut builder = config::Config::builder(); if cli @@ -139,11 +119,14 @@ fn load_config(cli: Cli) -> Result { #[display("Server crash: {_variant}")] pub enum Error { Io(std::io::Error), - Persistence(persistence::Error), + Parser(moonboard_parser::Error), #[display("Failed migration")] + #[from(ignore)] Migration(Box), Confik(confik::Error), + + Database(redb::DatabaseError), } diff --git a/crates/ascend/src/server/cli.rs b/crates/ascend/src/server/cli.rs index 4b25089..3405709 100644 --- a/crates/ascend/src/server/cli.rs +++ b/crates/ascend/src/server/cli.rs @@ -15,9 +15,6 @@ pub struct Cli { pub enum Command { #[default] Serve, - - /// Resets state, replacing it with defaults - ResetState, } fn default_config_location() -> camino::Utf8PathBuf { diff --git a/crates/ascend/src/server/db.rs b/crates/ascend/src/server/db.rs new file mode 100644 index 0000000..a82359f --- /dev/null +++ b/crates/ascend/src/server/db.rs @@ -0,0 +1,21 @@ +use redb::Database; +use redb::DatabaseError; +use std::path::PathBuf; + +mod bincode; +pub mod db_models; + +pub const DB_FILE: &str = "datastore/private/ascend.redb"; + +#[tracing::instrument(err)] +pub fn create() -> Result { + let file = PathBuf::from(DB_FILE); + + // Create parent dirs + if let Some(parent_dir) = file.parent() { + std::fs::create_dir_all(parent_dir)?; + } + + let db = Database::create(file)?; + Ok(db) +} diff --git a/crates/ascend/src/server/db/bincode.rs b/crates/ascend/src/server/db/bincode.rs new file mode 100644 index 0000000..0b3529e --- /dev/null +++ b/crates/ascend/src/server/db/bincode.rs @@ -0,0 +1,52 @@ +use redb::Value; + +/// Wrapper type to handle keys and values using bincode serialization +#[derive(Debug)] +pub struct Bincode(pub T); + +impl Value for Bincode +where + T: std::fmt::Debug + serde::Serialize + for<'a> serde::Deserialize<'a>, +{ + type SelfType<'a> + = T + where + Self: 'a; + + type AsBytes<'a> + = Vec + where + Self: 'a; + + fn fixed_width() -> Option { + None + } + + fn from_bytes<'a>(data: &'a [u8]) -> Self::SelfType<'a> + where + Self: 'a, + { + bincode::deserialize(data).unwrap() + } + + fn as_bytes<'a, 'b: 'a>(value: &'a Self::SelfType<'b>) -> Self::AsBytes<'a> + where + Self: 'a, + Self: 'b, + { + bincode::serialize(value).unwrap() + } + + fn type_name() -> redb::TypeName { + redb::TypeName::new(&format!("Bincode<{}>", std::any::type_name::())) + } +} + +impl redb::Key for Bincode +where + T: std::fmt::Debug + serde::Serialize + serde::de::DeserializeOwned + Ord, +{ + fn compare(data1: &[u8], data2: &[u8]) -> std::cmp::Ordering { + Self::from_bytes(data1).cmp(&Self::from_bytes(data2)) + } +} diff --git a/crates/ascend/src/server/db/db_models.rs b/crates/ascend/src/server/db/db_models.rs new file mode 100644 index 0000000..8f9c879 --- /dev/null +++ b/crates/ascend/src/server/db/db_models.rs @@ -0,0 +1,105 @@ +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> = 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> = TableDefinition::new("root"); + pub const TABLE_WALLS: TableDefinition, Bincode> = TableDefinition::new("walls"); + + #[derive(Serialize, Deserialize, Debug)] + pub struct Root { + pub walls: BTreeSet, + } + + #[derive(Serialize, Deserialize, Debug)] + pub struct Wall { + pub uid: WallId, + pub rows: u64, + pub cols: u64, + pub holds: BTreeMap, + } + + #[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, + 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, + } + + #[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> = TableDefinition::new("root"); +} diff --git a/crates/ascend/src/server/migrations.rs b/crates/ascend/src/server/migrations.rs index 7c8e26f..dfc2e46 100644 --- a/crates/ascend/src/server/migrations.rs +++ b/crates/ascend/src/server/migrations.rs @@ -1,20 +1,140 @@ -use crate::server::STATE_FILE; +use super::state::PersistentState; +use leptos::prelude::StorageAccess; +use redb::Database; +use redb::ReadableTable; +use redb::ReadableTableMetadata; +use std::collections::BTreeSet; use std::path::PathBuf; use type_toppings::ResultExt; #[tracing::instrument] -pub async fn run_migrations() { - migrate_state_file().await; +pub async fn run_migrations(db: &Database) -> Result<(), Box> { + migrate_state_file().await?; + migrate_from_ron_to_redb(db).await?; + migrate_to_v2(db).await?; + Ok(()) } /// State file moved to datastore/private -#[tracing::instrument] -async fn migrate_state_file() { +#[tracing::instrument(err)] +async fn migrate_state_file() -> Result<(), Box> { let m = PathBuf::from("state.ron"); if m.try_exists().expect_or_report_with(|| format!("Failed to read {}", m.display())) { - tracing::warn!("MIGRATING STATE FILE"); - let p = PathBuf::from(STATE_FILE); - tokio::fs::create_dir_all(p.parent().unwrap()).await.unwrap_or_report(); - tokio::fs::rename(m, &p).await.unwrap_or_report(); + 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 +#[tracing::instrument(err)] +async fn migrate_from_ron_to_redb(db: &Database) -> Result<(), Box> { + 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); + + if ron_state_file_path + .try_exists() + .expect_or_report_with(|| format!("Failed to read {}", ron_state_file_path.display())) + { + tracing::warn!("MIGRATING"); + + let ron_state: PersistentState = { + let content = tokio::fs::read_to_string(&ron_state_file_path).await?; + ron::from_str(&content)? + }; + + let write_txn = db.begin_write()?; + { + let mut version_table = write_txn.open_table(TABLE_VERSION)?; + assert!(version_table.is_empty()?); + version_table.insert((), Version { version: 1 }); + + let mut root_table = write_txn.open_table(v1::TABLE_ROOT)?; + assert!(root_table.is_empty()?); + + let root = v1::Root { + version: ron_state.version, + wall: ron_state.wall, + problems: ron_state.problems, + }; + + root_table.insert((), root)?; + } + write_txn.commit()?; + + tracing::info!("Removing ron state"); + tokio::fs::remove_file(ron_state_file_path).await?; + } + + Ok(()) +} + +#[tracing::instrument(err)] +async fn migrate_to_v2(db: &Database) -> Result<(), Box> { + use super::db::db_models::TABLE_VERSION; + use super::db::db_models::Version; + use super::db::db_models::v1; + use super::db::db_models::v2; + + let txn = db.begin_write()?; + { + let mut version_table = txn.open_table(TABLE_VERSION)?; + let version = version_table.get(())?.unwrap().value().version; + if version == 1 { + tracing::warn!("MIGRATING"); + version_table.insert((), Version { version: 2 })?; + + let root_table_v1 = txn.open_table(v1::TABLE_ROOT)?; + let root_v1 = root_table_v1.get(())?.unwrap().value(); + txn.delete_table(v1::TABLE_ROOT)?; + + let v1::Root { version: _, wall, problems } = root_v1; + + let mut walls = BTreeSet::new(); + let wall_uid = v2::WallId(uuid::Uuid::new_v4()); + let holds = wall + .holds + .into_iter() + .map(|(hold_position, hold)| { + ( + v2::HoldPosition { + row: hold_position.row, + col: hold_position.col, + }, + v2::Hold { + position: v2::HoldPosition { + row: hold.position.row, + col: hold.position.col, + }, + image: hold.image.map(|i| v2::Image { filename: i.filename }), + }, + ) + }) + .collect(); + + let wall_v2 = v2::Wall { + uid: wall_uid, + rows: wall.rows, + cols: wall.cols, + holds, + }; + + walls.insert(wall_v2.uid); + let root_v2 = v2::Root { walls }; + + let mut root_table_v2 = txn.open_table(v2::TABLE_ROOT)?; + root_table_v2.insert((), root_v2)?; + + let mut walls_table = txn.open_table(v2::TABLE_WALLS)?; + walls_table.insert(wall_v2.uid, wall_v2)?; + } + } + txn.commit()?; + + Ok(()) } diff --git a/crates/ascend/src/server/operations.rs b/crates/ascend/src/server/operations.rs index 54063ec..619374d 100644 --- a/crates/ascend/src/server/operations.rs +++ b/crates/ascend/src/server/operations.rs @@ -1,15 +1,16 @@ //! Server lib module to host re-usable server operations. +use crate::models; use crate::models::HoldPosition; use crate::models::HoldRole; -use crate::models::Problem; use crate::server::config::Config; -use crate::server::persistence; -use crate::server::state::State; +use redb::Database; use std::collections::BTreeMap; -#[tracing::instrument(skip(state))] -pub(crate) async fn import_mini_moonboard_problems(config: &Config, state: &State) -> Result<(), Error> { +#[tracing::instrument] +pub(crate) async fn import_mini_moonboard_problems(config: &Config, db: &Database) -> Result<(), Error> { + use moonboard_parser::mini_moonboard; + let mut problems = Vec::new(); let file_name = "problems Mini MoonBoard 2020 40.json"; @@ -17,7 +18,9 @@ pub(crate) async fn import_mini_moonboard_problems(config: &Config, state: &Stat tracing::info!("Parsing mini moonboard problems from {file_path}"); - let mini_moonboard = moonboard_parser::mini_moonboard::parse(file_path.as_std_path()).await?; + let set_by = "mini-mb-2020-parser"; + + let mini_moonboard = mini_moonboard::parse(file_path.as_std_path()).await?; for problem in mini_moonboard.problems { let mut holds = BTreeMap::::new(); for mv in problem.moves { @@ -33,8 +36,22 @@ pub(crate) async fn import_mini_moonboard_problems(config: &Config, state: &Stat }; holds.insert(hold_position, role); } - let route = Problem { holds }; - problems.push(route); + + // TODO: + + // let name = problem.name; + + // let method = match problem.method { + // 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); + // problems.push(problem); + + let problem = models::Problem { holds }; + problems.push(problem); } state @@ -50,5 +67,4 @@ pub(crate) async fn import_mini_moonboard_problems(config: &Config, state: &Stat #[derive(Debug, derive_more::Error, derive_more::Display, derive_more::From)] pub enum Error { Parser(moonboard_parser::Error), - Persistence(persistence::Error), } diff --git a/crates/ascend/src/server/persistence.rs b/crates/ascend/src/server/persistence.rs deleted file mode 100644 index 6104857..0000000 --- a/crates/ascend/src/server/persistence.rs +++ /dev/null @@ -1,134 +0,0 @@ -use serde::Serialize; -use serde::de::DeserializeOwned; -use std::path::Path; -use std::path::PathBuf; -use std::sync::Arc; -use tokio::sync::Mutex; - -#[derive(Debug, Clone)] -pub struct Persistent { - state: Arc>, - file_path: PathBuf, -} - -impl Persistent { - #[tracing::instrument(skip(state))] - pub fn new(state: T, file_path: PathBuf) -> Self { - Self { - state: Arc::new(Mutex::new(state)), - file_path, - } - } - - /// Instantiates state from file system - #[tracing::instrument] - pub async fn load(file_path: &Path) -> Result - where - T: DeserializeOwned, - { - let content = tokio::fs::read_to_string(&file_path).await.map_err(|source| Error::Read { - file_path: file_path.to_owned(), - source, - })?; - - let t = ron::from_str(&content).map_err(|source| Error::Deserialize { - file_path: file_path.to_owned(), - source, - })?; - - let persistent = Self { - state: Arc::new(Mutex::new(t)), - file_path: file_path.to_owned(), - }; - - Ok(persistent) - } - - // TODO: This is pretty poor - clones the entire state on access - /// Returns state - #[tracing::instrument(skip(self))] - pub async fn get(&self) -> T - where - T: Clone, - { - let state = self.state.lock().await; - state.clone() - } - - /// Returns state passed through given function - #[tracing::instrument(skip_all)] - pub async fn with(&self, f: F) -> R - where - F: FnOnce(&T) -> R, - { - let state = self.state.lock().await; - f(&state) - } - - /// Updates and persists state - #[tracing::instrument(skip_all)] - pub async fn update(&self, f: F) -> Result<(), Error> - where - F: FnOnce(&mut T), - T: Serialize, - { - let mut state = self.state.lock().await; - f(&mut state); - Self::persist(&self.file_path, &state).await?; - Ok(()) - } - - /// Sets and persists state - #[tracing::instrument(skip_all)] - pub async fn set(&self, new_state: T) -> Result<(), Error> - where - T: Serialize, - { - self.update(move |state| { - *state = new_state; - }) - .await - } - - /// Persist. - /// - /// Implicitly called by `set` and `update`. - #[tracing::instrument(skip_all, err)] - pub async fn persist(file_path: &Path, state: &T) -> Result<(), Error> - where - T: Serialize, - { - tracing::debug!("Persisting state"); - let serialized = ron::ser::to_string_pretty(state, ron::ser::PrettyConfig::default()).map_err(|source| Error::Serialize { source })?; - if let Some(parent) = file_path.parent() { - tokio::fs::create_dir_all(parent).await.map_err(|source| Error::CreateDir { - source, - dir: parent.to_owned(), - })?; - } - tokio::fs::write(file_path, serialized).await.map_err(|source| Error::Write { - file_path: file_path.to_owned(), - source, - })?; - Ok(()) - } -} - -#[derive(Debug, derive_more::Error, derive_more::Display)] -#[display("Persistent state error: {_variant}")] -pub enum Error { - #[display("Failed to read file: {}", file_path.display())] - Read { file_path: PathBuf, source: std::io::Error }, - - #[display("Failed to deserialize state from file: {}", file_path.display())] - Deserialize { file_path: PathBuf, source: ron::error::SpannedError }, - - #[display("Failed to serialize state")] - Serialize { source: ron::Error }, - - #[display("Failed to write file: {}", file_path.display())] - Write { file_path: PathBuf, source: std::io::Error }, - - #[display("Failed to create directory: {}", dir.display())] - CreateDir { dir: PathBuf, source: std::io::Error }, -} diff --git a/crates/ascend/src/server/state.rs b/crates/ascend/src/server/state.rs index cdf6def..eeb92b7 100644 --- a/crates/ascend/src/server/state.rs +++ b/crates/ascend/src/server/state.rs @@ -2,7 +2,6 @@ const STATE_VERSION: u64 = 1; -use super::persistence::Persistent; use crate::models; use crate::models::Wall; use serde::Deserialize; @@ -10,11 +9,6 @@ use serde::Serialize; use smart_default::SmartDefault; use std::collections::BTreeSet; -#[derive(Clone, Debug)] -pub struct State { - pub persistent: Persistent, -} - #[derive(Serialize, Deserialize, Clone, Debug, SmartDefault)] pub struct PersistentState { /// State schema version