use crate::error::*;
use crate::login::{LocalLogin, Login, MirrorLogin, SyncStatus};
use crate::util;
use rusqlite::{named_params, Connection};
use sql_support::SqlInterruptScope;
use std::time::SystemTime;
use sync15::ServerTimestamp;
use sync_guid::Guid;
#[derive(Default, Debug, Clone)]
pub(crate) struct UpdatePlan {
pub delete_mirror: Vec<Guid>,
pub delete_local: Vec<Guid>,
pub local_updates: Vec<MirrorLogin>,
pub mirror_inserts: Vec<(Login, i64, bool)>,
pub mirror_updates: Vec<(Login, i64)>,
}
impl UpdatePlan {
pub fn plan_two_way_merge(&mut self, local: &Login, upstream: (Login, ServerTimestamp)) {
let is_override = local.time_password_changed > upstream.0.time_password_changed;
self.mirror_inserts
.push((upstream.0, upstream.1.as_millis() as i64, is_override));
if !is_override {
self.delete_local.push(local.guid.clone());
}
}
pub fn plan_three_way_merge(
&mut self,
local: LocalLogin,
shared: MirrorLogin,
upstream: Login,
upstream_time: ServerTimestamp,
server_now: ServerTimestamp,
) {
let local_age = SystemTime::now()
.duration_since(local.local_modified)
.unwrap_or_default();
let remote_age = server_now.duration_since(upstream_time).unwrap_or_default();
let local_delta = local.login.delta(&shared.login);
let upstream_delta = upstream.delta(&shared.login);
let merged_delta = local_delta.merge(upstream_delta, remote_age < local_age);
self.mirror_updates
.push((upstream, upstream_time.as_millis() as i64));
let mut new = shared;
new.login.apply_delta(merged_delta);
new.server_modified = upstream_time;
self.local_updates.push(new);
}
pub fn plan_delete(&mut self, id: Guid) {
self.delete_local.push(id.clone());
self.delete_mirror.push(id);
}
pub fn plan_mirror_update(&mut self, login: Login, time: ServerTimestamp) {
self.mirror_updates.push((login, time.as_millis() as i64));
}
pub fn plan_mirror_insert(&mut self, login: Login, time: ServerTimestamp, is_override: bool) {
self.mirror_inserts
.push((login, time.as_millis() as i64, is_override));
}
fn perform_deletes(&self, conn: &Connection, scope: &SqlInterruptScope) -> Result<()> {
sql_support::each_chunk(&self.delete_local, |chunk, _| -> Result<()> {
conn.execute(
&format!(
"DELETE FROM loginsL WHERE guid IN ({vars})",
vars = sql_support::repeat_sql_vars(chunk.len())
),
chunk,
)?;
scope.err_if_interrupted()?;
Ok(())
})?;
sql_support::each_chunk(&self.delete_mirror, |chunk, _| {
conn.execute(
&format!(
"DELETE FROM loginsM WHERE guid IN ({vars})",
vars = sql_support::repeat_sql_vars(chunk.len())
),
chunk,
)?;
Ok(())
})
}
fn perform_mirror_updates(&self, conn: &Connection, scope: &SqlInterruptScope) -> Result<()> {
let sql = "
UPDATE loginsM
SET server_modified = :server_modified,
httpRealm = :http_realm,
formSubmitURL = :form_submit_url,
usernameField = :username_field,
passwordField = :password_field,
password = :password,
hostname = :hostname,
username = :username,
-- Avoid zeroes if the remote has been overwritten by an older client.
timesUsed = coalesce(nullif(:times_used, 0), timesUsed),
timeLastUsed = coalesce(nullif(:time_last_used, 0), timeLastUsed),
timePasswordChanged = coalesce(nullif(:time_password_changed, 0), timePasswordChanged),
timeCreated = coalesce(nullif(:time_created, 0), timeCreated)
WHERE guid = :guid
";
let mut stmt = conn.prepare_cached(sql)?;
for (login, timestamp) in &self.mirror_updates {
log::trace!("Updating mirror {:?}", login.guid_str());
stmt.execute_named(named_params! {
":server_modified": *timestamp,
":http_realm": login.http_realm,
":form_submit_url": login.form_submit_url,
":username_field": login.username_field,
":password_field": login.password_field,
":password": login.password,
":hostname": login.hostname,
":username": login.username,
":times_used": login.times_used,
":time_last_used": login.time_last_used,
":time_password_changed": login.time_password_changed,
":time_created": login.time_created,
":guid": login.guid_str(),
})?;
scope.err_if_interrupted()?;
}
Ok(())
}
fn perform_mirror_inserts(&self, conn: &Connection, scope: &SqlInterruptScope) -> Result<()> {
let sql = "
INSERT OR IGNORE INTO loginsM (
is_overridden,
server_modified,
httpRealm,
formSubmitURL,
usernameField,
passwordField,
password,
hostname,
username,
timesUsed,
timeLastUsed,
timePasswordChanged,
timeCreated,
guid
) VALUES (
:is_overridden,
:server_modified,
:http_realm,
:form_submit_url,
:username_field,
:password_field,
:password,
:hostname,
:username,
:times_used,
:time_last_used,
:time_password_changed,
:time_created,
:guid
)";
let mut stmt = conn.prepare_cached(&sql)?;
for (login, timestamp, is_overridden) in &self.mirror_inserts {
log::trace!("Inserting mirror {:?}", login.guid_str());
stmt.execute_named(named_params! {
":is_overridden": *is_overridden,
":server_modified": *timestamp,
":http_realm": login.http_realm,
":form_submit_url": login.form_submit_url,
":username_field": login.username_field,
":password_field": login.password_field,
":password": login.password,
":hostname": login.hostname,
":username": login.username,
":times_used": login.times_used,
":time_last_used": login.time_last_used,
":time_password_changed": login.time_password_changed,
":time_created": login.time_created,
":guid": login.guid_str(),
})?;
scope.err_if_interrupted()?;
}
Ok(())
}
fn perform_local_updates(&self, conn: &Connection, scope: &SqlInterruptScope) -> Result<()> {
let sql = format!(
"UPDATE loginsL
SET local_modified = :local_modified,
httpRealm = :http_realm,
formSubmitURL = :form_submit_url,
usernameField = :username_field,
passwordField = :password_field,
timeLastUsed = :time_last_used,
timePasswordChanged = :time_password_changed,
timesUsed = :times_used,
password = :password,
hostname = :hostname,
username = :username,
sync_status = {changed}
WHERE guid = :guid",
changed = SyncStatus::Changed as u8
);
let mut stmt = conn.prepare_cached(&sql)?;
let local_ms: i64 = util::system_time_ms_i64(SystemTime::now());
for l in &self.local_updates {
log::trace!("Updating local {:?}", l.guid_str());
stmt.execute_named(named_params! {
":local_modified": local_ms,
":http_realm": l.login.http_realm,
":form_submit_url": l.login.form_submit_url,
":username_field": l.login.username_field,
":password_field": l.login.password_field,
":password": l.login.password,
":hostname": l.login.hostname,
":username": l.login.username,
":time_last_used": l.login.time_last_used,
":time_password_changed": l.login.time_password_changed,
":times_used": l.login.times_used,
":guid": l.guid_str(),
})?;
scope.err_if_interrupted()?;
}
Ok(())
}
pub fn execute(&self, conn: &Connection, scope: &SqlInterruptScope) -> Result<()> {
log::debug!("UpdatePlan: deleting records...");
self.perform_deletes(conn, scope)?;
log::debug!("UpdatePlan: Updating existing mirror records...");
self.perform_mirror_updates(conn, scope)?;
log::debug!("UpdatePlan: Inserting new mirror records...");
self.perform_mirror_inserts(conn, scope)?;
log::debug!("UpdatePlan: Updating reconciled local records...");
self.perform_local_updates(conn, scope)?;
Ok(())
}
}