From 72e0b557d24290b56eff083ef51775187b450337 Mon Sep 17 00:00:00 2001 From: Tomas Dvorak Date: Sat, 19 Sep 2026 11:33:58 +0200 Subject: [PATCH] fix(desktop): normalize site URL so schemeless/trailing-slash entries connect (#145, #170) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Site URLs were stored and concatenated verbatim: "host:8443" (no scheme) failed URL parsing, and a trailing "/" produced //api/v3. normalize_site_url now trims, prepends https:// when no scheme is present, and strips trailing slashes — applied in ClientConfig::new (single funnel) and add_drive (canonical storage for reauthorize and view-online links). Generated with [Devin](https://devin.ai) Co-Authored-By: Devin <158243242+devin-ai-integration[bot]@users.noreply.github.com> --- desktop/crates/cloudreve-api/src/client.rs | 41 +++++++++++++++++++++- desktop/crates/cloudreve-api/src/lib.rs | 2 +- desktop/crates/cloudreve-sync/src/lib.rs | 1 + desktop/src-tauri/src/commands.rs | 8 +++-- 4 files changed, 48 insertions(+), 4 deletions(-) diff --git a/desktop/crates/cloudreve-api/src/client.rs b/desktop/crates/cloudreve-api/src/client.rs index 0c9a5b58..a414b960 100644 --- a/desktop/crates/cloudreve-api/src/client.rs +++ b/desktop/crates/cloudreve-api/src/client.rs @@ -29,7 +29,7 @@ impl ClientConfig { /// Create a new configuration with the given base URL pub fn new(base_url: impl Into) -> Self { Self { - base_url: base_url.into(), + base_url: normalize_site_url(&base_url.into()), timeout_seconds: 60, client_id: "".to_string(), user_agent: None, @@ -55,6 +55,20 @@ impl ClientConfig { } } +/// Normalize a user-provided site URL into a form `build_url` can safely +/// concatenate: trims whitespace, prepends `https://` when no scheme is +/// given (so `host:8443` is not parsed as `host` scheme `8443` path), and +/// strips trailing slashes so `base + /api/v3` never yields `//api/v3`. +pub fn normalize_site_url(url: &str) -> String { + let trimmed = url.trim(); + let with_scheme = if trimmed.contains("://") { + trimmed.to_string() + } else { + format!("https://{}", trimmed) + }; + with_scheme.trim_end_matches('/').to_string() +} + /// Token storage with expiration tracking #[derive(Debug, Clone)] pub(crate) struct TokenStore { @@ -590,3 +604,28 @@ impl Client { self.send(path, Method::PATCH, Some(body), options).await } } + +#[cfg(test)] +mod site_url_tests { + use super::normalize_site_url; + + #[test] + fn normalizes_schemeless_and_trailing_slash() { + assert_eq!( + normalize_site_url("example.com:8443"), + "https://example.com:8443" + ); + assert_eq!( + normalize_site_url("https://host.example.com/"), + "https://host.example.com" + ); + assert_eq!( + normalize_site_url(" https://host/sub/path// "), + "https://host/sub/path" + ); + assert_eq!( + normalize_site_url("http://192.168.1.10:5212"), + "http://192.168.1.10:5212" + ); + } +} diff --git a/desktop/crates/cloudreve-api/src/lib.rs b/desktop/crates/cloudreve-api/src/lib.rs index 8241735b..8759234b 100644 --- a/desktop/crates/cloudreve-api/src/lib.rs +++ b/desktop/crates/cloudreve-api/src/lib.rs @@ -44,5 +44,5 @@ pub mod error; pub mod models; pub use boolset::Boolset; -pub use client::{Client, ClientConfig}; +pub use client::{Client, ClientConfig, normalize_site_url}; pub use error::{ApiError, ApiResult}; diff --git a/desktop/crates/cloudreve-sync/src/lib.rs b/desktop/crates/cloudreve-sync/src/lib.rs index ca6fcaf0..51b007df 100644 --- a/desktop/crates/cloudreve-sync/src/lib.rs +++ b/desktop/crates/cloudreve-sync/src/lib.rs @@ -18,6 +18,7 @@ pub mod uploader; pub mod utils; // Re-export commonly used types +pub use cloudreve_api::normalize_site_url; pub use config::{AppConfig, ConfigManager}; pub use drive::manager::{ DriveInfo, DriveInfoStatus, DriveManager, StatusSummary, TaskWithProgress, diff --git a/desktop/src-tauri/src/commands.rs b/desktop/src-tauri/src/commands.rs index ecfd3d5a..33391cac 100644 --- a/desktop/src-tauri/src/commands.rs +++ b/desktop/src-tauri/src/commands.rs @@ -101,6 +101,10 @@ pub async fn add_drive( return Err(t!("localPathCannotBeRootDrive").to_string()); } + // Normalize the site URL once so every consumer (API client, reauthorize + // window, view-online links) sees the same canonical form. + let site_url = cloudreve_sync::normalize_site_url(&config.site_url); + // Convert relative expiry times (seconds) to absolute RFC3339 timestamps let now = Utc::now(); let access_expires = (now + Duration::seconds(config.access_token_expires as i64)).to_rfc3339(); @@ -121,7 +125,7 @@ pub async fn add_drive( .update_drive_credentials( &drive_id, config.drive_name, - config.site_url, + site_url.clone(), credentials, &config.user_id, ) @@ -144,7 +148,7 @@ pub async fn add_drive( let drive_config = DriveConfig { id: drive_id, name: config.drive_name, - instance_url: config.site_url, + instance_url: site_url, remote_path: config.remote_path, credentials, sync_path: config.local_path.into(),