wip: db migration

This commit is contained in:
2025-01-29 01:08:14 +01:00
parent 9d02ae1c6b
commit d6972e604e
14 changed files with 536 additions and 258 deletions

108
Cargo.lock generated
View File

@@ -26,6 +26,21 @@ dependencies = [
"memchr", "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]] [[package]]
name = "anstream" name = "anstream"
version = "0.6.18" version = "0.6.18"
@@ -105,7 +120,9 @@ name = "ascend"
version = "0.0.0" version = "0.0.0"
dependencies = [ dependencies = [
"axum", "axum",
"bincode",
"camino", "camino",
"chrono",
"clap", "clap",
"confik", "confik",
"console_error_panic_hook", "console_error_panic_hook",
@@ -117,6 +134,7 @@ dependencies = [
"leptos_router", "leptos_router",
"moonboard-parser", "moonboard-parser",
"rand", "rand",
"redb",
"ron", "ron",
"serde", "serde",
"serde_json", "serde_json",
@@ -129,6 +147,7 @@ dependencies = [
"tracing-subscriber", "tracing-subscriber",
"tracing-subscriber-wasm", "tracing-subscriber-wasm",
"type-toppings", "type-toppings",
"uuid",
"wasm-bindgen", "wasm-bindgen",
"web-sys", "web-sys",
"xdg", "xdg",
@@ -320,12 +339,36 @@ dependencies = [
"serde", "serde",
] ]
[[package]]
name = "cc"
version = "1.2.10"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "13208fcbb66eaeffe09b99fffbe1af420f00a7b35aa99ad683dfc1aa76145229"
dependencies = [
"shlex",
]
[[package]] [[package]]
name = "cfg-if" name = "cfg-if"
version = "1.0.0" version = "1.0.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd" 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]] [[package]]
name = "ciborium" name = "ciborium"
version = "0.2.2" version = "0.2.2"
@@ -510,6 +553,12 @@ dependencies = [
"unicode-segmentation", "unicode-segmentation",
] ]
[[package]]
name = "core-foundation-sys"
version = "0.8.7"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "773648b94d0e5d620f64f280777445740e61fe701025087ec8b57f45c791888b"
[[package]] [[package]]
name = "crossbeam-utils" name = "crossbeam-utils"
version = "0.8.21" version = "0.8.21"
@@ -1174,6 +1223,29 @@ dependencies = [
"tower-service", "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]] [[package]]
name = "icu_collections" name = "icu_collections"
version = "1.5.0" version = "1.5.0"
@@ -1776,6 +1848,15 @@ dependencies = [
"winapi", "winapi",
] ]
[[package]]
name = "num-traits"
version = "0.2.19"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "071dfc062690e90b734c0b2273ce72ad0ffa95f0c74596bc250dcfd960262841"
dependencies = [
"autocfg",
]
[[package]] [[package]]
name = "num_cpus" name = "num_cpus"
version = "1.16.0" version = "1.16.0"
@@ -2087,6 +2168,15 @@ dependencies = [
"syn", "syn",
] ]
[[package]]
name = "redb"
version = "2.4.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ea0a72cd7140de9fc3e318823b883abf819c20d478ec89ce880466dc2ef263c6"
dependencies = [
"libc",
]
[[package]] [[package]]
name = "redox_syscall" name = "redox_syscall"
version = "0.5.8" version = "0.5.8"
@@ -2370,6 +2460,12 @@ dependencies = [
"lazy_static", "lazy_static",
] ]
[[package]]
name = "shlex"
version = "1.3.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0fda2ff0d084019ba4d7c6f371c95d8fd75ce3524c3cb8fb653a3023f6323e64"
[[package]] [[package]]
name = "slab" name = "slab"
version = "0.4.9" version = "0.4.9"
@@ -2906,6 +3002,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b3758f5e68192bb96cc8f9b7e2c2cfdabb435499a28499a42f8f984092adad4b" checksum = "b3758f5e68192bb96cc8f9b7e2c2cfdabb435499a28499a42f8f984092adad4b"
dependencies = [ dependencies = [
"getrandom", "getrandom",
"serde",
] ]
[[package]] [[package]]
@@ -3048,7 +3145,7 @@ version = "0.1.9"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "cf221c93e13a30d793f7645a0e7762c55d169dbb0a49671918a2319d289b10bb" checksum = "cf221c93e13a30d793f7645a0e7762c55d169dbb0a49671918a2319d289b10bb"
dependencies = [ dependencies = [
"windows-sys 0.59.0", "windows-sys 0.52.0",
] ]
[[package]] [[package]]
@@ -3057,6 +3154,15 @@ version = "0.4.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f" 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]] [[package]]
name = "windows-sys" name = "windows-sys"
version = "0.52.0" version = "0.52.0"

