Update database schema
Also bump major version to match schema version
This commit is contained in:
2
Cargo.lock
generated
2
Cargo.lock
generated
@ -1584,7 +1584,7 @@ checksum = "7345c971d1ef21ffdbd103a75990a15eb03604fc8b8852ca8cb418ee1a099028"
|
||||
|
||||
[[package]]
|
||||
name = "stl"
|
||||
version = "0.2.0"
|
||||
version = "2.0.0"
|
||||
dependencies = [
|
||||
"bincode",
|
||||
"chrono",
|
||||
|
||||
@ -1,7 +1,7 @@
|
||||
[package]
|
||||
name = "stl"
|
||||
description = "studielogg aka scuffed toggl"
|
||||
version = "0.2.0"
|
||||
version = "2.0.0"
|
||||
authors = ["Joakim Hulthe <joakim@hulthe.net>"]
|
||||
license = "MPL-2.0"
|
||||
edition = "2018"
|
||||
|
||||
36
src/database/migrations/mod.rs
Normal file
36
src/database/migrations/mod.rs
Normal file
@ -0,0 +1,36 @@
|
||||
pub mod v1_to_v2;
|
||||
|
||||
use crate::database::unversioned::global::schema_version;
|
||||
use duplicate::duplicate;
|
||||
use sled::Db;
|
||||
|
||||
pub fn migrate(db: &mut Db, from: schema_version::V, to: schema_version::V) -> Result<(), MigrationError> {
|
||||
for current in from..to {
|
||||
let next = current + 1;
|
||||
|
||||
println!("Will migrate from {} to {}", current, next);
|
||||
match (current, next) {
|
||||
(1, 2) => v1_to_v2::migrate(db)?,
|
||||
_ => panic!("No valid migration from version {} to version {}", current, next),
|
||||
}
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
pub enum MigrationError {
|
||||
Serde(bincode::Error),
|
||||
Sled(sled::Error),
|
||||
}
|
||||
|
||||
#[duplicate(
|
||||
Variant Error;
|
||||
[ Sled ] [ sled::Error ];
|
||||
[ Serde ] [ bincode::Error ];
|
||||
)]
|
||||
impl From<Error> for MigrationError {
|
||||
fn from(e: Error) -> MigrationError {
|
||||
MigrationError::Variant(e)
|
||||
}
|
||||
}
|
||||
84
src/database/migrations/v1_to_v2.rs
Normal file
84
src/database/migrations/v1_to_v2.rs
Normal file
@ -0,0 +1,84 @@
|
||||
use crate::database::v1;
|
||||
use crate::database::v2;
|
||||
use super::MigrationError;
|
||||
use bincode::{serialize, deserialize};
|
||||
use sled::Db;
|
||||
use chrono::{DateTime, Utc};
|
||||
|
||||
/// Migrate from database version 1 to version 2
|
||||
pub fn migrate(db: &mut Db) -> Result<(), MigrationError> {
|
||||
|
||||
{ // Migrate sessions
|
||||
|
||||
// Open old & new trees
|
||||
let v1_past_sessions = db.open_tree(v1::trees::past_sessions::NAME)?;
|
||||
let v2_sessions = db.open_tree(v2::trees::sessions::NAME)?;
|
||||
|
||||
// Iterate over old tree
|
||||
for r in v1_past_sessions.iter() {
|
||||
let (k, v) = r?;
|
||||
|
||||
// Deserialize old value
|
||||
let v1::trees::past_sessions::V {
|
||||
category,
|
||||
started,
|
||||
ended
|
||||
} = deserialize(&v)?;
|
||||
|
||||
// Convert to new value
|
||||
let v2_value = v2::trees::sessions::V {
|
||||
category,
|
||||
started: DateTime::<Utc>::from_utc(started, Utc).into(),
|
||||
ended: DateTime::<Utc>::from_utc(ended, Utc).into(),
|
||||
deleted: false,
|
||||
};
|
||||
|
||||
// Insert into new tree
|
||||
v2_sessions.insert(k, serialize(&v2_value)?)?;
|
||||
}
|
||||
|
||||
// Remove old tree
|
||||
v1_past_sessions.clear()?;
|
||||
}
|
||||
|
||||
{ // Migrate categories
|
||||
// Open the old tree, and a TMP tree since the old tree name hasn't changed
|
||||
let categories = db.open_tree(v1::trees::categories::NAME)?;
|
||||
let v2_categories_tmp = db.open_tree("TEMP")?;
|
||||
|
||||
// Iterate over old tree
|
||||
for r in categories.iter() {
|
||||
let (k, v) = r?;
|
||||
|
||||
// Deserialize old value
|
||||
let v1::trees::categories::V {
|
||||
name,
|
||||
color,
|
||||
started,
|
||||
} = deserialize(&v)?;
|
||||
|
||||
// Convert to new value
|
||||
let v2_value = v2::trees::categories::V {
|
||||
name,
|
||||
description: None,
|
||||
color,
|
||||
started: started.map(|ndt| DateTime::<Utc>::from_utc(ndt, Utc).into()),
|
||||
parent: None,
|
||||
deleted: false,
|
||||
};
|
||||
|
||||
// Insert into temporary tree
|
||||
v2_categories_tmp.insert(k, serialize(&v2_value)?)?;
|
||||
}
|
||||
|
||||
// Copy data from temp-tree back into old tree
|
||||
categories.clear()?;
|
||||
for r in v2_categories_tmp.iter() {
|
||||
let (k, v) = r?;
|
||||
categories.insert(k ,v)?;
|
||||
}
|
||||
v2_categories_tmp.clear()?;
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
@ -1,5 +1,8 @@
|
||||
pub mod unversioned;
|
||||
pub mod v1;
|
||||
pub mod v2;
|
||||
|
||||
pub const SCHEMA_VERSION: unversioned::global::schema_version::V = 1;
|
||||
pub use self::v1 as latest;
|
||||
pub mod migrations;
|
||||
|
||||
pub const SCHEMA_VERSION: unversioned::global::schema_version::V = 2;
|
||||
pub use v2 as latest;
|
||||
|
||||
@ -1,3 +1,5 @@
|
||||
#![allow(dead_code)] // old schema, not used anymore
|
||||
|
||||
pub(self) use chrono::NaiveDateTime;
|
||||
pub(self) use serde_derive::{Deserialize, Serialize};
|
||||
pub(self) use uuid::Uuid;
|
||||
@ -18,7 +20,7 @@ pub mod trees {
|
||||
/// The name of the category
|
||||
pub name: String,
|
||||
|
||||
/// The color of the button in the rendered view
|
||||
/// The HTML color of the category in the rendered view
|
||||
pub color: String,
|
||||
|
||||
/// If the session is not active, this will be None
|
||||
|
||||
64
src/database/v2.rs
Normal file
64
src/database/v2.rs
Normal file
@ -0,0 +1,64 @@
|
||||
pub(self) use chrono::{DateTime, Local};
|
||||
pub(self) use serde_derive::{Deserialize, Serialize};
|
||||
pub(self) use uuid::Uuid;
|
||||
|
||||
/// Stuff in the default namespace
|
||||
pub mod global {}
|
||||
|
||||
pub mod trees {
|
||||
pub mod categories {
|
||||
use super::super::*;
|
||||
|
||||
pub const NAME: &str = "CATEGORIES";
|
||||
|
||||
pub type K = Uuid;
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct V {
|
||||
/// The name of the category
|
||||
pub name: String,
|
||||
|
||||
/// The description of the category
|
||||
pub description: Option<String>,
|
||||
|
||||
/// The HTML color of the category in the rendered view
|
||||
pub color: String,
|
||||
|
||||
/// If the session is not active, this will be None
|
||||
pub started: Option<DateTime<Local>>,
|
||||
|
||||
/// The parent category of this category
|
||||
/// If none, the category has no parent
|
||||
pub parent: Option<K>,
|
||||
|
||||
// FIXME: this field is currently not used
|
||||
/// Whether the item has been "deleted", e.g. it shoudn't be shown in the view
|
||||
pub deleted: bool,
|
||||
}
|
||||
}
|
||||
|
||||
pub mod sessions {
|
||||
use super::super::*;
|
||||
|
||||
pub const NAME: &str = "SESSIONS";
|
||||
|
||||
pub type K = Uuid;
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct V {
|
||||
/// The UUID of the category to which this session belongs
|
||||
pub category: trees::categories::K,
|
||||
|
||||
/// The time when this session was started
|
||||
pub started: DateTime<Local>,
|
||||
|
||||
/// The time when this session was ended
|
||||
pub ended: DateTime<Local>,
|
||||
|
||||
// FIXME: this field is currently not used
|
||||
/// Whether the item has been "deleted", e.g. it shoudn't be shown in the view
|
||||
pub deleted: bool,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -1,10 +1,10 @@
|
||||
use chrono::NaiveDateTime;
|
||||
use chrono::{DateTime, Local};
|
||||
use handlebars::handlebars_helper;
|
||||
use rocket_contrib::templates::Engines;
|
||||
|
||||
pub fn register_helpers(engines: &mut Engines) {
|
||||
handlebars_helper!(pretty_datetime: |dt: str| {
|
||||
let dt: NaiveDateTime = dt.parse().unwrap();
|
||||
let dt: DateTime<Local> = dt.parse().unwrap();
|
||||
format!("{}", dt.format("%Y-%m-%d %H:%M"))
|
||||
});
|
||||
engines
|
||||
|
||||
@ -5,7 +5,9 @@ mod routes;
|
||||
mod status_json;
|
||||
|
||||
use bincode::{deserialize, serialize};
|
||||
use database::{unversioned::global::schema_version, SCHEMA_VERSION};
|
||||
use crate::database::SCHEMA_VERSION;
|
||||
use crate::database::unversioned::global::schema_version;
|
||||
use crate::database::migrations::migrate;
|
||||
use dotenv::dotenv;
|
||||
use rocket_contrib::serve::StaticFiles;
|
||||
use rocket_contrib::templates::Template;
|
||||
@ -16,7 +18,7 @@ fn main() -> io::Result<()> {
|
||||
|
||||
let db_path = env::var("DB_PATH").expect("DB_PATH not set");
|
||||
|
||||
let sled = sled::open(db_path)?;
|
||||
let mut sled = sled::open(db_path)?;
|
||||
match sled.insert(
|
||||
serialize(schema_version::K).unwrap(),
|
||||
serialize(&SCHEMA_VERSION).unwrap(),
|
||||
@ -27,6 +29,9 @@ fn main() -> io::Result<()> {
|
||||
"Schema version: {}, previously: {}",
|
||||
SCHEMA_VERSION, prev_schema_version
|
||||
);
|
||||
|
||||
migrate(&mut sled, prev_schema_version, SCHEMA_VERSION)
|
||||
.expect("Migration failed")
|
||||
}
|
||||
None => {
|
||||
println!("Schema version: {}, previously: None", SCHEMA_VERSION);
|
||||
|
||||
@ -1,9 +1,8 @@
|
||||
use crate::database::latest::trees::{categories, past_sessions};
|
||||
use crate::database::latest::trees::{categories, sessions};
|
||||
use crate::routes::pages;
|
||||
use crate::status_json::StatusJson;
|
||||
use bincode::{deserialize, serialize};
|
||||
use chrono::NaiveDateTime;
|
||||
use chrono::{Duration, Utc};
|
||||
use chrono::{NaiveDateTime, TimeZone, Duration, Local};
|
||||
use rocket::http::Status;
|
||||
use rocket::request::{Form, FromForm};
|
||||
use rocket::response::Redirect;
|
||||
@ -29,8 +28,11 @@ pub fn create_category(
|
||||
serialize(&uuid::Uuid::new_v4())?,
|
||||
serialize(&categories::V {
|
||||
name: category.name,
|
||||
description: None,
|
||||
color: category.color,
|
||||
started: None,
|
||||
parent: None,
|
||||
deleted: false,
|
||||
})?,
|
||||
)?;
|
||||
|
||||
@ -68,8 +70,16 @@ pub fn bump_session(
|
||||
let mut category: categories::V = deserialize(&data).unwrap();
|
||||
match category.started.as_mut() {
|
||||
Some(started) => {
|
||||
*started -= duration;
|
||||
if let Some(new_started) = started.checked_sub_signed(duration) {
|
||||
*started = new_started;
|
||||
tx_categories.insert(&category_uuid_s, serialize(&category).unwrap())?;
|
||||
} else {
|
||||
return Ok(Err(StatusJson::new(
|
||||
Status::BadRequest,
|
||||
"Duration subtract resulted in overflow",
|
||||
)))
|
||||
}
|
||||
|
||||
Ok(Ok(Redirect::to(uri!(pages::index))))
|
||||
}
|
||||
None => {
|
||||
@ -92,15 +102,15 @@ pub fn toggle_category_session(
|
||||
let category_uuid_s = sled::IVec::from(serialize(&category_uuid.into_inner())?);
|
||||
|
||||
let categories_tree = db.open_tree(categories::NAME)?;
|
||||
let past_sessions_tree = db.open_tree(past_sessions::NAME)?;
|
||||
let sessions_tree = db.open_tree(sessions::NAME)?;
|
||||
|
||||
Ok((&categories_tree, &past_sessions_tree).transaction(
|
||||
|(tx_categories, tx_past_sessions)| {
|
||||
Ok((&categories_tree, &sessions_tree).transaction(
|
||||
|(tx_categories, tx_sessions)| {
|
||||
match tx_categories.get(&category_uuid_s)? {
|
||||
None => return Ok(Err(Status::NotFound)),
|
||||
Some(data) => {
|
||||
let mut category: categories::V = deserialize(&data).unwrap();
|
||||
let now = Utc::now().naive_utc();
|
||||
let now = Local::now();
|
||||
|
||||
match (set_active, category.started.take()) {
|
||||
(false, Some(started)) => {
|
||||
@ -108,13 +118,14 @@ pub fn toggle_category_session(
|
||||
let duration = now - started;
|
||||
if duration > Duration::minutes(5) {
|
||||
let session_uuid = serialize(&uuid::Uuid::new_v4()).unwrap();
|
||||
let past_session = past_sessions::V {
|
||||
let session = sessions::V {
|
||||
category: category_uuid.into_inner(),
|
||||
started,
|
||||
ended: now,
|
||||
deleted: category.deleted,
|
||||
};
|
||||
tx_past_sessions
|
||||
.insert(session_uuid, serialize(&past_session).unwrap())?;
|
||||
tx_sessions
|
||||
.insert(session_uuid, serialize(&session).unwrap())?;
|
||||
}
|
||||
}
|
||||
(true, None) => {
|
||||
@ -140,6 +151,7 @@ pub struct EditSession {
|
||||
category: Uuid,
|
||||
started: String,
|
||||
ended: String,
|
||||
deleted: bool,
|
||||
}
|
||||
|
||||
#[post("/session/<session_uuid>/edit", data = "<session>")]
|
||||
@ -150,12 +162,11 @@ pub fn edit_session(
|
||||
) -> Result<Redirect, StatusJson> {
|
||||
let session_uuid_s = sled::IVec::from(serialize(&session_uuid.into_inner())?);
|
||||
|
||||
dbg!(&session);
|
||||
|
||||
let session = past_sessions::V {
|
||||
let session = sessions::V {
|
||||
category: session.category.into_inner(),
|
||||
started: NaiveDateTime::parse_from_str(&session.started, "%Y-%m-%d %H:%M")?,
|
||||
ended: NaiveDateTime::parse_from_str(&session.ended, "%Y-%m-%d %H:%M")?,
|
||||
started: Local.from_local_datetime(&NaiveDateTime::parse_from_str(&session.started, "%Y-%m-%d %H:%M")?).unwrap(),
|
||||
ended: Local.from_local_datetime(&NaiveDateTime::parse_from_str(&session.ended, "%Y-%m-%d %H:%M")?).unwrap(),
|
||||
deleted: session.deleted,
|
||||
};
|
||||
|
||||
if session.started >= session.ended {
|
||||
@ -165,7 +176,7 @@ pub fn edit_session(
|
||||
));
|
||||
}
|
||||
|
||||
db.open_tree(past_sessions::NAME)?
|
||||
db.open_tree(sessions::NAME)?
|
||||
.insert(session_uuid_s, serialize(&session)?)?;
|
||||
|
||||
// FIXME: Uuid does not implement FromUriParam for some reason... File an issue?
|
||||
@ -177,19 +188,19 @@ pub fn edit_session(
|
||||
pub fn delete_session(session_uuid: Uuid, db: State<'_, sled::Db>) -> Result<Redirect, StatusJson> {
|
||||
let session_uuid_s = sled::IVec::from(serialize(&session_uuid.into_inner())?);
|
||||
|
||||
let past_sessions_tree = db.open_tree(past_sessions::NAME)?;
|
||||
let sessions_tree = db.open_tree(sessions::NAME)?;
|
||||
|
||||
match past_sessions_tree.remove(session_uuid_s)? {
|
||||
match sessions_tree.remove(session_uuid_s)? {
|
||||
Some(_) => Ok(Redirect::to(uri!(pages::history))),
|
||||
None => Err(Status::NotFound.into()),
|
||||
}
|
||||
|
||||
// TODO: mark as deleted instead of removing
|
||||
// Ok(past_sessions_tree.transaction(|tx_past_sessions| {
|
||||
// match tx_past_sessions.get(&session_uuid_s)? {
|
||||
// Ok(sessions_tree.transaction(|tx_sessions| {
|
||||
// match tx_sessions.get(&session_uuid_s)? {
|
||||
// None => return Ok(Err(Status::NotFound)),
|
||||
// Some(data) => {
|
||||
// let mut past_session: past_sessions::V = deserialize(&data).unwrap();
|
||||
// let mut session: sessions::V = deserialize(&data).unwrap();
|
||||
// }
|
||||
// }
|
||||
// Ok(Ok(Redirect::to(uri!(pages::history))))
|
||||
|
||||
@ -1,4 +1,4 @@
|
||||
use crate::database::latest::trees::{categories, past_sessions};
|
||||
use crate::database::latest::trees::{categories, sessions};
|
||||
use crate::status_json::StatusJson;
|
||||
use bincode::deserialize;
|
||||
use bincode::serialize;
|
||||
@ -35,14 +35,14 @@ pub fn index(db: State<'_, sled::Db>) -> Result<Template, StatusJson> {
|
||||
pub fn session_edit(session_uuid: Uuid, db: State<'_, sled::Db>) -> Result<Template, StatusJson> {
|
||||
#[derive(Debug, Serialize, Deserialize)]
|
||||
struct SessionPageContext {
|
||||
session: past_sessions::V,
|
||||
session: sessions::V,
|
||||
session_id: uuid::Uuid,
|
||||
}
|
||||
|
||||
let session_uuid_s = sled::IVec::from(serialize(&session_uuid.into_inner())?);
|
||||
|
||||
let past_sessions_tree = db.open_tree(past_sessions::NAME)?;
|
||||
match past_sessions_tree.get(session_uuid_s)? {
|
||||
let sessions_tree = db.open_tree(sessions::NAME)?;
|
||||
match sessions_tree.get(session_uuid_s)? {
|
||||
None => Err(Status::NotFound)?,
|
||||
Some(data) => {
|
||||
let context = SessionPageContext {
|
||||
@ -60,7 +60,7 @@ pub fn history(db: State<'_, sled::Db>) -> Result<Template, StatusJson> {
|
||||
#[derive(Debug, Serialize, Deserialize)]
|
||||
struct HistoryEntryContext {
|
||||
category: categories::V,
|
||||
session: past_sessions::V,
|
||||
session: sessions::V,
|
||||
session_id: uuid::Uuid,
|
||||
duration: Duration,
|
||||
}
|
||||
@ -71,7 +71,7 @@ pub fn history(db: State<'_, sled::Db>) -> Result<Template, StatusJson> {
|
||||
}
|
||||
|
||||
let categories_tree = db.open_tree(categories::NAME)?;
|
||||
let past_sessions_tree = db.open_tree(past_sessions::NAME)?;
|
||||
let sessions_tree = db.open_tree(sessions::NAME)?;
|
||||
|
||||
let categories: HashMap<categories::K, categories::V> = categories_tree
|
||||
.iter()
|
||||
@ -80,7 +80,7 @@ pub fn history(db: State<'_, sled::Db>) -> Result<Template, StatusJson> {
|
||||
})
|
||||
.collect::<Result<Result<_, _>, _>>()??;
|
||||
|
||||
let past_sessions: HashMap<past_sessions::K, past_sessions::V> = past_sessions_tree
|
||||
let sessions: HashMap<sessions::K, sessions::V> = sessions_tree
|
||||
.iter()
|
||||
.map(|result| {
|
||||
result.map(|(k, v)| deserialize(&k).and_then(|k| deserialize(&v).map(|v| (k, v))))
|
||||
@ -88,7 +88,7 @@ pub fn history(db: State<'_, sled::Db>) -> Result<Template, StatusJson> {
|
||||
.collect::<Result<Result<_, _>, _>>()??;
|
||||
|
||||
let mut context = TemplateContext {
|
||||
entries: past_sessions
|
||||
entries: sessions
|
||||
.into_iter()
|
||||
.map(|(session_id, session)| HistoryEntryContext {
|
||||
duration: (session.ended - session.started).to_std().unwrap(),
|
||||
|
||||
@ -21,6 +21,7 @@
|
||||
<br>
|
||||
<form action="/session/{{session_id}}/edit" method="post">
|
||||
<input type="hidden" id="category" name="category" value="{{session.category}}">
|
||||
<input type="hidden" id="deleted" name="deleted" value="{{session.deleted}}">
|
||||
<span>Started:</span>
|
||||
<input type="text" id="started" name="started" value="{{pretty_datetime session.started}}"></input>
|
||||
<br>
|
||||
|
||||
Reference in New Issue
Block a user