fix(desktop): normalize site URL so schemeless/trailing-slash entries connect (#145, #170)

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>
pull/3587/head
Tomas Dvorak 2 weeks ago
parent 75d31e24a9
commit 72e0b557d2

@ -29,7 +29,7 @@ impl ClientConfig {
/// Create a new configuration with the given base URL
pub fn new(base_url: impl Into<String>) -> 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"
);
}
}

@ -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};

@ -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,

@ -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(),

Loading…
Cancel
Save