use std::sync::Arc; use thiserror::Error; use tokio_rusqlite::{OptionalExtension as _, named_params, rusqlite}; use crate::{reader::Reader, writer::Writer}; #[derive(Clone)] pub struct ProgressSync { writer: Arc, reader: Arc, } #[derive(Error, Debug)] pub enum SyncError { #[error("Database call failed {0}")] DatabaseFailure(#[from] tokio_rusqlite::Error) } #[derive(Debug, PartialEq, Clone)] pub struct Document { // document identifier, name of the file, md5 of the file or something pub document_id: String, // opaque progress pub progress: String, pub percentage: f32, pub device: String, pub device_id: Option, pub timestamp: i64, } impl ProgressSync { pub fn new(writer: Arc, reader: Arc) -> Self { Self{writer,reader} } pub async fn set_progress(&self, username: &str, document: &Document) -> Result<(), SyncError> { let username = username.to_owned(); let doc = document.clone(); self.writer.connection.call(move |conn| -> rusqlite::Result<()> { conn.execute("insert into progress (user_id, document_id, progress, percentage, device, device_id, timestamp) select id, :document_id, :progress, :percentage, :device, :device_id, :timestamp from users where username = :username on conflict (user_id, document_id) do update set progress = excluded.progress, percentage = excluded.percentage, device = excluded.device, device_id = excluded.device_id, timestamp = excluded.timestamp", named_params! { ":username": username, ":document_id": doc.document_id, ":progress": doc.progress, ":percentage": doc.percentage, ":device": doc.device, ":device_id": doc.device_id, ":timestamp": doc.timestamp, })?; Ok(()) }).await?; Ok(()) } pub async fn get_progress(&self, username: &str, document_id: &str) -> Result, SyncError> { let username = username.to_owned(); let document_id = document_id.to_owned(); let doc = self.reader.connection.call(move |conn| -> rusqlite::Result> { let x : Option = conn.query_row("select document_id, progress, percentage, device, device_id, timestamp from progress left join users on progress.user_id = users.id where users.username = :username and progress.document_id = :document_id", named_params! { ":username": username, ":document_id": document_id, }, |row| { let document_id : String = row.get(0)?; let progress : String = row.get(1)?; let percentage : f32 = row.get(2)?; let device : String = row.get(3)?; let device_id : Option = row.get(4)?; let timestamp : i64 = row.get(5)?; Ok(Document{ document_id, progress, percentage, device, device_id, timestamp }) }).optional()?; Ok(x) }).await?; Ok(doc) } } #[cfg(test)] mod tests { use std::time::{SystemTime, UNIX_EPOCH}; use secrecy::SecretString; use tempfile::{TempDir, tempdir}; use crate::users::Users; use super::*; async fn create_data() -> anyhow::Result<(TempDir, Users, ProgressSync)> { let temp_dir = tempdir()?; let path = temp_dir.path().join("kosync.db"); let writer = Arc::new(Writer::new(&path).await?); let reader = Arc::new(Reader::new(&path).await?); let users = Users::new(writer.clone(), reader.clone(), SecretString::from("secret")); let sync = ProgressSync::new(writer.clone(), reader.clone()); Ok((temp_dir, users, sync)) } fn now() -> anyhow::Result { let n = SystemTime::now().duration_since(UNIX_EPOCH)?.as_secs() as i64; Ok(n) } #[tokio::test] async fn put_get_values() -> anyhow::Result<()> { let (_t, users, sync) = create_data().await?; let username = "foo@example.com"; let username = users.create_user(username, SecretString::from("password")).await?; let doc = Document { document_id: "foo".to_string(), progress: "prog".to_string(), percentage: 0.5, device: "kobo".to_string(), device_id: None, timestamp: now()?, }; sync.set_progress(&username, &doc).await?; let got = sync.get_progress(&username, "foo").await?; assert_eq!(got, Some(doc)); Ok(()) } #[tokio::test] async fn put_gets_latest() -> anyhow::Result<()> { let (_t, users, sync) = create_data().await?; let username = "foo@example.com"; let username = users.create_user(username, SecretString::from("password")).await?; let document_id = "foo".to_string(); let doc1 = Document { document_id: document_id.clone(), progress: "prog".to_string(), percentage: 0.5, device: "kobo".to_string(), device_id: None, timestamp: now()?, }; let doc2 = Document { document_id: document_id.clone(), progress: "prog2".to_string(), percentage: 0.5, device: "kobo".to_string(), device_id: None, timestamp: now()?, }; sync.set_progress(&username, &doc1).await?; sync.set_progress(&username, &doc2).await?; let got = sync.get_progress(&username, &document_id).await?; assert_eq!(got, Some(doc2)); Ok(()) } #[tokio::test] async fn put_gets_new_device() -> anyhow::Result<()> { let (_t, users, sync) = create_data().await?; let username = "foo@example.com"; let username = users.create_user(username, SecretString::from("password")).await?; let document_id = "foo".to_string(); let doc1 = Document { document_id: document_id.clone(), progress: "prog".to_string(), percentage: 0.5, device: "kobo".to_string(), device_id: None, timestamp: now()?, }; let doc2 = Document { document_id: document_id.clone(), progress: "prog2".to_string(), percentage: 0.5, device: "x4".to_string(), device_id: None, timestamp: now()? + 3, }; sync.set_progress(&username, &doc1).await?; sync.set_progress(&username, &doc2).await?; let got = sync.get_progress(&username, &document_id).await?; assert_eq!(got, Some(doc2)); Ok(()) } }