move sync/network errors into separate file

This commit is contained in:
Damien Elmes 2021-04-01 17:02:24 +10:00
parent 8363fcf2a8
commit af37164fba
9 changed files with 224 additions and 205 deletions

View File

@ -16,10 +16,12 @@ pub(super) fn anki_error_to_proto_error(err: AnkiError, tr: &I18n) -> pb::Backen
AnkiError::TemplateError { .. } => V::TemplateParse(pb::Empty {}),
AnkiError::IoError { .. } => V::IoError(pb::Empty {}),
AnkiError::DbError { .. } => V::DbError(pb::Empty {}),
AnkiError::NetworkError { kind, .. } => {
V::NetworkError(pb::NetworkError { kind: kind.into() })
}
AnkiError::SyncError { kind, .. } => V::SyncError(pb::SyncError { kind: kind.into() }),
AnkiError::NetworkError(err) => V::NetworkError(pb::NetworkError {
kind: err.kind.into(),
}),
AnkiError::SyncError(err) => V::SyncError(pb::SyncError {
kind: err.kind.into(),
}),
AnkiError::Interrupted => V::Interrupted(pb::Empty {}),
AnkiError::CollectionNotOpen => V::InvalidInput(pb::Empty {}),
AnkiError::CollectionAlreadyOpen => V::InvalidInput(pb::Empty {}),

View File

