Files
rukosync/src/sync.rs
T
2026-10-06 08:24:08 +03:00

206 lines
6.9 KiB
Rust

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<Writer>,
reader: Arc<Reader>,
}
#[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<String>,
pub timestamp: i64,
}
impl ProgressSync {
pub fn new(writer: Arc<Writer>, reader: Arc<Reader>) -> 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<Option<Document>, SyncError> {
let username = username.to_owned();
let document_id = document_id.to_owned();
let doc = self.reader.connection.call(move |conn| -> rusqlite::Result<Option<Document>> {
let x : Option<Document> = 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<String> = 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<i64> {
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(())
}
}