Store metadata
This commit is contained in:
1 parent
bdaa4b04c0
commit
3d8429f2f2
4 files changed
+187
-16
No files matched your search
@@ -14,6 +14,13 @@ pub fn syncs_routes() -> Router<AppState> {
|
|||||||
.route("/progress/{document}", routing::get(get_progress))
|
.route("/progress/{document}", routing::get(get_progress))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Deserialize)]
|
||||||
|
pub struct Metadata {
|
||||||
|
filename: String,
|
||||||
|
title: String,
|
||||||
|
authors: String,
|
||||||
|
}
|
||||||
|
|
||||||
#[derive(Debug,Deserialize)]
|
#[derive(Debug,Deserialize)]
|
||||||
pub struct PutProgress {
|
pub struct PutProgress {
|
||||||
pub document: String,
|
pub document: String,
|
||||||
@@ -21,6 +28,7 @@ pub struct PutProgress {
|
|||||||
pub percentage: f32,
|
pub percentage: f32,
|
||||||
pub device: String,
|
pub device: String,
|
||||||
pub device_id: Option<String>,
|
pub device_id: Option<String>,
|
||||||
|
pub metadata: Option<Metadata>,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug,Serialize)]
|
#[derive(Debug,Serialize)]
|
||||||
@@ -44,6 +52,10 @@ pub async fn put_progress(State(st) : State<AppState>, AuthUser{username} : Auth
|
|||||||
percentage: progress.percentage,
|
percentage: progress.percentage,
|
||||||
device: progress.device,
|
device: progress.device,
|
||||||
device_id: progress.device_id,
|
device_id: progress.device_id,
|
||||||
|
title: progress.metadata.as_ref().map(|m| m.title.clone()),
|
||||||
|
author: progress.metadata.as_ref().map(|m| m.authors.clone()),
|
||||||
|
filename: progress.metadata.as_ref().map(|m| m.filename.clone()),
|
||||||
|
..Document::default()
|
||||||
};
|
};
|
||||||
st.sync.set_progress(&username, &document).await?;
|
st.sync.set_progress(&username, &document).await?;
|
||||||
Ok(Json(ProgressCreated{timestamp: document.timestamp, document: document.document_id}))
|
Ok(Json(ProgressCreated{timestamp: document.timestamp, document: document.document_id}))
|
||||||
|
|||||||
+1
-1
@@ -8,7 +8,7 @@ use tracing_subscriber::{EnvFilter, fmt, layer::SubscriberExt as _, util::Subscr
|
|||||||
#[tokio::main]
|
#[tokio::main]
|
||||||
async fn main() -> anyhow::Result<()> {
|
async fn main() -> anyhow::Result<()> {
|
||||||
let level_filter = EnvFilter::builder()
|
let level_filter = EnvFilter::builder()
|
||||||
.with_default_directive(LevelFilter::INFO.into())
|
.with_default_directive(LevelFilter::DEBUG.into())
|
||||||
.from_env_lossy();
|
.from_env_lossy();
|
||||||
tracing_subscriber::registry()
|
tracing_subscriber::registry()
|
||||||
.with(fmt::layer())
|
.with(fmt::layer())
|
||||||
|
|||||||
+143
-5
@@ -18,7 +18,7 @@ pub enum SyncError {
|
|||||||
DatabaseFailure(#[from] tokio_rusqlite::Error)
|
DatabaseFailure(#[from] tokio_rusqlite::Error)
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug, PartialEq, Clone)]
|
#[derive(Debug, PartialEq, Clone, Default)]
|
||||||
pub struct Document {
|
pub struct Document {
|
||||||
// document identifier, name of the file, md5 of the file or something
|
// document identifier, name of the file, md5 of the file or something
|
||||||
pub document_id: String,
|
pub document_id: String,
|
||||||
@@ -28,6 +28,9 @@ pub struct Document {
|
|||||||
pub device: String,
|
pub device: String,
|
||||||
pub device_id: Option<String>,
|
pub device_id: Option<String>,
|
||||||
pub timestamp: i64,
|
pub timestamp: i64,
|
||||||
|
pub title: Option<String>,
|
||||||
|
pub filename: Option<String>,
|
||||||
|
pub author: Option<String>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl ProgressSync {
|
impl ProgressSync {
|
||||||
@@ -39,7 +42,21 @@ impl ProgressSync {
|
|||||||
let username = username.to_owned();
|
let username = username.to_owned();
|
||||||
let doc = document.clone();
|
let doc = document.clone();
|
||||||
self.writer.connection.call(move |conn| -> rusqlite::Result<()> {
|
self.writer.connection.call(move |conn| -> rusqlite::Result<()> {
|
||||||
conn.execute("insert into progress (user_id, document_id, progress, percentage, device, device_id, timestamp)
|
let t = conn.transaction()?;
|
||||||
|
t.execute("insert into document
|
||||||
|
(id, title, filename, author)
|
||||||
|
values
|
||||||
|
(:document_id, :title, :filename, :author) on conflict(id) do update
|
||||||
|
set title = coalesce(excluded.title, document.title),
|
||||||
|
author = coalesce(excluded.author, document.author),
|
||||||
|
filename = coalesce(excluded.filename, document.filename)
|
||||||
|
", named_params! {
|
||||||
|
":document_id": doc.document_id,
|
||||||
|
":filename": doc.filename,
|
||||||
|
":title": doc.title,
|
||||||
|
":author": doc.author,
|
||||||
|
})?;
|
||||||
|
t.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
|
select id, :document_id, :progress, :percentage, :device, :device_id, :timestamp from users
|
||||||
where username = :username", named_params!
|
where username = :username", named_params!
|
||||||
{ ":username": username,
|
{ ":username": username,
|
||||||
@@ -50,6 +67,7 @@ impl ProgressSync {
|
|||||||
":device_id": doc.device_id,
|
":device_id": doc.device_id,
|
||||||
":timestamp": doc.timestamp,
|
":timestamp": doc.timestamp,
|
||||||
})?;
|
})?;
|
||||||
|
t.commit()?;
|
||||||
Ok(())
|
Ok(())
|
||||||
}).await?;
|
}).await?;
|
||||||
Ok(())
|
Ok(())
|
||||||
@@ -60,9 +78,10 @@ impl ProgressSync {
|
|||||||
let document_id = document_id.to_owned();
|
let document_id = document_id.to_owned();
|
||||||
let doc = self.reader.connection.call(move |conn| -> rusqlite::Result<Option<Document>> {
|
let doc = self.reader.connection.call(move |conn| -> rusqlite::Result<Option<Document>> {
|
||||||
let x : Option<Document> = conn.query_row("select
|
let x : Option<Document> = conn.query_row("select
|
||||||
document_id, progress, percentage, device, device_id, timestamp
|
document_id, progress, percentage, device, device_id, timestamp, title, author, filename
|
||||||
from progress
|
from progress
|
||||||
left join users on progress.user_id = users.id
|
left join users on progress.user_id = users.id
|
||||||
|
left join document on progress.document_id = document.id
|
||||||
where users.username = :username and progress.document_id = :document_id
|
where users.username = :username and progress.document_id = :document_id
|
||||||
order by progress.rowid desc
|
order by progress.rowid desc
|
||||||
limit 1", named_params!
|
limit 1", named_params!
|
||||||
@@ -75,7 +94,20 @@ impl ProgressSync {
|
|||||||
let device : String = row.get(3)?;
|
let device : String = row.get(3)?;
|
||||||
let device_id : Option<String> = row.get(4)?;
|
let device_id : Option<String> = row.get(4)?;
|
||||||
let timestamp : i64 = row.get(5)?;
|
let timestamp : i64 = row.get(5)?;
|
||||||
Ok(Document{ document_id, progress, percentage, device, device_id, timestamp })
|
let title : Option<String> = row.get(6)?;
|
||||||
|
let author : Option<String> = row.get(7)?;
|
||||||
|
let filename : Option<String> = row.get(8)?;
|
||||||
|
Ok(Document{
|
||||||
|
document_id,
|
||||||
|
progress,
|
||||||
|
percentage,
|
||||||
|
device,
|
||||||
|
device_id,
|
||||||
|
timestamp,
|
||||||
|
title,
|
||||||
|
author,
|
||||||
|
filename,
|
||||||
|
})
|
||||||
}).optional()?;
|
}).optional()?;
|
||||||
Ok(x)
|
Ok(x)
|
||||||
}).await?;
|
}).await?;
|
||||||
@@ -102,7 +134,15 @@ impl ProgressSync {
|
|||||||
let device : String = row.get(3)?;
|
let device : String = row.get(3)?;
|
||||||
let device_id : Option<String> = row.get(4)?;
|
let device_id : Option<String> = row.get(4)?;
|
||||||
let timestamp : i64 = row.get(5)?;
|
let timestamp : i64 = row.get(5)?;
|
||||||
Ok(Document{ document_id, progress, percentage, device, device_id, timestamp })
|
Ok(Document{
|
||||||
|
document_id,
|
||||||
|
progress,
|
||||||
|
percentage,
|
||||||
|
device,
|
||||||
|
device_id,
|
||||||
|
timestamp,
|
||||||
|
..Document::default()
|
||||||
|
})
|
||||||
})?.collect::<rusqlite::Result<Vec<_>>>()?;
|
})?.collect::<rusqlite::Result<Vec<_>>>()?;
|
||||||
Ok(x)
|
Ok(x)
|
||||||
}).await?;
|
}).await?;
|
||||||
@@ -150,6 +190,7 @@ use secrecy::SecretString;
|
|||||||
device: "kobo".to_string(),
|
device: "kobo".to_string(),
|
||||||
device_id: None,
|
device_id: None,
|
||||||
timestamp: now()?,
|
timestamp: now()?,
|
||||||
|
..Document::default()
|
||||||
};
|
};
|
||||||
|
|
||||||
sync.set_progress(&username, &doc).await?;
|
sync.set_progress(&username, &doc).await?;
|
||||||
@@ -174,6 +215,7 @@ use secrecy::SecretString;
|
|||||||
device: "kobo".to_string(),
|
device: "kobo".to_string(),
|
||||||
device_id: None,
|
device_id: None,
|
||||||
timestamp: now()?,
|
timestamp: now()?,
|
||||||
|
..Document::default()
|
||||||
};
|
};
|
||||||
let doc2 = Document {
|
let doc2 = Document {
|
||||||
document_id: document_id.clone(),
|
document_id: document_id.clone(),
|
||||||
@@ -182,6 +224,7 @@ use secrecy::SecretString;
|
|||||||
device: "kobo".to_string(),
|
device: "kobo".to_string(),
|
||||||
device_id: None,
|
device_id: None,
|
||||||
timestamp: now()?,
|
timestamp: now()?,
|
||||||
|
..Document::default()
|
||||||
};
|
};
|
||||||
|
|
||||||
sync.set_progress(&username, &doc1).await?;
|
sync.set_progress(&username, &doc1).await?;
|
||||||
@@ -208,6 +251,7 @@ use secrecy::SecretString;
|
|||||||
device: "kobo".to_string(),
|
device: "kobo".to_string(),
|
||||||
device_id: None,
|
device_id: None,
|
||||||
timestamp: now()?,
|
timestamp: now()?,
|
||||||
|
..Document::default()
|
||||||
};
|
};
|
||||||
let doc2 = Document {
|
let doc2 = Document {
|
||||||
document_id: document_id.clone(),
|
document_id: document_id.clone(),
|
||||||
@@ -216,6 +260,7 @@ use secrecy::SecretString;
|
|||||||
device: "x4".to_string(),
|
device: "x4".to_string(),
|
||||||
device_id: None,
|
device_id: None,
|
||||||
timestamp: now()? + 3,
|
timestamp: now()? + 3,
|
||||||
|
..Document::default()
|
||||||
};
|
};
|
||||||
|
|
||||||
sync.set_progress(&username, &doc1).await?;
|
sync.set_progress(&username, &doc1).await?;
|
||||||
@@ -241,6 +286,7 @@ use secrecy::SecretString;
|
|||||||
device: "kobo".to_string(),
|
device: "kobo".to_string(),
|
||||||
device_id: None,
|
device_id: None,
|
||||||
timestamp: now()?,
|
timestamp: now()?,
|
||||||
|
..Document::default()
|
||||||
};
|
};
|
||||||
let doc2 = Document {
|
let doc2 = Document {
|
||||||
document_id: document_id.clone(),
|
document_id: document_id.clone(),
|
||||||
@@ -249,6 +295,7 @@ use secrecy::SecretString;
|
|||||||
device: "kobo".to_string(),
|
device: "kobo".to_string(),
|
||||||
device_id: None,
|
device_id: None,
|
||||||
timestamp: now()? + 3,
|
timestamp: now()? + 3,
|
||||||
|
..Document::default()
|
||||||
};
|
};
|
||||||
|
|
||||||
sync.set_progress(&username, &doc1).await?;
|
sync.set_progress(&username, &doc1).await?;
|
||||||
@@ -258,4 +305,95 @@ use secrecy::SecretString;
|
|||||||
assert_eq!(got, vec![doc2, doc1]);
|
assert_eq!(got, vec![doc2, doc1]);
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[tokio::test]
|
||||||
|
async fn stores_metadata() -> 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()?,
|
||||||
|
filename: Some("foo.epub".to_string()),
|
||||||
|
title: Some("A Book".to_string()),
|
||||||
|
author: Some("A.N. Author".to_string()),
|
||||||
|
};
|
||||||
|
|
||||||
|
sync.set_progress(&username, &doc1).await?;
|
||||||
|
let got = sync.get_progress(&username, &document_id).await?;
|
||||||
|
|
||||||
|
assert_eq!(got, Some(doc1));
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tokio::test]
|
||||||
|
async fn updates_metadata() -> 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()?,
|
||||||
|
filename: Some("foo.epub".to_string()),
|
||||||
|
title: Some("A Book".to_string()),
|
||||||
|
author: Some("A.N. Author".to_string()),
|
||||||
|
};
|
||||||
|
let doc2 = Document {
|
||||||
|
filename: Some("foobar.epub".to_string()),
|
||||||
|
..doc1.clone()
|
||||||
|
};
|
||||||
|
|
||||||
|
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 metadata_is_not_renulled() -> 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()?,
|
||||||
|
filename: Some("foo.epub".to_string()),
|
||||||
|
title: Some("A Book".to_string()),
|
||||||
|
author: Some("A.N. Author".to_string()),
|
||||||
|
};
|
||||||
|
let doc2 = Document {
|
||||||
|
filename: None,
|
||||||
|
..doc1.clone()
|
||||||
|
};
|
||||||
|
|
||||||
|
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(doc1));
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
}
|
}
|
||||||
+31
-10
@@ -1,6 +1,7 @@
|
|||||||
use std::path::Path;
|
use std::path::Path;
|
||||||
|
|
||||||
use tokio_rusqlite::{Connection, rusqlite};
|
use sha2::{Digest as _, Sha256};
|
||||||
|
use tokio_rusqlite::{Connection, OptionalExtension as _, named_params, rusqlite};
|
||||||
|
|
||||||
|
|
||||||
pub struct Writer {
|
pub struct Writer {
|
||||||
@@ -9,21 +10,41 @@ pub struct Writer {
|
|||||||
pub connection: Connection,
|
pub connection: Connection,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const MIGRATIONS : [&str; 8] = [
|
||||||
|
"create table if not exists users (id primary key, username not null, secret not null, unique(username))",
|
||||||
|
"create table if not exists progress (user_id not null, document_id not null, progress not null, percentage not null, device not null, device_id, timestamp not null, unique(user_id, document_id), foreign key (user_id) references users(id))",
|
||||||
|
"create table document (id not null primary key, title, filename, author)",
|
||||||
|
"insert into document (id) select distinct(document_id) from progress",
|
||||||
|
"alter table progress rename to progress_bak",
|
||||||
|
"create table progress (user_id not null, document_id not null, progress not null, percentage not null, device not null, device_id, timestamp not null, foreign key (user_id) references users(id), foreign key (document_id) references document(id))",
|
||||||
|
"insert into progress (user_id, document_id, progress, percentage, device, device_id, timestamp) select user_id, document_id, progress, percentage, device, device_id, timestamp from progress_bak",
|
||||||
|
"drop table progress_bak",
|
||||||
|
];
|
||||||
|
|
||||||
|
struct Migration { sql: String, fingerprint: Vec<u8> }
|
||||||
|
|
||||||
|
fn get_migrations() -> Vec<Migration> {
|
||||||
|
MIGRATIONS
|
||||||
|
.iter()
|
||||||
|
.map(|m| { Migration{sql: m.to_string(), fingerprint: Sha256::digest(m).to_vec() } })
|
||||||
|
.collect()
|
||||||
|
}
|
||||||
|
|
||||||
impl Writer {
|
impl Writer {
|
||||||
|
#[tracing::instrument]
|
||||||
pub async fn new(path: &Path) -> Result<Self, tokio_rusqlite::Error> {
|
pub async fn new(path: &Path) -> Result<Self, tokio_rusqlite::Error> {
|
||||||
let connection = Connection::open(path).await?;
|
let connection = Connection::open(path).await?;
|
||||||
|
|
||||||
connection.call(|conn| -> rusqlite::Result<()> {
|
connection.call(|conn| -> rusqlite::Result<()> {
|
||||||
conn.execute("create table if not exists users (id primary key, username not null, secret not null, unique(username))", [])?;
|
conn.execute("create table if not exists migrations (hash not null, created_at)", [])?;
|
||||||
conn.execute("create table if not exists progress (user_id not null, document_id not null, progress not null, percentage not null, device not null, device_id, timestamp not null, unique(user_id, document_id), foreign key (user_id) references users(id))", [])?;
|
for Migration{sql,fingerprint} in get_migrations() {
|
||||||
|
if let None = conn.query_one("select * from migrations where hash = :hash", named_params! {":hash": fingerprint}, |_| Ok(true)).optional()? {
|
||||||
|
tracing::debug!("Applying {:?}", sql);
|
||||||
|
conn.execute(&sql, [])?;
|
||||||
|
conn.execute("insert into migrations (hash, created_at) values (:hash, :now)", named_params! {":hash": fingerprint, ":now": 0 })?;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Migrations
|
|
||||||
//
|
|
||||||
// Store history
|
|
||||||
conn.execute("alter table progress rename to progress_bak", [])?;
|
|
||||||
conn.execute("create table if not exists progress (user_id not null, document_id not null, progress not null, percentage not null, device not null, device_id, timestamp not null, foreign key (user_id) references users(id))", [])?;
|
|
||||||
conn.execute("insert into progress (user_id, document_id, progress, percentage, device, device_id, timestamp) select user_id, document_id, progress, percentage, device, device_id, timestamp from progress_bak", [])?;
|
|
||||||
conn.execute("drop table progress_bak", [])?;
|
|
||||||
Ok(())
|
Ok(())
|
||||||
}).await?;
|
}).await?;
|
||||||
|
|
||||||
|
|||||||
Reference in new issue
Block a user