From 4d564c3d90e364ad8c627d523ed80dc4a6b70b57 Mon Sep 17 00:00:00 2001 From: Olaf Wintermann Date: Sun, 14 Jun 2026 17:25:11 +0200 Subject: [PATCH] add version number to notes and check version when updating a note --- application/src/backend.rs | 87 ++++++++++++++++--- application/src/lockmanager.rs | 41 ++++++--- application/src/note.rs | 20 +++-- entity/src/note.rs | 7 +- .../src/m20260502_184134_create_settings.rs | 1 + 5 files changed, 125 insertions(+), 31 deletions(-) diff --git a/application/src/backend.rs b/application/src/backend.rs index 169c382..ce601ec 100644 --- a/application/src/backend.rs +++ b/application/src/backend.rs @@ -29,20 +29,21 @@ use std::future::Future; use std::pin::Pin; -use sea_orm::{ActiveModelTrait, Database, DatabaseConnection, DbErr, EntityTrait, QueryFilter, ColumnTrait, Set, QueryOrder}; +use sea_orm::{ActiveModelTrait, Database, DatabaseConnection, EntityTrait, QueryFilter, ColumnTrait, Set, QueryOrder, DbErr, ExprTrait}; use tokio::runtime::Runtime; use std::sync::{Arc}; use std::thread::JoinHandle; use tokio::sync::{broadcast, mpsc}; use tokio::sync::broadcast::error::SendError; -use migration::{Migrator, MigratorTrait}; +use migration::{Expr, Migrator, MigratorTrait}; use ui_rs::ui; use entity::{collection, note, notecontent, profile}; use entity::profile::Entity as Profile; use entity::collection::{create_notebook_hierarchy, CollectionType, Entity as Collection, Node}; -use entity::note::{Entity as Note}; +use entity::note::{Column, Entity as Note}; use entity::notecontent::{Entity as NoteContent}; +use migration::prelude::Utc; use crate::lockmanager::LockManager; pub struct Backend { @@ -381,19 +382,62 @@ impl BackendHandle { } pub fn save_note(&self, id: NoteId, note: note::ActiveModel, content: Option, callback: F) - where F: FnOnce(Result<(note::Model, i32), DbErr>) + Send + 'static { + where F: FnOnce(SaveNoteResult) + Send + 'static { let bhandle = self.clone(); let cmd = Box::pin(async move { - let result = if note.note_id.is_set() { - note.update(&bhandle.backend.db).await + let result = if let Set(note_id) = note.note_id { + let mut update = Note::update_many(); + if let Set(kind) = note.kind { + update = update.col_expr(Column::Kind, Expr::value(kind)); + } + if let Set(title) = note.title { + update = update.col_expr(Column::Title, Expr::value(title)); + } + + let version = if let Set(version) = note.version { + version + } else { + // not setting version when calling save_note is a mistake and will probably + // lead to a VersionConflict + 0 + }; + + let uresult = update + .col_expr(Column::Lastmodified, Expr::value(Utc::now())) + .col_expr(Column::Version, Expr::col(Column::Version).add(1)) + .filter(Column::NoteId.eq(note_id)) + .filter(Column::Version.eq(version)) + .exec(&bhandle.backend.db).await; + + match uresult { + Ok(updated) => { + if updated.rows_affected != 1 { + Err(SaveNoteResult::VersionConflict) + } else { + let result = Note::find_by_id(note_id).one(&bhandle.backend.db).await; + match result { + Ok(Some(note)) => Ok(note), + Ok(None) => Err(SaveNoteResult::Error(DbErr::Custom("note not found".into()))), + Err(e) => Err(SaveNoteResult::Error(e)), + } + } + }, + Err(e) => { + Err(SaveNoteResult::Error(e)) + } + } } else { - note.insert(&bhandle.backend.db).await + let result = note.insert(&bhandle.backend.db).await; + match result { + Ok(note) => Ok(note), + Err(e) => Err(SaveNoteResult::Error(e)), + } }; match result { Ok(note) => { let note_id = note.note_id; - let result: Result<(note::Model, i32), DbErr>; + let result: SaveNoteResult; if let Some(mut content) = content { let result2 = if content.id.is_set() { content.update(&bhandle.backend.db).await @@ -404,14 +448,14 @@ impl BackendHandle { match result2 { Ok(ctn) => { - result = Ok((note.clone(), ctn.id)); + result = SaveNoteResult::Ok((note.clone(), ctn.id)); } Err(e) => { - result = Err(e); + result = SaveNoteResult::Error(e); } } } else { - result = Ok((note.clone(), 0)); + result = SaveNoteResult::Ok((note.clone(), 0)); } if result.is_ok() { @@ -425,10 +469,29 @@ impl BackendHandle { callback(result); }, Err(e) => { - callback(Err(e)); + callback(e); } } }); let _ = self.tx.send(cmd); } } + + +pub enum SaveNoteResult { + Ok((entity::note::Model, i32)), + VersionConflict, + Error(DbErr) +} + +impl SaveNoteResult { + pub fn is_ok(&self) -> bool { + matches!(self, SaveNoteResult::Ok(_)) + } + + /* + pub fn is_err(&self) -> bool { + matches!(self, SaveNoteResult::Error(_)) + } + */ +} \ No newline at end of file diff --git a/application/src/lockmanager.rs b/application/src/lockmanager.rs index 1f0d9a0..7afb736 100644 --- a/application/src/lockmanager.rs +++ b/application/src/lockmanager.rs @@ -25,49 +25,62 @@ * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE * POSSIBILITY OF SUCH DAMAGE. */ -use std::collections::HashMap; +use std::collections::{HashMap, HashSet}; use std::sync::Arc; pub struct LockManager { - locks: Arc>>, + locks: Arc>>, + revisions: Arc>>, } impl LockManager { pub fn new() -> Self { LockManager { - locks: Arc::new(std::sync::Mutex::new(HashMap::new())), + locks: Arc::new(std::sync::Mutex::new(HashSet::new())), + revisions: Arc::new(std::sync::Mutex::new(HashMap::new())), } } - fn locks(&self) -> std::sync::MutexGuard<'_, HashMap> { + fn locks(&self) -> std::sync::MutexGuard<'_, HashSet> { self.locks.lock().unwrap_or_else(|e| { // Very unlikely that the mutex gets poisoned (probably only by OOM), and if it does, who cares - println!("Error: LockManager poisoned"); + println!("Error: LockManager locks poisoned"); self.locks.clear_poison(); e.into_inner() }) } + fn revisions(&self) -> std::sync::MutexGuard<'_, HashMap> { + self.revisions.lock().unwrap_or_else(|e| { + // Very unlikely that the mutex gets poisoned (probably only by OOM), and if it does, who cares + println!("Error: LockManager revisions poisoned"); + self.revisions.clear_poison(); + e.into_inner() + }) + } + pub fn lock_note(&self, note_id: i32) -> Option { let mut locks = self.locks(); - if locks.contains_key(¬e_id) { + if locks.contains(¬e_id) { println!("note {} already locked", note_id); return None } println!("lock note {}", note_id); - locks.insert(note_id, true); - + locks.insert(note_id); + let note_lock = NoteLock { locks: self.locks.clone(), note_id: note_id }; Some(note_lock) } + + } pub struct NoteLock { - locks: Arc>>, - note_id: i32 + locks: Arc>>, + pub note_id: i32 } impl Drop for NoteLock { @@ -76,4 +89,10 @@ impl Drop for NoteLock { let mut locks = self.locks.lock().unwrap_or_else(|e|{ e.into_inner()}); locks.remove(&self.note_id); } -} \ No newline at end of file +} + +pub struct NoteRevision { + revisions: Arc>>, + pub note_id: i32, + pub revision: u64 +} diff --git a/application/src/note.rs b/application/src/note.rs index 7690757..9dc7743 100644 --- a/application/src/note.rs +++ b/application/src/note.rs @@ -33,7 +33,7 @@ use sea_orm::{NotSet, Set}; use entity::note::NoteType; use ui_rs::{action, ui_actions, UiModel}; use ui_rs::ui::*; -use crate::backend::{BackendHandle, BroadcastMessage, NoteId, NoteTitleUpdate}; +use crate::backend::{BackendHandle, BroadcastMessage, NoteId, NoteTitleUpdate, SaveNoteResult}; use crate::lockmanager::NoteLock; use crate::window::NoteTypeTabView; @@ -49,6 +49,7 @@ pub struct Note { pub collection_id: i32, pub content_id: i32, + pub version: i64, pub kind: NoteType, @@ -76,6 +77,7 @@ impl Note { id: id, lock: None, content_id: 0, + version: 0, kind: NoteType::PlainTextNote, extract_title: false, title_start: -1, @@ -122,6 +124,7 @@ impl Note { pub fn init_from_model(&mut self, model: &entity::note::Model) { self.id = NoteId::Id(model.note_id); + self.version = model.version; let tab = match model.kind { NoteType::PlainTextNote => NoteTypeTabView::TextArea, @@ -218,7 +221,8 @@ impl Note { kind: Set(self.kind.clone()), title: Set(title), lastmodified: Set(Utc::now().into()), - created + created: created, + version: Set(self.version) }; let notecontent = entity::notecontent::ActiveModel { @@ -231,13 +235,17 @@ impl Note { self.backend.save_note(self.id.clone(), note, Some(notecontent), |result|{ proxy.call_mainthread(move |_doc, note|{ match result { - Ok((notemodel, content_id)) => { + SaveNoteResult::Ok((notemodel, content_id)) => { note.id = NoteId::Id(notemodel.note_id); note.content_id = content_id; - println!("Note saved: note_id: {}, content_id: {}", notemodel.note_id, content_id); + note.version = notemodel.version; + println!("Note saved: note_id: {}, content_id: {}, version: {}", notemodel.note_id, content_id, note.version); }, - Err(error) => { - println!("Failed to save note: {}", error); + SaveNoteResult::VersionConflict => { + println!("Failed to save note: version conflict"); + }, + SaveNoteResult::Error(e) => { + println!("Failed to save note: {}", e); } } }); diff --git a/entity/src/note.rs b/entity/src/note.rs index c7d51ca..82831da 100644 --- a/entity/src/note.rs +++ b/entity/src/note.rs @@ -16,7 +16,9 @@ pub struct Model { pub created: DateTimeWithTimeZone, #[sea_orm(has_one)] - pub content: HasOne + pub content: HasOne, + + pub version: i64 } #[derive(EnumIter, DeriveActiveEnum, Clone, Debug, PartialEq, Default)] @@ -37,7 +39,8 @@ impl Model { kind: Default::default(), title: Default::default(), lastmodified: Default::default(), - created: Default::default() + created: Default::default(), + version: 0 } } } diff --git a/migration/src/m20260502_184134_create_settings.rs b/migration/src/m20260502_184134_create_settings.rs index 1001304..3bdc1cb 100644 --- a/migration/src/m20260502_184134_create_settings.rs +++ b/migration/src/m20260502_184134_create_settings.rs @@ -52,6 +52,7 @@ impl MigrationTrait for Migration { .col(string("title")) .col(timestamp_with_time_zone("lastmodified")) .col(timestamp_with_time_zone("created")) + .col(big_integer("version")) .to_owned() ).await?; -- 2.52.0