View File

@@ -9,33 +9,37 @@ publish = false
crate-type = ["cdylib", "rlib"] crate-type = ["cdylib", "rlib"]
[dependencies] [dependencies]
moonboard-parser = { workspace = true, optional = true }
axum = { version = "0.7", 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" console_error_panic_hook = "0.1"
derive_more = { version = "1", features = ["display", "error", "from"] }
http = "1"
leptos = { version = "0.7.4", features = ["tracing"] } leptos = { version = "0.7.4", features = ["tracing"] }
server_fn = { version = "0.7.4", features = ["cbor"] }
leptos_axum = { version = "0.7", optional = true } leptos_axum = { version = "0.7", optional = true }
leptos_meta = { version = "0.7" } leptos_meta = { version = "0.7" }
leptos_router = { version = "0.7.0" } 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 } tokio = { version = "1", features = ["rt-multi-thread"], optional = true }
tower = { version = "0.4", optional = true } tower = { version = "0.4", optional = true }
tower-http = { version = "0.5", features = ["fs"], 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 = { version = "0.1" }
tracing-subscriber = { version = "0.3.18", features = ["env-filter"] } tracing-subscriber = { version = "0.3.18", features = ["env-filter"] }
tracing-subscriber-wasm = "0.1.0" tracing-subscriber-wasm = "0.1.0"
ron = { version = "0.8" } type-toppings = { version = "0.2.1", features = ["result"] }
rand = { version = "0.8", optional = true } wasm-bindgen = "=0.2.99"
web-sys = { version = "0.3.76", features = ["File", "FileList"] } 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 } 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] [dev-dependencies.serde_json]
version = "1" version = "1"
@@ -44,6 +48,9 @@ version = "1"
hydrate = ["leptos/hydrate"] hydrate = ["leptos/hydrate"]
ssr = [ ssr = [
"dep:axum", "dep:axum",
"dep:uuid",
"dep:redb",
"dep:bincode",
"dep:tokio", "dep:tokio",
"dep:rand", "dep:rand",
"dep:tower", "dep:tower",

View File

@@ -151,12 +151,9 @@ pub struct Image {
#[server] #[server]
async fn load_initial_data() -> Result<RonCodec<InitialData>, ServerFnError> { async fn load_initial_data() -> Result<RonCodec<InitialData>, ServerFnError> {
use crate::server::state::State; todo!()
// let wall = state.persistent.with(|s| s.wall.clone()).await;
let state = expect_context::<State>(); // Ok(RonCodec::new(InitialData { wall }))
let wall = state.persistent.with(|s| s.wall.clone()).await;
Ok(RonCodec::new(InitialData { wall }))
} }
#[server(name = SetImage, input = Cbor)] #[server(name = SetImage, input = Cbor)]
@@ -164,25 +161,24 @@ async fn load_initial_data() -> Result<RonCodec<InitialData>, ServerFnError> {
async fn set_image(hold_position: HoldPosition, image: Image) -> Result<models::Hold, ServerFnError> { async fn set_image(hold_position: HoldPosition, image: Image) -> Result<models::Hold, ServerFnError> {
tracing::info!("Setting image, {}, {} bytes", image.file_name, image.file_contents.len()); 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 // TODO: Fix file extension presumption, and possibly use uuid
let filename = format!("row{}_col{}.jpg", hold_position.row, hold_position.col); let filename = format!("row{}_col{}.jpg", hold_position.row, hold_position.col);
tokio::fs::create_dir_all("datastore/public/holds").await?; tokio::fs::create_dir_all("datastore/public/holds").await?;
tokio::fs::write(format!("datastore/public/holds/{filename}"), image.file_contents).await?; tokio::fs::write(format!("datastore/public/holds/{filename}"), image.file_contents).await?;
let state = expect_context::<State>(); todo!()
state // let state = expect_context::<State>();
.persistent // state
.update(|s| { // .persistent
if let Some(hold) = s.wall.holds.get_mut(&hold_position) { // .update(|s| {
hold.image = Some(models::Image { filename }); // if let Some(hold) = s.wall.holds.get_mut(&hold_position) {
} // hold.image = Some(models::Image { filename });
}) // }
.await?; // })
// .await?;
// Return updated hold // // Return updated hold
let hold = state.persistent.with(|s| s.wall.holds.get(&hold_position).cloned().unwrap()).await; // let hold = state.persistent.with(|s| s.wall.holds.get(&hold_position).cloned().unwrap()).await;
Ok(hold) // Ok(hold)
} }

View File

@@ -2,9 +2,11 @@ use crate::codec::ron::RonCodec;
use crate::components::header::HeaderItem; use crate::components::header::HeaderItem;
use crate::components::header::HeaderItems; use crate::components::header::HeaderItems;
use crate::components::header::StyledHeader; use crate::components::header::StyledHeader;
use crate::models;
use leptos::prelude::*; use leptos::prelude::*;
use serde::Deserialize; use serde::Deserialize;
use serde::Serialize; use serde::Serialize;
use std::collections::BTreeSet;
use std::ops::Deref; use std::ops::Deref;
#[component] #[component]
@@ -56,31 +58,39 @@ fn Ready(data: InitialData) -> impl leptos::IntoView {
} }
#[derive(Debug, Serialize, Deserialize, Clone)] #[derive(Debug, Serialize, Deserialize, Clone)]
pub struct InitialData {} pub struct InitialData {
problems: BTreeSet<models::Problem>,
}
#[server] #[server]
async fn load_initial_data() -> Result<RonCodec<InitialData>, ServerFnError> { async fn load_initial_data() -> Result<RonCodec<InitialData>, ServerFnError> {
// use crate::server::state::State; todo!()
// let state = expect_context::<State>(); // let state = expect_context::<State>();
// 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)] #[server(name = ImportFromMiniMoonboard)]
#[tracing::instrument] #[tracing::instrument]
async fn import_from_mini_moonboard() -> Result<(), ServerFnError> { async fn import_from_mini_moonboard() -> Result<(), ServerFnError> {
use crate::server::config::Config; 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::<Config>(); // let config = expect_context::<Config>();
let state = expect_context::<State>(); // let state = expect_context::<State>();
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 // // TODO: Return information about what was done
Ok(()) // Ok(())
} }

View File

@@ -80,7 +80,13 @@ fn Ready(data: InitialData) -> impl leptos::IntoView {
<div style="max-height: 90vh; max-width: 90vh;" class=move || { grid_classes.clone() }>{cells}</div> <div style="max-height: 90vh; max-width: 90vh;" class=move || { grid_classes.clone() }>{cells}</div>
<div> <div>
<Button onclick=move |_| problem_fetcher.mark_dirty() text="Next problem ➤" /> <div>
// TODO:
// <p>{current_problem.read().as_ref().map(|p| p.name.clone())}</p>
// <p>{current_problem.read().as_ref().map(|p| p.set_by.clone())}</p>
</div>
<Button onclick=move |_| problem_fetcher.mark_dirty() text="➤ Next problem" />
</ div> </ div>
</div> </div>
} }
@@ -120,31 +126,30 @@ pub struct InitialData {
#[server] #[server]
async fn load_initial_data() -> Result<RonCodec<InitialData>, ServerFnError> { async fn load_initial_data() -> Result<RonCodec<InitialData>, ServerFnError> {
use crate::server::state::State; todo!()
// let state = expect_context::<State>();
let state = expect_context::<State>(); // 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] #[server]
async fn get_random_problem() -> Result<RonCodec<Option<models::Problem>>, ServerFnError> { async fn get_random_problem() -> Result<RonCodec<Option<models::Problem>>, ServerFnError> {
use crate::server::state::State; todo!()
use rand::seq::IteratorRandom; // use rand::seq::IteratorRandom;
let state = expect_context::<State>(); // let state = expect_context::<State>();
let problem = state // let problem = state
.persistent // .persistent
.with(|s| { // .with(|s| {
let problems = &s.problems.problems; // let problems = &s.problems.problems;
let rng = &mut rand::thread_rng(); // let rng = &mut rand::thread_rng();
problems.iter().choose(rng).cloned() // problems.iter().choose(rng).cloned()
}) // })
.await; // .await;
tracing::debug!("Returning randomized problem: {problem:?}"); // tracing::debug!("Returning randomized problem: {problem:?}");
Ok(RonCodec::new(problem)) // Ok(RonCodec::new(problem))
} }

View File

@@ -4,20 +4,18 @@ use cli::Cli;
use config::Config; use config::Config;
use confik::Configuration; use confik::Configuration;
use confik::EnvSource; use confik::EnvSource;
use persistence::Persistent;
use state::PersistentState; use state::PersistentState;
use state::State;
use std::path::Path; use std::path::Path;
use std::path::PathBuf; use std::sync::Arc;
use tower_http::services::ServeDir; use tower_http::services::ServeDir;
use tracing::level_filters::LevelFilter; use tracing::level_filters::LevelFilter;
use type_toppings::ResultExt; use type_toppings::ResultExt;
mod cli; mod cli;
pub mod config; pub mod config;
mod db;
mod migrations; mod migrations;
pub mod operations; pub mod operations;
pub mod persistence;
pub mod state; pub mod state;
pub const STATE_FILE: &str = "datastore/private/state.ron"; pub const STATE_FILE: &str = "datastore/private/state.ron";
@@ -43,12 +41,6 @@ pub async fn main() {
match cli.command { match cli.command {
Command::Serve => serve(cli).await.unwrap_or_report(), 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::App;
use crate::app::shell; use crate::app::shell;
use axum::Router; use axum::Router;
use leptos::prelude::*;
use leptos_axum::LeptosRoutes; use leptos_axum::LeptosRoutes;
use leptos_axum::generate_route_list; 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 // Setting get_configuration(None) means we'll be using cargo-leptos's env values
// For deployment these variables are: // For deployment these variables are:
// <https://github.com/leptos-rs/start-axum#executing-a-server-on-a-remote-machine-without-the-toolchain> // <https://github.com/leptos-rs/start-axum#executing-a-server-on-a-remote-machine-without-the-toolchain>
// Alternately a file can be specified such as Some("Cargo.toml") // 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 // 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 leptos_options = leptos_conf_file.leptos_options;
let addr = leptos_options.site_addr; let addr = leptos_options.site_addr;
let routes = generate_route_list(App); let routes = generate_route_list(App);
let config = load_config(cli)?; let config = load_config(cli)?;
let server_state = load_state().await?;
tracing::debug!("Creating app router");
let app = Router::new() let app = Router::new()
.leptos_routes_with_context( .leptos_routes_with_context(
&leptos_options, &leptos_options,
routes, routes,
move || { move || {
provide_context(server_state.clone()); leptos::prelude::provide_context(Arc::clone(&db));
provide_context(config.clone()) leptos::prelude::provide_context(config.clone())
}, },
{ {
let leptos_options = leptos_options.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)) .fallback(leptos_axum::file_and_error_handler(shell))
.with_state(leptos_options); .with_state(leptos_options);
tracing::debug!("Binding TCP listener");
let listener = tokio::net::TcpListener::bind(&addr).await?; let listener = tokio::net::TcpListener::bind(&addr).await?;
tracing::info!("Listening on http://{addr}"); tracing::info!("Listening on http://{addr}");
axum::serve(listener, app.into_make_service()).await?; axum::serve(listener, app.into_make_service()).await?;
@@ -106,22 +102,6 @@ fn file_service(path: impl AsRef<Path>) -> ServeDir {
ServeDir::new(path) ServeDir::new(path)
} }
#[tracing::instrument]
async fn load_state() -> Result<State, Error> {
tracing::info!("Loading state");
let p = PathBuf::from(STATE_FILE);
let persistent = if p.try_exists()? {
Persistent::<PersistentState>::load(&p).await?
} else {
tracing::info!("No state found at {STATE_FILE}, creating default state");
Persistent::<PersistentState>::new(PersistentState::default(), p)
};
Ok(State { persistent })
}
fn load_config(cli: Cli) -> Result<Config, Error> { fn load_config(cli: Cli) -> Result<Config, Error> {
let mut builder = config::Config::builder(); let mut builder = config::Config::builder();
if cli if cli
@@ -139,11 +119,14 @@ fn load_config(cli: Cli) -> Result<Config, Error> {
#[display("Server crash: {_variant}")] #[display("Server crash: {_variant}")]
pub enum Error { pub enum Error {
Io(std::io::Error), Io(std::io::Error),
Persistence(persistence::Error),
Parser(moonboard_parser::Error), Parser(moonboard_parser::Error),
#[display("Failed migration")] #[display("Failed migration")]
#[from(ignore)]
Migration(Box<dyn std::error::Error>), Migration(Box<dyn std::error::Error>),
Confik(confik::Error), Confik(confik::Error),
Database(redb::DatabaseError),
} }

View File

@@ -15,9 +15,6 @@ pub struct Cli {
pub enum Command { pub enum Command {
#[default] #[default]
Serve, Serve,
/// Resets state, replacing it with defaults
ResetState,
} }
fn default_config_location() -> camino::Utf8PathBuf { fn default_config_location() -> camino::Utf8PathBuf {

View File

@@ -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<Database, DatabaseError> {
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)
}

View File

@@ -0,0 +1,52 @@
use redb::Value;
/// Wrapper type to handle keys and values using bincode serialization
#[derive(Debug)]
pub struct Bincode<T>(pub T);
impl<T> Value for Bincode<T>
where
T: std::fmt::Debug + serde::Serialize + for<'a> serde::Deserialize<'a>,
{
type SelfType<'a>
= T
where
Self: 'a;
type AsBytes<'a>
= Vec<u8>
where
Self: 'a;
fn fixed_width() -> Option<usize> {
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::<T>()))
}
}
impl<T> redb::Key for Bincode<T>
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))
}
}

View File

@@ -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<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,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 std::path::PathBuf;
use type_toppings::ResultExt; use type_toppings::ResultExt;
#[tracing::instrument] #[tracing::instrument]
pub async fn run_migrations() { pub async fn run_migrations(db: &Database) -> Result<(), Box<dyn std::error::Error>> {
migrate_state_file().await; migrate_state_file().await?;
migrate_from_ron_to_redb(db).await?;
migrate_to_v2(db).await?;
Ok(())
} }
/// State file moved to datastore/private /// State file moved to datastore/private
#[tracing::instrument] #[tracing::instrument(err)]
async fn migrate_state_file() { async fn migrate_state_file() -> Result<(), Box<dyn std::error::Error>> {
let m = PathBuf::from("state.ron"); let m = PathBuf::from("state.ron");
if m.try_exists().expect_or_report_with(|| format!("Failed to read {}", m.display())) { if m.try_exists().expect_or_report_with(|| format!("Failed to read {}", m.display())) {
tracing::warn!("MIGRATING STATE FILE"); tracing::warn!("MIGRATING");
let p = PathBuf::from(STATE_FILE); let p = PathBuf::from(super::STATE_FILE);
tokio::fs::create_dir_all(p.parent().unwrap()).await.unwrap_or_report(); tokio::fs::create_dir_all(p.parent().unwrap()).await?;
tokio::fs::rename(m, &p).await.unwrap_or_report(); 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<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);
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<dyn std::error::Error>> {
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(())
}

View File

@@ -1,15 +1,16 @@
//! Server lib module to host re-usable server operations. //! Server lib module to host re-usable server operations.
use crate::models;
use crate::models::HoldPosition; use crate::models::HoldPosition;
use crate::models::HoldRole; use crate::models::HoldRole;
use crate::models::Problem;
use crate::server::config::Config; use crate::server::config::Config;
use crate::server::persistence; use redb::Database;
use crate::server::state::State;
use std::collections::BTreeMap; use std::collections::BTreeMap;
#[tracing::instrument(skip(state))] #[tracing::instrument]
pub(crate) async fn import_mini_moonboard_problems(config: &Config, state: &State) -> Result<(), Error> { 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 mut problems = Vec::new();
let file_name = "problems Mini MoonBoard 2020 40.json"; 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}"); 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 { for 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 problem.moves {
@@ -33,8 +36,22 @@ pub(crate) async fn import_mini_moonboard_problems(config: &Config, state: &Stat
}; };
holds.insert(hold_position, role); 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 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)] #[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),
Persistence(persistence::Error),
} }

View File

@@ -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<T> {
state: Arc<Mutex<T>>,
file_path: PathBuf,
}
impl<T> Persistent<T> {
#[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<Self, Error>
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<F, R>(&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<F>(&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 },
}

View File

@@ -2,7 +2,6 @@
const STATE_VERSION: u64 = 1; const STATE_VERSION: u64 = 1;
use super::persistence::Persistent;
use crate::models; use crate::models;
use crate::models::Wall; use crate::models::Wall;
use serde::Deserialize; use serde::Deserialize;
@@ -10,11 +9,6 @@ use serde::Serialize;
use smart_default::SmartDefault; use smart_default::SmartDefault;
use std::collections::BTreeSet; use std::collections::BTreeSet;
#[derive(Clone, Debug)]
pub struct State {
pub persistent: Persistent<PersistentState>,
}
#[derive(Serialize, Deserialize, Clone, Debug, SmartDefault)] #[derive(Serialize, Deserialize, Clone, Debug, SmartDefault)]
pub struct PersistentState { pub struct PersistentState {
/// State schema version /// State schema version