@ -24,12 +24,13 @@ impl Backend {
F: FnOnce(&mut LocalServer) -> Result<T>,
{
let mut state_guard = self.state.lock().unwrap();
let out = func(state_guard.sync.http_sync_server.as_mut().ok_or_else(|| {
AnkiError::SyncError {
kind: SyncErrorKind::SyncNotStarted,
info: Default::default(),
}
})?);
let out = func(
state_guard
.sync
.http_sync_server
.as_mut()
.ok_or_else(|| AnkiError::sync_error("", SyncErrorKind::SyncNotStarted))?,
);
if out.is_err() {
self.abort_and_restore_collection(Some(state_guard))
}
@ -81,10 +82,7 @@ impl Backend {
.sync
.http_sync_server
.take()
.ok_or_else(|| AnkiError::SyncError {
kind: SyncErrorKind::SyncNotStarted,
info: String::new(),
})
.ok_or_else(|| AnkiError::sync_error("", SyncErrorKind::SyncNotStarted))
}
fn start(&self, input: StartIn) -> Result<Graves> {

View File

@ -2,16 +2,17 @@
// License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
mod db;
mod network;
mod search;
pub use {
db::DbErrorKind,
network::{NetworkError, NetworkErrorKind, SyncError, SyncErrorKind},
search::{ParseError, SearchErrorKind},
};
use crate::i18n::I18n;
pub use failure::{Error, Fail};
use reqwest::StatusCode;
use std::io;
use tempfile::PathPersistError;
@ -34,14 +35,11 @@ pub enum AnkiError {
#[fail(display = "DB error: {}", info)]
DbError { info: String, kind: DbErrorKind },
#[fail(display = "Network error: {:?} {}", kind, info)]
NetworkError {
info: String,
kind: NetworkErrorKind,
},
#[fail(display = "Network error: {:?}", _0)]
NetworkError(NetworkError),
#[fail(display = "Sync error: {:?}, {}", kind, info)]
SyncError { info: String, kind: SyncErrorKind },
#[fail(display = "Sync error: {:?}", _0)]
SyncError(SyncError),
#[fail(display = "JSON encode/decode error: {}", info)]
JsonError { info: String },
@ -87,45 +85,13 @@ impl AnkiError {
}
pub(crate) fn server_message<S: Into<String>>(msg: S) -> AnkiError {
AnkiError::SyncError {
info: msg.into(),
kind: SyncErrorKind::ServerMessage,
}
}
pub(crate) fn sync_misc<S: Into<String>>(msg: S) -> AnkiError {
AnkiError::SyncError {
info: msg.into(),
kind: SyncErrorKind::Other,
}
AnkiError::sync_error(msg, SyncErrorKind::ServerMessage)
}
pub fn localized_description(&self, tr: &I18n) -> String {
match self {
AnkiError::SyncError { info, kind } => match kind {
SyncErrorKind::ServerMessage => info.into(),
SyncErrorKind::Other => info.into(),
SyncErrorKind::Conflict => tr.sync_conflict(),
SyncErrorKind::ServerError => tr.sync_server_error(),
SyncErrorKind::ClientTooOld => tr.sync_client_too_old(),
SyncErrorKind::AuthFailed => tr.sync_wrong_pass(),
SyncErrorKind::ResyncRequired => tr.sync_resync_required(),
SyncErrorKind::ClockIncorrect => tr.sync_clock_off(),
SyncErrorKind::DatabaseCheckRequired => tr.sync_sanity_check_failed(),
// server message
SyncErrorKind::SyncNotStarted => "sync not started".into(),
}
.into(),
AnkiError::NetworkError { kind, info } => {
let summary = match kind {
NetworkErrorKind::Offline => tr.network_offline(),
NetworkErrorKind::Timeout => tr.network_timeout(),
NetworkErrorKind::ProxyAuth => tr.network_proxy_auth(),
NetworkErrorKind::Other => tr.network_other(),
};
let details = tr.network_details(info.as_str());
format!("{}\n\n{}", summary, details)
}
AnkiError::SyncError(err) => err.localized_description(tr),
AnkiError::NetworkError(err) => err.localized_description(tr),
AnkiError::TemplateError { info } => {
// already localized
info.into()
@ -176,114 +142,6 @@ impl From<io::Error> for AnkiError {
}
}
#[derive(Debug, PartialEq)]
pub enum NetworkErrorKind {
Offline,
Timeout,
ProxyAuth,
Other,
}
impl From<reqwest::Error> for AnkiError {
fn from(err: reqwest::Error) -> Self {
let url = err.url().map(|url| url.as_str()).unwrap_or("");
let str_err = format!("{}", err);
// strip url from error to avoid exposing keys
let info = str_err.replace(url, "");
if err.is_timeout() {
AnkiError::NetworkError {
info,
kind: NetworkErrorKind::Timeout,
}
} else if err.is_status() {
error_for_status_code(info, err.status().unwrap())
} else {
guess_reqwest_error(info)
}
}
}
#[derive(Debug, PartialEq)]
pub enum SyncErrorKind {
Conflict,
ServerError,
ClientTooOld,
AuthFailed,
ServerMessage,
ClockIncorrect,
Other,
ResyncRequired,
DatabaseCheckRequired,
SyncNotStarted,
}
fn error_for_status_code(info: String, code: StatusCode) -> AnkiError {
use reqwest::StatusCode as S;
match code {
S::PROXY_AUTHENTICATION_REQUIRED => AnkiError::NetworkError {
info,
kind: NetworkErrorKind::ProxyAuth,
},
S::CONFLICT => AnkiError::SyncError {
info,
kind: SyncErrorKind::Conflict,
},
S::FORBIDDEN => AnkiError::SyncError {
info,
kind: SyncErrorKind::AuthFailed,
},
S::NOT_IMPLEMENTED => AnkiError::SyncError {
info,
kind: SyncErrorKind::ClientTooOld,
},
S::INTERNAL_SERVER_ERROR | S::BAD_GATEWAY | S::GATEWAY_TIMEOUT | S::SERVICE_UNAVAILABLE => {
AnkiError::SyncError {
info,
kind: SyncErrorKind::ServerError,
}
}
S::BAD_REQUEST => AnkiError::SyncError {
info,
kind: SyncErrorKind::DatabaseCheckRequired,
},
_ => AnkiError::NetworkError {
info,
kind: NetworkErrorKind::Other,
},
}
}
fn guess_reqwest_error(mut info: String) -> AnkiError {
if info.contains("dns error: cancelled") {
return AnkiError::Interrupted;
}
let kind = if info.contains("unreachable") || info.contains("dns") {
NetworkErrorKind::Offline
} else if info.contains("timed out") {
NetworkErrorKind::Timeout
} else {
if info.contains("invalid type") {
info = format!(
"{} {} {}\n\n{}",
"Please force a full sync in the Preferences screen to bring your devices into sync.",
"Then, please use the Check Database feature, and sync to your other devices.",
"If problems persist, please post on the support forum.",
info,
);
}
NetworkErrorKind::Other
};
AnkiError::NetworkError { info, kind }
}
impl From<zip::result::ZipError> for AnkiError {
fn from(err: zip::result::ZipError) -> Self {
AnkiError::sync_misc(err.to_string())
}
}
impl From<serde_json::Error> for AnkiError {
fn from(err: serde_json::Error) -> Self {
AnkiError::JsonError {

167
rslib/src/error/network.rs Normal file
View File

@ -0,0 +1,167 @@
// Copyright: Ankitects Pty Ltd and contributors
// License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
use super::AnkiError;
use anki_i18n::I18n;
use reqwest::StatusCode;
#[derive(Debug, PartialEq)]
pub struct NetworkError {
pub info: String,
pub kind: NetworkErrorKind,
}
#[derive(Debug, PartialEq)]
pub enum NetworkErrorKind {
Offline,
Timeout,
ProxyAuth,
Other,
}
#[derive(Debug, PartialEq)]
pub struct SyncError {
pub info: String,
pub kind: SyncErrorKind,
}
#[derive(Debug, PartialEq)]
pub enum SyncErrorKind {
Conflict,
ServerError,
ClientTooOld,
AuthFailed,
ServerMessage,
ClockIncorrect,
Other,
ResyncRequired,
DatabaseCheckRequired,
SyncNotStarted,
}
impl AnkiError {
pub(crate) fn sync_error(info: impl Into<String>, kind: SyncErrorKind) -> Self {
AnkiError::SyncError(SyncError {
info: info.into(),
kind,
})
}
}
impl From<reqwest::Error> for AnkiError {
fn from(err: reqwest::Error) -> Self {
let url = err.url().map(|url| url.as_str()).unwrap_or("");
let str_err = format!("{}", err);
// strip url from error to avoid exposing keys
let info = str_err.replace(url, "");
if err.is_timeout() {
AnkiError::NetworkError(NetworkError {
info,
kind: NetworkErrorKind::Timeout,
})
} else if err.is_status() {
error_for_status_code(info, err.status().unwrap())
} else {
guess_reqwest_error(info)
}
}
}
fn error_for_status_code(info: String, code: StatusCode) -> AnkiError {
use reqwest::StatusCode as S;
match code {
S::PROXY_AUTHENTICATION_REQUIRED => AnkiError::NetworkError(NetworkError {
info,
kind: NetworkErrorKind::ProxyAuth,
}),
S::CONFLICT => AnkiError::SyncError(SyncError {
info,
kind: SyncErrorKind::Conflict,
}),
S::FORBIDDEN => AnkiError::SyncError(SyncError {
info,
kind: SyncErrorKind::AuthFailed,
}),
S::NOT_IMPLEMENTED => AnkiError::SyncError(SyncError {
info,
kind: SyncErrorKind::ClientTooOld,
}),
S::INTERNAL_SERVER_ERROR | S::BAD_GATEWAY | S::GATEWAY_TIMEOUT | S::SERVICE_UNAVAILABLE => {
AnkiError::SyncError(SyncError {
info,
kind: SyncErrorKind::ServerError,
})
}
S::BAD_REQUEST => AnkiError::SyncError(SyncError {
info,
kind: SyncErrorKind::DatabaseCheckRequired,
}),
_ => AnkiError::NetworkError(NetworkError {
info,
kind: NetworkErrorKind::Other,
}),
}
}
fn guess_reqwest_error(mut info: String) -> AnkiError {
if info.contains("dns error: cancelled") {
return AnkiError::Interrupted;
}
let kind = if info.contains("unreachable") || info.contains("dns") {
NetworkErrorKind::Offline
} else if info.contains("timed out") {
NetworkErrorKind::Timeout
} else {
if info.contains("invalid type") {
info = format!(
"{} {} {}\n\n{}",
"Please force a full sync in the Preferences screen to bring your devices into sync.",
"Then, please use the Check Database feature, and sync to your other devices.",
"If problems persist, please post on the support forum.",
info,
);
}
NetworkErrorKind::Other
};
AnkiError::NetworkError(NetworkError { info, kind })
}
impl From<zip::result::ZipError> for AnkiError {
fn from(err: zip::result::ZipError) -> Self {
AnkiError::sync_error(err.to_string(), SyncErrorKind::Other)
}
}
impl SyncError {
pub fn localized_description(&self, tr: &I18n) -> String {
match self.kind {
SyncErrorKind::ServerMessage => self.info.clone().into(),
SyncErrorKind::Other => self.info.clone().into(),
SyncErrorKind::Conflict => tr.sync_conflict(),
SyncErrorKind::ServerError => tr.sync_server_error(),
SyncErrorKind::ClientTooOld => tr.sync_client_too_old(),
SyncErrorKind::AuthFailed => tr.sync_wrong_pass(),
SyncErrorKind::ResyncRequired => tr.sync_resync_required(),
SyncErrorKind::ClockIncorrect => tr.sync_clock_off(),
SyncErrorKind::DatabaseCheckRequired => tr.sync_sanity_check_failed(),
SyncErrorKind::SyncNotStarted => "sync not started".into(),
}
.into()
}
}
impl NetworkError {
pub fn localized_description(&self, tr: &I18n) -> String {
let summary = match self.kind {
NetworkErrorKind::Offline => tr.network_offline(),
NetworkErrorKind::Timeout => tr.network_timeout(),
NetworkErrorKind::ProxyAuth => tr.network_proxy_auth(),
NetworkErrorKind::Other => tr.network_other(),
};
let details = tr.network_details(self.info.as_str());
format!("{}\n\n{}", summary, details)
}
}

View File

@ -402,10 +402,7 @@ where
Ok(())
} else {
self.ctx.transact(|ctx| ctx.force_resync())?;
Err(AnkiError::SyncError {
info: "".into(),
kind: SyncErrorKind::ResyncRequired,
})
Err(AnkiError::sync_error("", SyncErrorKind::ResyncRequired))
}
} else {
Err(AnkiError::server_message(resp.err))
@ -608,7 +605,7 @@ fn extract_into_media_folder(
let real_name = fmap
.get(name)
.ok_or_else(|| AnkiError::sync_misc("malformed zip"))?;
.ok_or_else(|| AnkiError::sync_error("malformed zip", SyncErrorKind::Other))?;
let mut data = Vec::with_capacity(file.size() as usize);
file.read_to_end(&mut data)?;

View File

@ -570,8 +570,8 @@ fn note_differs_from_db(existing_note: &mut Note, note: &mut Note) -> bool {
mod test {
use super::{anki_base91, field_checksum};
use crate::{
collection::open_test_collection, config::BoolKey, decks::DeckId, error::Result, prelude::*,
search::SortMode,
collection::open_test_collection, config::BoolKey, decks::DeckId, error::Result,
prelude::*, search::SortMode,
};
#[test]

View File

@ -39,10 +39,10 @@ impl SqliteStorage {
"notetypes",
] {
if self.table_has_usn(table)? {
return Err(AnkiError::SyncError {
info: format!("table had usn=-1: {}", table),
kind: SyncErrorKind::Other,
});
return Err(AnkiError::sync_error(
format!("table had usn=-1: {}", table),
SyncErrorKind::Other,
));
}
}

View File

@ -281,10 +281,7 @@ impl HttpSyncClient {
resp.error_for_status_ref()?;
let text = resp.text().await?;
if text != "OK" {
Err(AnkiError::SyncError {
info: text,
kind: SyncErrorKind::Other,
})
Err(AnkiError::sync_error(text, SyncErrorKind::Other))
} else {
Ok(())
}
@ -349,7 +346,10 @@ fn sync_endpoint(host_number: u32) -> String {
#[cfg(test)]
mod test {
use super::*;
use crate::{error::SyncErrorKind, sync::SanityCheckDueCounts};
use crate::{
error::{SyncError, SyncErrorKind},
sync::SanityCheckDueCounts,
};
use tokio::runtime::Runtime;
async fn http_client_inner(username: String, password: String) -> Result<()> {
@ -357,10 +357,10 @@ mod test {
assert!(matches!(
syncer.login("nosuchuser", "nosuchpass").await,
Err(AnkiError::SyncError {
Err(AnkiError::SyncError(SyncError {
kind: SyncErrorKind::AuthFailed,
..
})
}))
));
assert!(syncer.login(&username, &password).await.is_ok());
@ -370,10 +370,10 @@ mod test {
// aborting before a start is a conflict
assert!(matches!(
syncer.abort().await,
Err(AnkiError::SyncError {
Err(AnkiError::SyncError(SyncError {
kind: SyncErrorKind::Conflict,
..
})
}))
));
let _graves = syncer.start(Usn(1), true, None).await?;

View File

@ -10,6 +10,7 @@ use crate::{
card::{Card, CardQueue, CardType},
deckconf::DeckConfSchema11,
decks::DeckSchema11,
error::SyncError,
error::SyncErrorKind,
notes::Note,
notetype::{Notetype, NotetypeSchema11},
@ -339,10 +340,10 @@ where
self.col.storage.rollback_trx()?;
let _ = self.remote.abort().await;
if let AnkiError::SyncError {
kind: SyncErrorKind::DatabaseCheckRequired,
if let AnkiError::SyncError(SyncError {
info,
} = &e
kind: SyncErrorKind::DatabaseCheckRequired,
}) = &e
{
debug!(self.col.log, "sanity check failed:\n{}", info);
}
@ -359,10 +360,10 @@ where
debug!(self.col.log, "remote {:?}", &remote);
if !remote.should_continue {
debug!(self.col.log, "server says abort"; "message"=>&remote.server_message);
return Err(AnkiError::SyncError {
info: remote.server_message,
kind: SyncErrorKind::ServerMessage,
});
return Err(AnkiError::sync_error(
remote.server_message,
SyncErrorKind::ServerMessage,
));
}
let local = self.col.sync_meta()?;
@ -370,11 +371,7 @@ where
let delta = remote.current_time.0 - local.current_time.0;
if delta.abs() > 300 {
debug!(self.col.log, "clock off"; "delta"=>delta);
return Err(AnkiError::SyncError {
// fixme: need to rethink error handling; defer translation and pass in time difference
info: "".into(),
kind: SyncErrorKind::ClockIncorrect,
});
return Err(AnkiError::sync_error("", SyncErrorKind::ClockIncorrect));
}
Ok(local.compared_to_remote(remote))
@ -551,10 +548,10 @@ where
let out: SanityCheckOut = self.remote.sanity_check(local_counts).await?;
debug!(self.col.log, "got server reply");
if out.status != SanityCheckStatus::Ok {
Err(AnkiError::SyncError {
info: format!("local {:?}\nremote {:?}", out.client, out.server),
kind: SyncErrorKind::DatabaseCheckRequired,
})
Err(AnkiError::sync_error(
format!("local {:?}\nremote {:?}", out.client, out.server),
SyncErrorKind::DatabaseCheckRequired,
))
} else {
Ok(())
}
@ -852,10 +849,10 @@ impl Collection {
if (existing_nt.fields.len() != nt.fields.len())
|| (existing_nt.templates.len() != nt.templates.len())
{
return Err(AnkiError::SyncError {
info: "notetype schema changed".into(),
kind: SyncErrorKind::ResyncRequired,
});
return Err(AnkiError::sync_error(
"notetype schema changed",
SyncErrorKind::ResyncRequired,
));
}
true
} else {