try to implement items/sync
* we need to figure out a way to test this API
This commit is contained in:
parent
15809cd545
commit
4be4de241f
33
Cargo.lock
generated
33
Cargo.lock
generated
|
@ -96,6 +96,18 @@ version = "0.1.10"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "4785bdd1c96b2a846b2bd7cc02e86b6b3dbf14e7e53446c4f54c92a361040822"
|
checksum = "4785bdd1c96b2a846b2bd7cc02e86b6b3dbf14e7e53446c4f54c92a361040822"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "chrono"
|
||||||
|
version = "0.4.10"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "31850b4a4d6bae316f7a09e691c944c28299298837edc0a03f755618c23cbc01"
|
||||||
|
dependencies = [
|
||||||
|
"num-integer",
|
||||||
|
"num-traits",
|
||||||
|
"serde",
|
||||||
|
"time",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "cloudabi"
|
name = "cloudabi"
|
||||||
version = "0.0.3"
|
version = "0.0.3"
|
||||||
|
@ -166,6 +178,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "9d7cc03b910de9935007861dce440881f69102aaaedfd4bc5a6f40340ca5840c"
|
checksum = "9d7cc03b910de9935007861dce440881f69102aaaedfd4bc5a6f40340ca5840c"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"byteorder",
|
"byteorder",
|
||||||
|
"chrono",
|
||||||
"diesel_derives",
|
"diesel_derives",
|
||||||
"libsqlite3-sys",
|
"libsqlite3-sys",
|
||||||
"r2d2",
|
"r2d2",
|
||||||
|
@ -584,6 +597,25 @@ dependencies = [
|
||||||
"winapi 0.3.8",
|
"winapi 0.3.8",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "num-integer"
|
||||||
|
version = "0.1.42"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "3f6ea62e9d81a77cd3ee9a2a5b9b609447857f3d358704331e4ef39eb247fcba"
|
||||||
|
dependencies = [
|
||||||
|
"autocfg",
|
||||||
|
"num-traits",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "num-traits"
|
||||||
|
version = "0.2.11"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "c62be47e61d1842b9170f0fdeec8eba98e60e90e5446449a0545e5152acd7096"
|
||||||
|
dependencies = [
|
||||||
|
"autocfg",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "num_cpus"
|
name = "num_cpus"
|
||||||
version = "1.12.0"
|
version = "1.12.0"
|
||||||
|
@ -1001,6 +1033,7 @@ dependencies = [
|
||||||
name = "sfrs"
|
name = "sfrs"
|
||||||
version = "0.1.0"
|
version = "0.1.0"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
|
"chrono",
|
||||||
"diesel",
|
"diesel",
|
||||||
"diesel_migrations",
|
"diesel_migrations",
|
||||||
"dotenv",
|
"dotenv",
|
||||||
|
|
|
@ -8,10 +8,11 @@ edition = "2018"
|
||||||
rocket = "0.4.2"
|
rocket = "0.4.2"
|
||||||
rocket_contrib = { version = "0.4.2", features = ["diesel_sqlite_pool"] }
|
rocket_contrib = { version = "0.4.2", features = ["diesel_sqlite_pool"] }
|
||||||
jwt = "0.4.0"
|
jwt = "0.4.0"
|
||||||
diesel = { version = "1.4.3", features = ["sqlite"] }
|
diesel = { version = "1.4.3", features = ["sqlite", "chrono"] }
|
||||||
diesel_migrations = "1.4.0"
|
diesel_migrations = "1.4.0"
|
||||||
dotenv = "0.9.0"
|
dotenv = "0.9.0"
|
||||||
lazy_static = "1.4.0"
|
lazy_static = "1.4.0"
|
||||||
serde = { version = "1.0.104", features = ["derive"] }
|
serde = { version = "1.0.104", features = ["derive"] }
|
||||||
scrypt = "0.2.0"
|
scrypt = "0.2.0"
|
||||||
rust-crypto = "0.2.36"
|
rust-crypto = "0.2.36"
|
||||||
|
chrono = { version = "0.4", features = ["serde"] }
|
|
@ -6,8 +6,8 @@ CREATE TABLE items (
|
||||||
content_type VARCHAR NOT NULL,
|
content_type VARCHAR NOT NULL,
|
||||||
enc_item_key VARCHAR,
|
enc_item_key VARCHAR,
|
||||||
deleted BOOLEAN NOT NULL,
|
deleted BOOLEAN NOT NULL,
|
||||||
created_at DATE NOT NULL,
|
created_at DATETIME NOT NULL,
|
||||||
updated_at DATE NOT NULL,
|
updated_at DATETIME,
|
||||||
FOREIGN KEY (owner)
|
FOREIGN KEY (owner)
|
||||||
REFERENCES users (id)
|
REFERENCES users (id)
|
||||||
)
|
)
|
88
src/api.rs
88
src/api.rs
|
@ -1,5 +1,6 @@
|
||||||
use crate::DbConn;
|
use crate::DbConn;
|
||||||
use crate::user;
|
use crate::user;
|
||||||
|
use crate::item;
|
||||||
use rocket::http::Status;
|
use rocket::http::Status;
|
||||||
use rocket::response::status::Custom;
|
use rocket::response::status::Custom;
|
||||||
use rocket_contrib::json::Json;
|
use rocket_contrib::json::Json;
|
||||||
|
@ -12,7 +13,8 @@ pub fn routes() -> impl Into<Vec<rocket::Route>> {
|
||||||
auth_change_pw,
|
auth_change_pw,
|
||||||
auth_sign_in,
|
auth_sign_in,
|
||||||
auth_params,
|
auth_params,
|
||||||
auth_ping
|
auth_ping,
|
||||||
|
items_sync
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -126,4 +128,88 @@ fn auth_change_pw(db: DbConn, params: Json<ChangePwParams>) -> Custom<JsonResp<(
|
||||||
#[get("/auth/ping")]
|
#[get("/auth/ping")]
|
||||||
fn auth_ping(_db: DbConn, u: user::User) -> Custom<JsonResp<String>> {
|
fn auth_ping(_db: DbConn, u: user::User) -> Custom<JsonResp<String>> {
|
||||||
Custom(Status::Ok, Json(Response::Success(u.email)))
|
Custom(Status::Ok, Json(Response::Success(u.email)))
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Deserialize)]
|
||||||
|
struct SyncParams {
|
||||||
|
items: Vec<item::SyncItem>,
|
||||||
|
sync_token: Option<String>,
|
||||||
|
cursor_token: Option<String>,
|
||||||
|
limit: Option<i64>
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Serialize)]
|
||||||
|
struct SyncResp {
|
||||||
|
retrieved_items: Vec<item::SyncItem>,
|
||||||
|
saved_items: Vec<item::SyncItem>,
|
||||||
|
unsaved: Vec<item::SyncItem>,
|
||||||
|
sync_token: Option<String>, // for convenience, we will actually always return this
|
||||||
|
cursor_token: Option<String>
|
||||||
|
}
|
||||||
|
|
||||||
|
#[post("/items/sync", format = "json", data = "<params>")]
|
||||||
|
fn items_sync(db: DbConn, u: user::User, params: Json<SyncParams>) -> Custom<JsonResp<SyncResp>> {
|
||||||
|
let mut resp = SyncResp {
|
||||||
|
retrieved_items: vec![],
|
||||||
|
saved_items: vec![],
|
||||||
|
unsaved: vec![],
|
||||||
|
sync_token: None,
|
||||||
|
cursor_token: None
|
||||||
|
};
|
||||||
|
|
||||||
|
let inner_params = params.into_inner();
|
||||||
|
|
||||||
|
// First, update all items sent by client
|
||||||
|
for it in inner_params.items.into_iter() {
|
||||||
|
if let Err(item::ItemOpError(_)) = item::SyncItem::items_insert(&db, &u, &it) {
|
||||||
|
// Let's not fail just because one of them...
|
||||||
|
// At least the client will know there's an error
|
||||||
|
// (maybe mistakes it for conflict)
|
||||||
|
resp.unsaved.push(it);
|
||||||
|
} else {
|
||||||
|
resp.saved_items.push(it);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
let mut from_id: Option<i64> = None;
|
||||||
|
let mut max_id: Option<i64> = None;
|
||||||
|
|
||||||
|
if let Some(cursor_token) = inner_params.cursor_token {
|
||||||
|
// If the client provides cursor_token,
|
||||||
|
// then, we return all records
|
||||||
|
// until sync_token (the head of the last sync)
|
||||||
|
from_id = cursor_token.parse().ok();
|
||||||
|
max_id = inner_params.sync_token.clone()
|
||||||
|
.and_then(|i| i.parse().ok());
|
||||||
|
} else if let Some(sync_token) = inner_params.sync_token {
|
||||||
|
// If there is no cursor_token, then we are doing
|
||||||
|
// a normal sync, so just return all records from sync_token
|
||||||
|
from_id = sync_token.parse().ok();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Then, retrieve what the client needs
|
||||||
|
let result = item::SyncItem::items_of_user(&db, &u,
|
||||||
|
from_id, max_id, inner_params.limit);
|
||||||
|
|
||||||
|
match result {
|
||||||
|
Err(item::ItemOpError(e)) => {
|
||||||
|
error_resp(Status::InternalServerError, vec![e])
|
||||||
|
},
|
||||||
|
Ok(items) => {
|
||||||
|
if !items.is_empty() {
|
||||||
|
// max_id = the last sync token
|
||||||
|
// if we still haven't reached the last sync token yet,
|
||||||
|
// return a new cursor token and keep the sync token
|
||||||
|
if let Some(max_id) = max_id {
|
||||||
|
resp.cursor_token = Some(items[0].id.to_string());
|
||||||
|
resp.sync_token = Some(max_id.to_string());
|
||||||
|
} else {
|
||||||
|
// Else, use the current max id as the sync_token
|
||||||
|
resp.sync_token = Some(items[0].id.to_string());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
resp.retrieved_items = items.into_iter().map(|x| x.into()).collect();
|
||||||
|
Custom(Status::Ok, Json(Response::Success(resp)))
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
139
src/item.rs
Normal file
139
src/item.rs
Normal file
|
@ -0,0 +1,139 @@
|
||||||
|
use crate::schema::items;
|
||||||
|
use crate::schema::items::dsl::*;
|
||||||
|
use crate::{lock_db_write, lock_db_read};
|
||||||
|
use crate::user;
|
||||||
|
use chrono::naive::NaiveDateTime;
|
||||||
|
use diesel::prelude::*;
|
||||||
|
use diesel::sqlite::SqliteConnection;
|
||||||
|
use serde::{Serialize, Deserialize};
|
||||||
|
use std::vec::Vec;
|
||||||
|
|
||||||
|
#[derive(Debug)]
|
||||||
|
pub struct ItemOpError(pub String);
|
||||||
|
|
||||||
|
impl ItemOpError {
|
||||||
|
fn new(s: impl Into<String>) -> ItemOpError {
|
||||||
|
ItemOpError(s.into())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Into<ItemOpError> for &str {
|
||||||
|
fn into(self) -> ItemOpError {
|
||||||
|
ItemOpError::new(self)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Queryable)]
|
||||||
|
pub struct Item {
|
||||||
|
pub id: i64,
|
||||||
|
pub owner: i32,
|
||||||
|
pub uuid: String,
|
||||||
|
pub content: Option<String>,
|
||||||
|
pub content_type: String,
|
||||||
|
pub enc_item_key: Option<String>,
|
||||||
|
pub deleted: bool,
|
||||||
|
pub created_at: NaiveDateTime,
|
||||||
|
pub updated_at: Option<NaiveDateTime>
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Insertable)]
|
||||||
|
#[table_name = "items"]
|
||||||
|
struct InsertItem {
|
||||||
|
owner: i32,
|
||||||
|
uuid: String,
|
||||||
|
content: Option<String>,
|
||||||
|
content_type: String,
|
||||||
|
enc_item_key: Option<String>,
|
||||||
|
deleted: bool,
|
||||||
|
created_at: NaiveDateTime,
|
||||||
|
updated_at: Option<NaiveDateTime>
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Serialize, Deserialize)]
|
||||||
|
pub struct SyncItem {
|
||||||
|
pub uuid: String,
|
||||||
|
pub content: Option<String>,
|
||||||
|
pub content_type: String,
|
||||||
|
pub enc_item_key: Option<String>,
|
||||||
|
pub deleted: bool,
|
||||||
|
pub created_at: NaiveDateTime,
|
||||||
|
pub updated_at: Option<NaiveDateTime>
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Into<SyncItem> for Item {
|
||||||
|
fn into(self) -> SyncItem {
|
||||||
|
SyncItem {
|
||||||
|
uuid: self.uuid,
|
||||||
|
content: self.content,
|
||||||
|
content_type: self.content_type,
|
||||||
|
enc_item_key: self.enc_item_key,
|
||||||
|
deleted: self.deleted,
|
||||||
|
created_at: self.created_at,
|
||||||
|
updated_at: self.updated_at
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl SyncItem {
|
||||||
|
pub fn items_of_user(
|
||||||
|
db: &SqliteConnection, u: &user::User,
|
||||||
|
since_id: Option<i64>, max_id: Option<i64>,
|
||||||
|
limit: Option<i64>
|
||||||
|
) -> Result<Vec<Item>, ItemOpError> {
|
||||||
|
lock_db_read!()
|
||||||
|
.and_then(|_| {
|
||||||
|
let mut stmt = items.filter(owner.eq(u.id)).into_boxed();
|
||||||
|
if let Some(limit) = limit {
|
||||||
|
stmt = stmt.limit(limit);
|
||||||
|
}
|
||||||
|
|
||||||
|
if let Some(since_id) = since_id {
|
||||||
|
stmt = stmt.filter(id.gt(since_id));
|
||||||
|
}
|
||||||
|
|
||||||
|
if let Some(max_id) = max_id {
|
||||||
|
stmt = stmt.filter(id.le(max_id));
|
||||||
|
}
|
||||||
|
|
||||||
|
stmt.order(id.desc())
|
||||||
|
.load::<Item>(db)
|
||||||
|
.map_err(|_| "Database error".into())
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn items_insert(db: &SqliteConnection, u: &user::User, it: &SyncItem) -> Result<(), ItemOpError> {
|
||||||
|
// First, try to find the original item, if any, delete it, and insert a new one with the same UUID
|
||||||
|
// This way, the ID is updated each time an item is updated
|
||||||
|
// This method acts both as insertion and update
|
||||||
|
let orig = lock_db_read!()
|
||||||
|
.and_then(|_| {
|
||||||
|
items.filter(uuid.eq(&it.uuid).and(owner.eq(u.id)))
|
||||||
|
.load::<Item>(db)
|
||||||
|
.map_err(|_| "Database error".into())
|
||||||
|
})?;
|
||||||
|
// TODO: Detect sync conflict? similar to the Go version.
|
||||||
|
|
||||||
|
let _lock = lock_db_write!()?;
|
||||||
|
if !orig.is_empty() {
|
||||||
|
diesel::delete(items.filter(uuid.eq(&it.uuid).and(owner.eq(u.id))))
|
||||||
|
.execute(db)
|
||||||
|
.map(|_| ())
|
||||||
|
.map_err(|_| "Database error".into())?;
|
||||||
|
}
|
||||||
|
|
||||||
|
diesel::insert_into(items::table)
|
||||||
|
.values(InsertItem {
|
||||||
|
owner: u.id,
|
||||||
|
uuid: it.uuid.clone(),
|
||||||
|
content: if it.deleted { None } else { it.content.clone() },
|
||||||
|
content_type: it.content_type.clone(),
|
||||||
|
enc_item_key: if it.deleted { None } else { it.enc_item_key.clone() },
|
||||||
|
deleted: it.deleted,
|
||||||
|
created_at: it.created_at,
|
||||||
|
updated_at: it.updated_at
|
||||||
|
})
|
||||||
|
.execute(db)
|
||||||
|
.map(|_| ())
|
||||||
|
.map_err(|_| "Database error".into())
|
||||||
|
}
|
||||||
|
}
|
|
@ -19,6 +19,7 @@ extern crate lazy_static;
|
||||||
mod schema;
|
mod schema;
|
||||||
mod api;
|
mod api;
|
||||||
mod user;
|
mod user;
|
||||||
|
mod item;
|
||||||
|
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
mod tests;
|
mod tests;
|
||||||
|
|
|
@ -1,14 +1,14 @@
|
||||||
table! {
|
table! {
|
||||||
items (id) {
|
items (id) {
|
||||||
id -> Integer,
|
id -> BigInt, // Forced, diesel does not support intepreting Integer as i64
|
||||||
owner -> Integer,
|
owner -> Integer,
|
||||||
uuid -> Text,
|
uuid -> Text,
|
||||||
content -> Nullable<Text>,
|
content -> Nullable<Text>,
|
||||||
content_type -> Text,
|
content_type -> Text,
|
||||||
enc_item_key -> Nullable<Text>,
|
enc_item_key -> Nullable<Text>,
|
||||||
deleted -> Bool,
|
deleted -> Bool,
|
||||||
created_at -> Date,
|
created_at -> Timestamp,
|
||||||
updated_at -> Date,
|
updated_at -> Nullable<Timestamp>,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
Loading…
Reference in a new issue