diff --git a/ftl/core/preferences.ftl b/ftl/core/preferences.ftl index cac8b9360..4ebb21585 100644 --- a/ftl/core/preferences.ftl +++ b/ftl/core/preferences.ftl @@ -67,6 +67,7 @@ preferences-note = Note preferences-scheduler = Scheduler preferences-user-interface = User Interface preferences-import-export = Import/Export +preferences-network-timeout = Network timeout ## NO NEED TO TRANSLATE. This text is no longer used by Anki, and will be removed in the future. diff --git a/proto/anki/sync.proto b/proto/anki/sync.proto index c4d1bffe9..b0c0404e7 100644 --- a/proto/anki/sync.proto +++ b/proto/anki/sync.proto @@ -24,6 +24,7 @@ service SyncService { message SyncAuth { string hkey = 1; optional string endpoint = 2; + optional uint32 io_timeout_secs = 3; } message SyncLoginRequest { diff --git a/qt/aqt/forms/preferences.ui b/qt/aqt/forms/preferences.ui index 969861c2d..8803b82ce 100644 --- a/qt/aqt/forms/preferences.ui +++ b/qt/aqt/forms/preferences.ui @@ -712,6 +712,47 @@ + + + + + + preferences_network_timeout + + + + + + + 30 + + + 99999 + + + + + + + scheduling_seconds + + + + + + + Qt::Horizontal + + + + 40 + 20 + + + + + + @@ -1087,6 +1128,7 @@ syncOnProgramOpen autoSyncMedia fullSync + network_timeout media_log syncDeauth custom_sync_url diff --git a/qt/aqt/preferences.py b/qt/aqt/preferences.py index 7a96d1eaf..5fa3da198 100644 --- a/qt/aqt/preferences.py +++ b/qt/aqt/preferences.py @@ -183,6 +183,7 @@ class Preferences(QDialog): qconnect(self.form.syncDeauth.clicked, self.sync_logout) self.form.syncDeauth.setText(tr.sync_log_out_button()) self.form.custom_sync_url.setText(self.mw.pm.custom_sync_url()) + self.form.network_timeout.setValue(self.mw.pm.network_timeout()) def on_media_log(self) -> None: self.mw.media_syncer.show_sync_log() @@ -211,6 +212,7 @@ class Preferences(QDialog): if self.form.fullSync.isChecked(): self.mw.col.mod_schema(check=False) self.mw.pm.set_custom_sync_url(self.form.custom_sync_url.text()) + self.mw.pm.set_network_timeout(self.form.network_timeout.value()) # Global preferences ###################################################################### diff --git a/qt/aqt/profiles.py b/qt/aqt/profiles.py index 6b78207fa..8a7a2d2ac 100644 --- a/qt/aqt/profiles.py +++ b/qt/aqt/profiles.py @@ -642,7 +642,11 @@ create table if not exists profiles def sync_auth(self) -> SyncAuth | None: if not (hkey := self.profile.get("syncKey")): return None - return SyncAuth(hkey=hkey, endpoint=self.sync_endpoint()) + return SyncAuth( + hkey=hkey, + endpoint=self.sync_endpoint(), + io_timeout_secs=self.network_timeout(), + ) def clear_sync_auth(self) -> None: self.set_sync_key(None) @@ -680,3 +684,9 @@ create table if not exists profiles def set_show_browser_table_tooltips(self, val: bool) -> None: self.profile["browserTableTooltips"] = val + + def set_network_timeout(self, timeout_secs: int) -> None: + self.profile["networkTimeout"] = timeout_secs + + def network_timeout(self) -> int: + return self.profile.get("networkTimeout") or 30 diff --git a/rslib/src/backend/sync/mod.rs b/rslib/src/backend/sync/mod.rs index 4c7aba72b..e90a9229d 100644 --- a/rslib/src/backend/sync/mod.rs +++ b/rslib/src/backend/sync/mod.rs @@ -89,6 +89,7 @@ impl TryFrom for SyncAuth { .or_invalid("Invalid sync server specified. Please check the preferences.") }) .transpose()?, + io_timeout_secs: value.io_timeout_secs, }) } } @@ -236,6 +237,7 @@ impl Backend { ret.map(|a| pb::sync::SyncAuth { hkey: a.hkey, endpoint: None, + io_timeout_secs: None, }) } diff --git a/rslib/src/sync/collection/tests.rs b/rslib/src/sync/collection/tests.rs index a332de1eb..eba4c52a5 100644 --- a/rslib/src/sync/collection/tests.rs +++ b/rslib/src/sync/collection/tests.rs @@ -98,6 +98,7 @@ where let auth = SyncAuth { hkey: AUTH.host_key.clone(), endpoint: Some(endpoint), + io_timeout_secs: None, }; let client = HttpSyncClient::new(auth); op(client).await diff --git a/rslib/src/sync/http_client/mod.rs b/rslib/src/sync/http_client/mod.rs index 3aa099b96..4a3aff2de 100644 --- a/rslib/src/sync/http_client/mod.rs +++ b/rslib/src/sync/http_client/mod.rs @@ -32,11 +32,13 @@ pub struct HttpSyncClient { session_key: String, client: Client, pub endpoint: Url, + pub io_timeout: Duration, full_sync_progress_fn: Mutex>, } impl HttpSyncClient { pub fn new(auth: SyncAuth) -> HttpSyncClient { + let io_timeout = Duration::from_secs(auth.io_timeout_secs.unwrap_or(30) as u64); HttpSyncClient { sync_key: auth.hkey, session_key: simple_session_id(), @@ -44,6 +46,7 @@ impl HttpSyncClient { endpoint: auth .endpoint .unwrap_or_else(|| Url::try_from("https://sync.ankiweb.net/").unwrap()), + io_timeout, full_sync_progress_fn: Mutex::new(None), } } @@ -56,6 +59,7 @@ impl HttpSyncClient { client: self.client.clone(), endpoint: self.endpoint.clone(), full_sync_progress_fn: Mutex::new(None), + io_timeout: self.io_timeout, } } @@ -86,7 +90,7 @@ impl HttpSyncClient { .post(url) .header(&SYNC_HEADER_NAME, serde_json::to_string(&header).unwrap()); io_monitor - .zstd_request_with_timeout(request, data, Duration::from_secs(30)) + .zstd_request_with_timeout(request, data, self.io_timeout) .await .map(SyncResponse::from_vec) } diff --git a/rslib/src/sync/login.rs b/rslib/src/sync/login.rs index 701604e50..1d51782be 100644 --- a/rslib/src/sync/login.rs +++ b/rslib/src/sync/login.rs @@ -14,6 +14,7 @@ use crate::sync::request::IntoSyncRequest; pub struct SyncAuth { pub hkey: String, pub endpoint: Option, + pub io_timeout_secs: Option, } #[derive(Serialize, Deserialize, Debug)] @@ -53,5 +54,6 @@ pub async fn sync_login>( Ok(SyncAuth { hkey: resp.key, endpoint: None, + io_timeout_secs: None, }) }