diff --git a/rslib/src/backend/err.rs b/rslib/src/backend/err.rs index 95fe2aca0..421e1238d 100644 --- a/rslib/src/backend/err.rs +++ b/rslib/src/backend/err.rs @@ -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 {}), diff --git a/rslib/src/backend/sync/server.rs b/rslib/src/backend/sync/server.rs index 7f98ffc05..a5a907e48 100644 --- a/rslib/src/backend/sync/server.rs +++ b/rslib/src/backend/sync/server.rs @@ -24,12 +24,13 @@ impl Backend { F: FnOnce(&mut LocalServer) -> Result, { 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 { diff --git a/rslib/src/error/mod.rs b/rslib/src/error/mod.rs index 318936ffd..3ed660b34 100644 --- a/rslib/src/error/mod.rs +++ b/rslib/src/error/mod.rs @@ -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>(msg: S) -> AnkiError { - AnkiError::SyncError { - info: msg.into(), - kind: SyncErrorKind::ServerMessage, - } - } - - pub(crate) fn sync_misc>(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 for AnkiError { } } -#[derive(Debug, PartialEq)] -pub enum NetworkErrorKind { - Offline, - Timeout, - ProxyAuth, - Other, -} - -impl From 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 for AnkiError { - fn from(err: zip::result::ZipError) -> Self { - AnkiError::sync_misc(err.to_string()) - } -} - impl From for AnkiError { fn from(err: serde_json::Error) -> Self { AnkiError::JsonError { diff --git a/rslib/src/error/network.rs b/rslib/src/error/network.rs new file mode 100644 index 000000000..7524d477e --- /dev/null +++ b/rslib/src/error/network.rs @@ -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, kind: SyncErrorKind) -> Self { + AnkiError::SyncError(SyncError { + info: info.into(), + kind, + }) + } +} + +impl From 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 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) + } +} diff --git a/rslib/src/media/sync.rs b/rslib/src/media/sync.rs index c1983c838..da01110cc 100644 --- a/rslib/src/media/sync.rs +++ b/rslib/src/media/sync.rs @@ -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)?; diff --git a/rslib/src/notes/mod.rs b/rslib/src/notes/mod.rs index 34c3a3bb3..861455c93 100644 --- a/rslib/src/notes/mod.rs +++ b/rslib/src/notes/mod.rs @@ -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] diff --git a/rslib/src/storage/sync_check.rs b/rslib/src/storage/sync_check.rs index add63301c..8a7ebd660 100644 --- a/rslib/src/storage/sync_check.rs +++ b/rslib/src/storage/sync_check.rs @@ -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, + )); } } diff --git a/rslib/src/sync/http_client.rs b/rslib/src/sync/http_client.rs index 3e7aa135b..b1d29cca4 100644 --- a/rslib/src/sync/http_client.rs +++ b/rslib/src/sync/http_client.rs @@ -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?; diff --git a/rslib/src/sync/mod.rs b/rslib/src/sync/mod.rs index 5ab7c6276..2d8e4e94d 100644 --- a/rslib/src/sync/mod.rs +++ b/rslib/src/sync/mod.rs @@ -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 {