diff --git a/crates/ascend/src/server.rs b/crates/ascend/src/server.rs index a870fab..79e4efd 100644 --- a/crates/ascend/src/server.rs +++ b/crates/ascend/src/server.rs @@ -15,8 +15,6 @@ pub mod db; mod migrations; pub mod operations; -pub const STATE_FILE: &str = "datastore/private/state.ron"; - #[tracing::instrument] pub async fn main() { use crate::server::cli::Cli; @@ -50,8 +48,9 @@ async fn serve(cli: Cli) -> Result<(), Error> { use leptos_axum::generate_route_list; tracing::debug!("Creating DB"); - let db = db::Database::create()?; + let db = db::Database::create().map_err(db::DatabaseOperationError::from)?; + db::init_at_current_version(&db).await?; migrations::run_migrations(&db).await.map_err(Error::Migration)?; // Setting get_configuration(None) means we'll be using cargo-leptos's env values @@ -125,5 +124,5 @@ pub enum Error { Confik(confik::Error), - Database(redb::Error), + Database(db::DatabaseOperationError), } diff --git a/crates/ascend/src/server/db.rs b/crates/ascend/src/server/db.rs index ddc03b8..0332361 100644 --- a/crates/ascend/src/server/db.rs +++ b/crates/ascend/src/server/db.rs @@ -1,9 +1,12 @@ use bincode::Bincode; use redb::ReadTransaction; +use redb::ReadableTable; +use redb::ReadableTableMetadata; use redb::TableDefinition; use redb::WriteTransaction; use serde::Deserialize; use serde::Serialize; +use std::collections::BTreeSet; use std::path::PathBuf; use std::sync::Arc; @@ -83,6 +86,46 @@ impl Version { } } +#[tracing::instrument(skip_all, err)] +pub async fn init_at_current_version(db: &Database) -> Result<(), DatabaseOperationError> { + db.write(|txn| { + let mut version_table = txn.open_table(TABLE_VERSION)?; + let is_missing_version = version_table.get(())?.is_none(); + if is_missing_version { + let v = Version::current(); + tracing::warn!("INITIALIZING DATABASE AT VERSION {v}"); + version_table.insert((), v)?; + + // Root table + { + let mut table = txn.open_table(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(current::TABLE_WALLS)?; + assert!(table.is_empty()?); + } + + // Problems table + { + // Opening the table creates the table + let table = txn.open_table(current::TABLE_PROBLEMS)?; + assert!(table.is_empty()?); + } + } + + Ok(()) + }) + .await?; + + Ok(()) +} + +use crate::models; pub use v2 as current; pub mod v2 { diff --git a/crates/ascend/src/server/migrations.rs b/crates/ascend/src/server/migrations.rs index 5ddec99..b97926f 100644 --- a/crates/ascend/src/server/migrations.rs +++ b/crates/ascend/src/server/migrations.rs @@ -1,218 +1,6 @@ -use super::db; use super::db::Database; -use crate::models; -use image::ImageDecoder; -use redb::ReadableTable; -use redb::ReadableTableMetadata; -use std::collections::BTreeMap; -use std::collections::BTreeSet; -use std::path::Path; -use std::path::PathBuf; -use type_toppings::ResultExt; #[tracing::instrument(skip_all, err)] -pub async fn run_migrations(db: &Database) -> Result<(), Box> { - migrate_from_ron_to_redb(db).await?; - init_at_current_version(db).await?; - migrate_to_v2(db).await?; - Ok(()) -} - -/// Use redb DB instead of Ron state file -#[tracing::instrument(skip_all, err)] -async fn migrate_from_ron_to_redb(db: &Database) -> Result<(), Box> { - 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: models::v1::PersistentState = { - let content = tokio::fs::read_to_string(&ron_state_file_path).await?; - ron::from_str(&content)? - }; - - db.write(|txn| { - let mut version_table = txn.open_table(db::TABLE_VERSION)?; - assert!(version_table.is_empty()?); - version_table.insert((), db::Version { version: 1 })?; - - let mut root_table = txn.open_table(db::v1::TABLE_ROOT)?; - assert!(root_table.is_empty()?); - - let persistent_state = models::v1::PersistentState { - version: ron_state.version, - wall: ron_state.wall, - problems: ron_state.problems, - }; - - root_table.insert((), persistent_state)?; - Ok(()) - }) - .await?; - - tracing::info!("Removing ron state"); - tokio::fs::remove_file(ron_state_file_path).await?; - } - - Ok(()) -} - -// TODO: Move out, is not really a migration -#[tracing::instrument(skip_all, err)] -async fn init_at_current_version(db: &Database) -> Result<(), Box> { - db.write(|txn| { - 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()?); - } - } - - Ok(()) - }) - .await?; - - Ok(()) -} - -#[tracing::instrument(skip_all, err)] -async fn migrate_to_v2(db: &Database) -> Result<(), Box> { - use super::db; - - db.write(|txn| { - let mut version_table = txn.open_table(db::TABLE_VERSION)?; - let version = version_table.get(())?.unwrap().value().version; - if version == 1 { - tracing::warn!("MIGRATING"); - version_table.insert((), db::Version { version: 2 })?; - - let root_table_v1 = txn.open_table(db::v1::TABLE_ROOT)?; - let root_v1 = root_table_v1.get(())?.unwrap().value(); - drop(root_table_v1); - txn.delete_table(db::v1::TABLE_ROOT)?; - - 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 wall_uid = models::v2::WallUid(uuid::Uuid::new_v4()); - let holds = wall - .holds - .into_iter() - .map(|(hold_position, hold)| -> Result<_, Box> { - let image = hold - .image - .map(|i| -> Result<_, Box> { - let holds_dir = Path::new("datastore/public/holds"); - - let p = holds_dir.join(i.filename); - tracing::info!("reading {}", p.display()); - let file_contents = std::fs::read(p)?; - - let mut decoder = image::ImageReader::new(std::io::Cursor::new(file_contents)) - .with_guessed_format()? - .into_decoder()?; - let orientation = decoder.orientation()?; - let mut img = image::DynamicImage::from_decoder(decoder)?; - img.apply_orientation(orientation); - - std::fs::create_dir_all(holds_dir)?; - - let targets = [(50, 50), (150, 150), (300, 300), (400, 400)]; - - let uid = models::ImageUid::create(); - let mut resolutions = BTreeMap::new(); - for (width, height) in targets { - tracing::info!("resizing to {width}x{height}"); - let resized = img.resize_to_fill(width, height, image::imageops::FilterType::Lanczos3); - - let filename = format!("hold_row{}_col{}_{width}x{height}_{uid}.webp", hold_position.row, hold_position.col); - let path = holds_dir.join(&filename); - tracing::info!("opening {}", path.display()); - let mut file = std::fs::OpenOptions::new().write(true).append(false).create_new(true).open(&path)?; - resized.write_to(&mut file, image::ImageFormat::WebP)?; - - let res = models::ImageResolution { - width: width.into(), - height: height.into(), - }; - resolutions.insert(res, models::ImageFilename { filename }); - } - - Ok(models::Image { uid, resolutions }) - }) - .transpose()?; - - Ok(( - models::v1::HoldPosition { - row: hold_position.row, - col: hold_position.col, - }, - models::v2::Hold { - position: models::v1::HoldPosition { - row: hold.position.row, - col: hold.position.col, - }, - image, - }, - )) - }) - .collect::>() - .unwrap(); - - let wall_v2 = models::v2::Wall { - uid: wall_uid, - rows: wall.rows, - cols: wall.cols, - holds, - problems: BTreeSet::new(), - }; - - walls.insert(wall_v2.uid); - let root_v2 = models::v2::Root { walls }; - - let mut root_table_v2 = txn.open_table(db::v2::TABLE_ROOT)?; - root_table_v2.insert((), root_v2)?; - drop(root_table_v2); - - let mut walls_table = txn.open_table(db::v2::TABLE_WALLS)?; - walls_table.insert(wall_v2.uid, wall_v2)?; - drop(walls_table); - - let problems_table = txn.open_table(db::v2::TABLE_PROBLEMS)?; - drop(problems_table); - } - - Ok(()) - }) - .await?; - +pub async fn run_migrations(_db: &Database) -> Result<(), Box> { Ok(()) }