diff --git a/crates/wastebin_core/src/db.rs b/crates/wastebin_core/src/db.rs index 87a710f..fc3ff72 100644 --- a/crates/wastebin_core/src/db.rs +++ b/crates/wastebin_core/src/db.rs @@ -59,9 +59,8 @@ struct Handler { /// Commands issued to the database handler and corresponding to [`Database`] calls. enum Command { Insert { - id: Id, entry: write::DatabaseEntry, - result: oneshot::Sender>, + result: oneshot::Sender>, }, Get { id: Id, @@ -396,9 +395,9 @@ impl Handler { }; match command { - Command::Insert { id, entry, result } => { + Command::Insert { entry, result } => { result - .send(self.insert(id, entry)) + .send(self.insert(entry)) .map_err(|_| Error::ResultSendError)?; } Command::Get { id, result } => { @@ -447,31 +446,56 @@ impl Handler { fn insert( &self, - id: Id, write::DatabaseEntry { entry, data, nonce }: write::DatabaseEntry, - ) -> Result<(), Error> { + ) -> Result<(Id, write::Entry), Error> { + let mut counter = 0; + let title = entry.title.clone(); let nonce = nonce.as_ref().map(|n| n.as_slice()); - match entry.expires { - None => self.conn.execute( - "INSERT INTO entries (id, uid, data, burn_after_reading, nonce, title) VALUES (?1, ?2, ?3, ?4, ?5, ?6)", - params![id.to_i64(), entry.uid, data, entry.burn_after_reading, nonce, entry.title], - )?, - Some(expires) => self.conn.execute( - "INSERT INTO entries (id, uid, data, burn_after_reading, nonce, expires, title) VALUES (?1, ?2, ?3, ?4, ?5, datetime('now', ?6), ?7)", - params![ - id.to_i64(), - entry.uid, - data, - entry.burn_after_reading, - nonce, - format!("{expires} seconds"), - entry.title, - ], - )?, - }; + loop { + let id = Id::rand(); + + let result = match entry.expires { + None => self.conn.execute( + "INSERT INTO entries (id, uid, data, burn_after_reading, nonce, title) VALUES (?1, ?2, ?3, ?4, ?5, ?6)", + params![id.to_i64(), entry.uid, data, entry.burn_after_reading, nonce, title], + ), + Some(expires) => self.conn.execute( + "INSERT INTO entries (id, uid, data, burn_after_reading, nonce, expires, title) VALUES (?1, ?2, ?3, ?4, ?5, datetime('now', ?6), ?7)", + params![ + id.to_i64(), + entry.uid, + data, + entry.burn_after_reading, + nonce, + format!("{expires} seconds"), + title, + ], + ), + }; - Ok(()) + match result { + Err(rusqlite::Error::SqliteFailure( + rusqlite::ffi::Error { + code, + extended_code, + }, + Some(ref _message), + )) if code == rusqlite::ErrorCode::ConstraintViolation + && extended_code == rusqlite::ffi::SQLITE_CONSTRAINT_PRIMARYKEY + && counter < 10 => + { + // Retry if ID is already existent + counter += 1; + continue; + } + Err(err) => break Err(err)?, + Ok(rows) => { + debug_assert!(rows == 1); + return Ok((id, entry)); + } + } + } } fn get_metadata(&self, id: Id) -> Result { @@ -623,13 +647,14 @@ impl Database { Ok((Self { sender }, fut)) } - /// Insert `entry` under `id` into the database and optionally set owner to `uid`. - pub async fn insert(&self, id: Id, entry: write::Entry) -> Result<(), Error> { + /// Insert `entry` under a new random id into the database and optionally set owner to `uid`. + /// Returns the id of the new entry on success. + pub async fn insert(&self, entry: write::Entry) -> Result<(Id, write::Entry), Error> { let entry = entry.compress().await?.encrypt().await?; let (result, command_result) = oneshot::channel(); self.sender - .send(Command::Insert { id, entry, result }) + .send(Command::Insert { entry, result }) .await .map_err(|_| Error::SendError)?; @@ -770,8 +795,7 @@ mod tests { ..Default::default() }; - let id = Id::from(1234u32); - db.insert(id, entry).await?; + let (id, _entry) = db.insert(entry).await?; let entry = db.get(id, None).await?.unwrap_inner(); assert_eq!(entry.text, "hello world"); @@ -807,8 +831,7 @@ mod tests { ..Default::default() }; - let id = Id::from(1234u32); - db.insert(id, entry).await?; + let (id, _entry) = db.insert(entry).await?; tokio::time::sleep(tokio::time::Duration::from_secs(2)).await; @@ -822,8 +845,7 @@ mod tests { async fn delete() -> Result<(), Box> { let db = new_db()?; - let id = Id::from(1234u32); - db.insert(id, write::Entry::default()).await?; + let (id, _entry) = db.insert(write::Entry::default()).await?; assert!(db.get(id, None).await.is_ok()); assert!(db.delete(id).await.is_ok()); @@ -836,14 +858,13 @@ mod tests { async fn delete_for() -> Result<(), Box> { let db = new_db()?; - let id = Id::from(1234u32); let uid = 42; let entry = write::Entry { uid: Some(uid), ..Default::default() }; - db.insert(id, entry).await?; + let (id, _entry) = db.insert(entry).await?; assert!(db.get(id, None).await.is_ok()); assert!(db.delete_for(id, uid).await.is_ok()); @@ -853,7 +874,7 @@ mod tests { uid: Some(uid), ..Default::default() }; - db.insert(id, entry).await?; + let (id, _entry) = db.insert(entry).await?; let incorrect_uid = 99; assert!(matches!( @@ -875,14 +896,13 @@ mod tests { ..Default::default() }; - let id = Id::from(1234u32); - db.insert(id, entry).await?; + let (id, _entry) = db.insert(entry).await?; tokio::time::sleep(tokio::time::Duration::from_secs(2)).await; let ids = db.purge().await?; assert_eq!(ids.len(), 1); - assert_eq!(ids[0].to_i64(), 1234); + assert_eq!(ids[0], id); Ok(()) } @@ -899,8 +919,7 @@ mod tests { ..Default::default() }; - let id = Id::from(5678u32); - db.insert(id, entry).await?; + let (id, _entry) = db.insert(entry).await?; let metadata = db.get_metadata(id).await?; assert_eq!(metadata.uid, Some(42)); diff --git a/crates/wastebin_server/src/handlers/insert/api.rs b/crates/wastebin_server/src/handlers/insert/api.rs index 0ed773a..a551eb6 100644 --- a/crates/wastebin_server/src/handlers/insert/api.rs +++ b/crates/wastebin_server/src/handlers/insert/api.rs @@ -6,7 +6,6 @@ use serde::{Deserialize, Serialize}; use crate::errors::{Error, JsonErrorResponse}; use wastebin_core::db::{Database, write}; -use wastebin_core::id::Id; #[derive(Debug, Serialize, Deserialize)] pub(crate) struct Entry { @@ -41,10 +40,9 @@ pub async fn post( State(db): State, Json(entry): Json, ) -> Result, JsonErrorResponse> { - let id = Id::rand(); let entry: write::Entry = entry.into(); + let (id, entry) = db.insert(entry).await.map_err(Error::Database)?; let path = format!("/{}", id.to_url_path(&entry)); - db.insert(id, entry).await.map_err(Error::Database)?; Ok(Json::from(RedirectResponse { path })) } diff --git a/crates/wastebin_server/src/handlers/insert/form.rs b/crates/wastebin_server/src/handlers/insert/form.rs index a218f6c..4394fad 100644 --- a/crates/wastebin_server/src/handlers/insert/form.rs +++ b/crates/wastebin_server/src/handlers/insert/form.rs @@ -10,7 +10,6 @@ use crate::handlers::cookie; use crate::handlers::extract::{Theme, Uid}; use crate::handlers::html::make_error; use wastebin_core::db::{Database, write}; -use wastebin_core::id::Id; #[derive(Debug, Default, Serialize, Deserialize)] pub(crate) struct Entry { @@ -67,7 +66,7 @@ pub async fn post( let mut entry: write::Entry = entry.into(); entry.uid = Some(uid); - let id = Id::rand(); + let (id, entry) = db.insert(entry).await?; let url = { let url_path = id.to_url_path(&entry); @@ -78,8 +77,6 @@ pub async fn post( } }; - db.insert(id, entry).await?; - let mut cookie = cookie("uid", uid.to_string()); cookie.set_secure(true);