From ddcdb4026ca6df3d95ec862af07f49ce43e6a76a Mon Sep 17 00:00:00 2001 From: kqjy Date: Mon, 20 Apr 2026 22:02:05 +0800 Subject: [PATCH] Fix domains mapping missing --- .../myfsio-server/src/handlers/ui_pages.rs | 16 ++- .../src/services/website_domains.rs | 100 +++++++++++++++++- .../myfsio-server/tests/template_render.rs | 81 ++++++++++++++ 3 files changed, 190 insertions(+), 7 deletions(-) diff --git a/rust/myfsio-engine/crates/myfsio-server/src/handlers/ui_pages.rs b/rust/myfsio-engine/crates/myfsio-server/src/handlers/ui_pages.rs index 4b300a3..9a59b1a 100644 --- a/rust/myfsio-engine/crates/myfsio-server/src/handlers/ui_pages.rs +++ b/rust/myfsio-engine/crates/myfsio-server/src/handlers/ui_pages.rs @@ -257,11 +257,23 @@ fn config_encryption_to_ui(value: Option<&Value>) -> Value { } fn config_website_to_ui(value: Option<&Value>) -> Value { - match value { + let parsed = match value { Some(Value::Object(map)) => Value::Object(map.clone()), Some(Value::String(s)) => serde_json::from_str(s).unwrap_or(Value::Null), _ => Value::Null, - } + }; + + let Some(map) = parsed.as_object() else { + return Value::Null; + }; + + json!({ + "index_document": map + .get("index_document") + .and_then(Value::as_str) + .unwrap_or("index.html"), + "error_document": map.get("error_document").and_then(Value::as_str), + }) } fn bucket_access_descriptor( diff --git a/rust/myfsio-engine/crates/myfsio-server/src/services/website_domains.rs b/rust/myfsio-engine/crates/myfsio-server/src/services/website_domains.rs index 5060d52..c36277d 100644 --- a/rust/myfsio-engine/crates/myfsio-server/src/services/website_domains.rs +++ b/rust/myfsio-engine/crates/myfsio-server/src/services/website_domains.rs @@ -5,11 +5,33 @@ use std::path::PathBuf; use std::sync::Arc; #[derive(Debug, Clone, Serialize, Deserialize, Default)] +#[serde(deny_unknown_fields)] struct DomainData { #[serde(default)] mappings: HashMap, } +#[derive(Debug, Deserialize)] +#[serde(untagged)] +enum DomainDataFile { + Wrapped(DomainData), + Flat(HashMap), +} + +impl DomainDataFile { + fn into_domain_data(self) -> DomainData { + match self { + Self::Wrapped(data) => data, + Self::Flat(mappings) => DomainData { + mappings: mappings + .into_iter() + .map(|(domain, bucket)| (normalize_domain(&domain), bucket)) + .collect(), + }, + } + } +} + pub struct WebsiteDomainStore { path: PathBuf, data: Arc>, @@ -24,7 +46,8 @@ impl WebsiteDomainStore { let data = if path.exists() { std::fs::read_to_string(&path) .ok() - .and_then(|s| serde_json::from_str(&s).ok()) + .and_then(|s| serde_json::from_str::(&s).ok()) + .map(DomainDataFile::into_domain_data) .unwrap_or_default() } else { DomainData::default() @@ -40,7 +63,7 @@ impl WebsiteDomainStore { if let Some(parent) = self.path.parent() { let _ = std::fs::create_dir_all(parent); } - if let Ok(json) = serde_json::to_string_pretty(&*data) { + if let Ok(json) = serde_json::to_string_pretty(&data.mappings) { let _ = std::fs::write(&self.path, json); } } @@ -60,19 +83,22 @@ impl WebsiteDomainStore { } pub fn get_bucket(&self, domain: &str) -> Option { - self.data.read().mappings.get(domain).cloned() + let domain = normalize_domain(domain); + self.data.read().mappings.get(&domain).cloned() } pub fn set_mapping(&self, domain: &str, bucket: &str) { + let domain = normalize_domain(domain); self.data .write() .mappings - .insert(domain.to_string(), bucket.to_string()); + .insert(domain, bucket.to_string()); self.save(); } pub fn delete_mapping(&self, domain: &str) -> bool { - let removed = self.data.write().mappings.remove(domain).is_some(); + let domain = normalize_domain(domain); + let removed = self.data.write().mappings.remove(&domain).is_some(); if removed { self.save(); } @@ -105,3 +131,67 @@ pub fn is_valid_domain(domain: &str) -> bool { } true } + +#[cfg(test)] +mod tests { + use super::WebsiteDomainStore; + use serde_json::json; + use tempfile::tempdir; + + #[test] + fn loads_legacy_flat_mapping_file() { + let tmp = tempdir().expect("tempdir"); + let config_dir = tmp.path().join(".myfsio.sys").join("config"); + std::fs::create_dir_all(&config_dir).expect("create config dir"); + std::fs::write( + config_dir.join("website_domains.json"), + r#"{"Example.COM":"site-bucket"}"#, + ) + .expect("write config"); + + let store = WebsiteDomainStore::new(tmp.path()); + + assert_eq!( + store.get_bucket("example.com"), + Some("site-bucket".to_string()) + ); + } + + #[test] + fn loads_wrapped_mapping_file() { + let tmp = tempdir().expect("tempdir"); + let config_dir = tmp.path().join(".myfsio.sys").join("config"); + std::fs::create_dir_all(&config_dir).expect("create config dir"); + std::fs::write( + config_dir.join("website_domains.json"), + r#"{"mappings":{"example.com":"site-bucket"}}"#, + ) + .expect("write config"); + + let store = WebsiteDomainStore::new(tmp.path()); + + assert_eq!( + store.get_bucket("example.com"), + Some("site-bucket".to_string()) + ); + } + + #[test] + fn saves_in_shared_plain_mapping_format() { + let tmp = tempdir().expect("tempdir"); + let store = WebsiteDomainStore::new(tmp.path()); + + store.set_mapping("Example.COM", "site-bucket"); + + let saved = std::fs::read_to_string( + tmp.path() + .join(".myfsio.sys") + .join("config") + .join("website_domains.json"), + ) + .expect("read config"); + let json: serde_json::Value = serde_json::from_str(&saved).expect("parse config"); + + assert_eq!(json, json!({"example.com": "site-bucket"})); + } +} diff --git a/rust/myfsio-engine/crates/myfsio-server/tests/template_render.rs b/rust/myfsio-engine/crates/myfsio-server/tests/template_render.rs index ba11a77..cd4bd55 100644 --- a/rust/myfsio-engine/crates/myfsio-server/tests/template_render.rs +++ b/rust/myfsio-engine/crates/myfsio-server/tests/template_render.rs @@ -341,3 +341,84 @@ fn render_bucket_detail() { ctx.insert("objects_stream_url", &""); render_or_panic("bucket_detail.html", &ctx); } + +#[test] +fn render_bucket_detail_without_error_document() { + let mut ctx = base_ctx(); + ctx.insert("bucket_name", &"site-bucket"); + ctx.insert( + "bucket", + &json!({ + "name": "site-bucket", + "creation_date": "2025-01-01T00:00:00Z", + }), + ); + ctx.insert("objects", &Vec::::new()); + ctx.insert("prefixes", &Vec::::new()); + ctx.insert("total_objects", &0u64); + ctx.insert("total_bytes", &0u64); + ctx.insert("current_objects", &0u64); + ctx.insert("current_bytes", &0u64); + ctx.insert("version_count", &0u64); + ctx.insert("version_bytes", &0u64); + ctx.insert("max_objects", &Value::Null); + ctx.insert("max_bytes", &Value::Null); + ctx.insert("has_max_objects", &false); + ctx.insert("has_max_bytes", &false); + ctx.insert("obj_pct", &0); + ctx.insert("bytes_pct", &0); + ctx.insert("has_quota", &false); + ctx.insert("versioning_enabled", &false); + ctx.insert("versioning_status", &"Disabled"); + ctx.insert("encryption_config", &json!({"Rules": []})); + ctx.insert("enc_rules", &Vec::::new()); + ctx.insert("enc_algorithm", &""); + ctx.insert("enc_kms_key", &""); + ctx.insert("replication_rules", &Vec::::new()); + ctx.insert("replication_rule", &Value::Null); + ctx.insert("website_config", &json!({"index_document": "index.html"})); + ctx.insert("bucket_policy", &""); + ctx.insert("bucket_policy_text", &""); + ctx.insert("connections", &Vec::::new()); + ctx.insert("current_prefix", &""); + ctx.insert("parent_prefix", &""); + ctx.insert("has_more", &false); + ctx.insert("next_token", &""); + ctx.insert("active_tab", &"objects"); + ctx.insert("multipart_uploads", &Vec::::new()); + ctx.insert("target_conn", &Value::Null); + ctx.insert("target_conn_name", &""); + ctx.insert("preset_choice", &""); + ctx.insert("default_policy", &""); + ctx.insert("can_manage_cors", &true); + ctx.insert("can_manage_lifecycle", &true); + ctx.insert("can_manage_quota", &true); + ctx.insert("can_manage_versioning", &true); + ctx.insert("can_manage_website", &true); + ctx.insert("can_edit_policy", &true); + ctx.insert("is_replication_admin", &true); + ctx.insert("lifecycle_enabled", &false); + ctx.insert("site_sync_enabled", &false); + ctx.insert("website_hosting_enabled", &true); + ctx.insert("website_domains", &Vec::::new()); + ctx.insert("kms_keys", &Vec::::new()); + ctx.insert( + "bucket_stats", + &json!({ + "bytes": 0, "objects": 0, "total_bytes": 0, "total_objects": 0, + "version_bytes": 0, "version_count": 0 + }), + ); + ctx.insert( + "bucket_quota", + &json!({ "max_bytes": null, "max_objects": null }), + ); + ctx.insert("buckets_for_copy_url", &""); + ctx.insert("acl_url", &""); + ctx.insert("cors_url", &""); + ctx.insert("folders_url", &""); + ctx.insert("lifecycle_url", &""); + ctx.insert("objects_api_url", &""); + ctx.insert("objects_stream_url", &""); + render_or_panic("bucket_detail.html", &ctx); +}