mirror of
https://github.com/rustfs/rustfs.git
synced 2026-08-27 15:37:02 +00:00
refactor: move ecstore endpoint layout owners (#3647)
This commit is contained in:
@@ -1,634 +1 @@
|
|||||||
// Copyright 2024 RustFS Team
|
pub use crate::layout::endpoint::*;
|
||||||
//
|
|
||||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
|
||||||
// you may not use this file except in compliance with the License.
|
|
||||||
// You may obtain a copy of the License at
|
|
||||||
//
|
|
||||||
// http://www.apache.org/licenses/LICENSE-2.0
|
|
||||||
//
|
|
||||||
// Unless required by applicable law or agreed to in writing, software
|
|
||||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
|
||||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
||||||
// See the License for the specific language governing permissions and
|
|
||||||
// limitations under the License.
|
|
||||||
|
|
||||||
use crate::disk::error::{Error, Result};
|
|
||||||
use path_absolutize::Absolutize;
|
|
||||||
use rustfs_utils::{is_local_host, is_socket_addr};
|
|
||||||
use std::{fmt::Display, path::Path};
|
|
||||||
use tracing::debug;
|
|
||||||
use url::{ParseError, Url};
|
|
||||||
|
|
||||||
#[cfg(windows)]
|
|
||||||
pub(crate) fn windows_fallback_local_path(
|
|
||||||
path: &str,
|
|
||||||
canonicalize_error: &std::io::Error,
|
|
||||||
context: &'static str,
|
|
||||||
) -> std::io::Result<std::path::PathBuf> {
|
|
||||||
let absolute = Path::new(path).absolutize()?.to_path_buf();
|
|
||||||
tracing::warn!(
|
|
||||||
path = %path,
|
|
||||||
canonicalize_error = %canonicalize_error,
|
|
||||||
resolved = ?absolute,
|
|
||||||
context = context,
|
|
||||||
"using windows fallback path resolution for local endpoint"
|
|
||||||
);
|
|
||||||
Ok(absolute)
|
|
||||||
}
|
|
||||||
|
|
||||||
/// enum for endpoint type.
|
|
||||||
#[derive(PartialEq, Eq, Debug)]
|
|
||||||
pub enum EndpointType {
|
|
||||||
/// path style endpoint type enum.
|
|
||||||
Path,
|
|
||||||
|
|
||||||
/// URL style endpoint type enum.
|
|
||||||
Url,
|
|
||||||
}
|
|
||||||
|
|
||||||
/// any type of endpoint.
|
|
||||||
#[derive(Debug, PartialEq, Eq, Clone, Hash)]
|
|
||||||
pub struct Endpoint {
|
|
||||||
pub url: Url,
|
|
||||||
pub is_local: bool,
|
|
||||||
|
|
||||||
pub pool_idx: i32,
|
|
||||||
pub set_idx: i32,
|
|
||||||
pub disk_idx: i32,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl Display for Endpoint {
|
|
||||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
|
||||||
if self.url.scheme() == "file" {
|
|
||||||
write!(f, "{}", self.get_file_path())
|
|
||||||
} else {
|
|
||||||
write!(f, "{}", self.url)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl TryFrom<&str> for Endpoint {
|
|
||||||
/// The type returned in the event of a conversion error.
|
|
||||||
type Error = Error;
|
|
||||||
|
|
||||||
/// Performs the conversion.
|
|
||||||
fn try_from(value: &str) -> std::result::Result<Self, Self::Error> {
|
|
||||||
// check whether given path is not empty.
|
|
||||||
if ["", "/", "\\"].iter().any(|&v| v.eq(value)) {
|
|
||||||
return Err(Error::other("empty or root endpoint is not supported"));
|
|
||||||
}
|
|
||||||
|
|
||||||
let mut is_local = false;
|
|
||||||
let url = match Url::parse(value) {
|
|
||||||
#[allow(unused_mut)]
|
|
||||||
Ok(mut url) if url.has_host() => {
|
|
||||||
// URL style of endpoint.
|
|
||||||
// Valid URL style endpoint is
|
|
||||||
// - Scheme field must contain "http" or "https"
|
|
||||||
// - All field should be empty except Host and Path.
|
|
||||||
if !((url.scheme() == "http" || url.scheme() == "https")
|
|
||||||
&& url.username().is_empty()
|
|
||||||
&& url.fragment().is_none()
|
|
||||||
&& url.query().is_none())
|
|
||||||
{
|
|
||||||
return Err(Error::other("invalid URL endpoint format"));
|
|
||||||
}
|
|
||||||
|
|
||||||
let path = url.path().to_string();
|
|
||||||
|
|
||||||
#[cfg(not(windows))]
|
|
||||||
let path = Path::new(&path).absolutize()?;
|
|
||||||
|
|
||||||
#[cfg(windows)]
|
|
||||||
let path = if has_leading_slash_windows_drive(&path) {
|
|
||||||
// Url::path() exposes file-like Windows paths as `/C:/...`.
|
|
||||||
// Strip only that synthetic leading slash; plain URL paths
|
|
||||||
// such as `/export1` must stay URL paths, not become
|
|
||||||
// relative paths under the current drive.
|
|
||||||
Path::new(&path[1..]).absolutize()?.to_string_lossy().into_owned()
|
|
||||||
} else {
|
|
||||||
path
|
|
||||||
};
|
|
||||||
#[cfg(windows)]
|
|
||||||
let path = Path::new(&path);
|
|
||||||
|
|
||||||
debug!("endpoint try_from: path={}", path.display());
|
|
||||||
|
|
||||||
if path.parent().is_none() || path.as_os_str().is_empty() {
|
|
||||||
return Err(Error::other("empty or root path is not supported in URL endpoint"));
|
|
||||||
}
|
|
||||||
|
|
||||||
match path.to_str() {
|
|
||||||
Some(v) => url.set_path(v),
|
|
||||||
None => return Err(Error::other("invalid path")),
|
|
||||||
}
|
|
||||||
|
|
||||||
url
|
|
||||||
}
|
|
||||||
Ok(_) => {
|
|
||||||
// like d:/foo
|
|
||||||
is_local = true;
|
|
||||||
url_parse_from_file_path(value)?
|
|
||||||
}
|
|
||||||
Err(e) => match e {
|
|
||||||
ParseError::InvalidPort => {
|
|
||||||
return Err(Error::other("invalid URL endpoint format: port number must be between 1 to 65535"));
|
|
||||||
}
|
|
||||||
ParseError::EmptyHost => return Err(Error::other("invalid URL endpoint format: empty host name")),
|
|
||||||
ParseError::RelativeUrlWithoutBase => {
|
|
||||||
// like /foo
|
|
||||||
is_local = true;
|
|
||||||
url_parse_from_file_path(value)?
|
|
||||||
}
|
|
||||||
_ => return Err(Error::other(format!("invalid URL endpoint format: {e}"))),
|
|
||||||
},
|
|
||||||
};
|
|
||||||
|
|
||||||
Ok(Endpoint {
|
|
||||||
url,
|
|
||||||
is_local,
|
|
||||||
pool_idx: -1,
|
|
||||||
set_idx: -1,
|
|
||||||
disk_idx: -1,
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl Endpoint {
|
|
||||||
/// returns type of endpoint.
|
|
||||||
pub fn get_type(&self) -> EndpointType {
|
|
||||||
if self.url.scheme() == "file" {
|
|
||||||
EndpointType::Path
|
|
||||||
} else {
|
|
||||||
EndpointType::Url
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// sets a specific pool number to this node
|
|
||||||
pub fn set_pool_index(&mut self, idx: usize) {
|
|
||||||
self.pool_idx = idx as i32
|
|
||||||
}
|
|
||||||
|
|
||||||
/// sets a specific set number to this node
|
|
||||||
pub fn set_set_index(&mut self, idx: usize) {
|
|
||||||
self.set_idx = idx as i32
|
|
||||||
}
|
|
||||||
|
|
||||||
/// sets a specific disk number to this node
|
|
||||||
pub fn set_disk_index(&mut self, idx: usize) {
|
|
||||||
self.disk_idx = idx as i32
|
|
||||||
}
|
|
||||||
|
|
||||||
/// resolves the host and updates if it is local or not.
|
|
||||||
pub fn update_is_local(&mut self, local_port: u16) -> Result<()> {
|
|
||||||
match (self.url.scheme(), self.url.host()) {
|
|
||||||
(v, Some(host)) if v != "file" => {
|
|
||||||
self.is_local = is_local_host(host, self.url.port().unwrap_or_default(), local_port)?;
|
|
||||||
}
|
|
||||||
_ => {}
|
|
||||||
}
|
|
||||||
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
|
|
||||||
/// returns the host to be used for grid connections.
|
|
||||||
pub fn grid_host(&self) -> String {
|
|
||||||
match (self.url.host(), self.url.port()) {
|
|
||||||
(Some(host), Some(port)) => {
|
|
||||||
debug!("grid_host scheme={}: host={}, port={}", self.url.scheme(), host, port);
|
|
||||||
format!("{}://{}:{}", self.url.scheme(), host, port)
|
|
||||||
}
|
|
||||||
(Some(host), None) => {
|
|
||||||
debug!("grid_host scheme={}: host={}", self.url.scheme(), host);
|
|
||||||
format!("{}://{}", self.url.scheme(), host)
|
|
||||||
}
|
|
||||||
_ => String::new(),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn host_port(&self) -> String {
|
|
||||||
match (self.url.host(), self.url.port()) {
|
|
||||||
(Some(host), Some(port)) => {
|
|
||||||
debug!("host_port host={}, port={}", host, port);
|
|
||||||
format!("{host}:{port}")
|
|
||||||
}
|
|
||||||
(Some(host), None) => {
|
|
||||||
debug!("host_port host={}, port={}", host, self.url.port().unwrap_or(0));
|
|
||||||
format!("{host}")
|
|
||||||
}
|
|
||||||
_ => String::new(),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn get_file_path(&self) -> String {
|
|
||||||
let path: &str = self.url.path();
|
|
||||||
let decoded: std::borrow::Cow<'_, str> = match urlencoding::decode(path) {
|
|
||||||
Ok(decoded) => decoded,
|
|
||||||
Err(e) => {
|
|
||||||
debug!("Failed to decode path '{}': {}, using original path", path, e);
|
|
||||||
std::borrow::Cow::Borrowed(path)
|
|
||||||
}
|
|
||||||
};
|
|
||||||
#[cfg(windows)]
|
|
||||||
if self.url.scheme() == "file" {
|
|
||||||
let stripped: &str = decoded.strip_prefix('/').unwrap_or(&decoded);
|
|
||||||
debug!("get_file_path windows: path={}", stripped);
|
|
||||||
return stripped.to_string();
|
|
||||||
}
|
|
||||||
decoded.into_owned()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[cfg(windows)]
|
|
||||||
fn has_leading_slash_windows_drive(path: &str) -> bool {
|
|
||||||
let bytes = path.as_bytes();
|
|
||||||
bytes.len() >= 4 && bytes[0] == b'/' && bytes[1].is_ascii_alphabetic() && bytes[2] == b':' && bytes[3] == b'/'
|
|
||||||
}
|
|
||||||
|
|
||||||
/// parse a file path into a URL.
|
|
||||||
fn url_parse_from_file_path(value: &str) -> Result<Url> {
|
|
||||||
// Only check if the arg is an ip address and ask for scheme since its absent.
|
|
||||||
// localhost, example.com, any FQDN cannot be disambiguated from a regular file path such as
|
|
||||||
// /mnt/export1. So we go ahead and start the rustfs server in FS modes in these cases.
|
|
||||||
let addr: Vec<&str> = value.splitn(2, '/').collect();
|
|
||||||
if is_socket_addr(addr[0]) {
|
|
||||||
return Err(Error::other("invalid URL endpoint format: missing scheme http or https"));
|
|
||||||
}
|
|
||||||
|
|
||||||
let file_path = match Path::new(value).absolutize() {
|
|
||||||
Ok(path) => path,
|
|
||||||
Err(err) => return Err(Error::other(format!("absolute path failed: {err}"))),
|
|
||||||
};
|
|
||||||
|
|
||||||
match Url::from_file_path(file_path) {
|
|
||||||
Ok(url) => Ok(url),
|
|
||||||
Err(_) => Err(Error::other("Convert a file path into an URL failed")),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[cfg(test)]
|
|
||||||
mod test {
|
|
||||||
use super::*;
|
|
||||||
|
|
||||||
fn expected_file_path(path: &str) -> String {
|
|
||||||
Path::new(path).absolutize().unwrap().to_string_lossy().replace('\\', "/")
|
|
||||||
}
|
|
||||||
|
|
||||||
fn expected_file_url(path: &str) -> Url {
|
|
||||||
url_parse_from_file_path(path).unwrap()
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn test_new_endpoint() {
|
|
||||||
#[derive(Default)]
|
|
||||||
struct TestCase<'a> {
|
|
||||||
arg: &'a str,
|
|
||||||
expected_endpoint: Option<Endpoint>,
|
|
||||||
expected_type: Option<EndpointType>,
|
|
||||||
expected_err: Option<Error>,
|
|
||||||
}
|
|
||||||
|
|
||||||
let u2 = Url::parse("https://example.org/path").unwrap();
|
|
||||||
let u4 = Url::parse("http://192.168.253.200/path").unwrap();
|
|
||||||
let u6 = Url::parse("http://server:/path").unwrap();
|
|
||||||
let root_slash_foo = expected_file_url("/foo");
|
|
||||||
|
|
||||||
let test_cases = [
|
|
||||||
TestCase {
|
|
||||||
arg: "/foo",
|
|
||||||
expected_endpoint: Some(Endpoint {
|
|
||||||
url: root_slash_foo,
|
|
||||||
is_local: true,
|
|
||||||
pool_idx: -1,
|
|
||||||
set_idx: -1,
|
|
||||||
disk_idx: -1,
|
|
||||||
}),
|
|
||||||
expected_type: Some(EndpointType::Path),
|
|
||||||
expected_err: None,
|
|
||||||
},
|
|
||||||
TestCase {
|
|
||||||
arg: "https://example.org/path",
|
|
||||||
expected_endpoint: Some(Endpoint {
|
|
||||||
url: u2,
|
|
||||||
is_local: false,
|
|
||||||
pool_idx: -1,
|
|
||||||
set_idx: -1,
|
|
||||||
disk_idx: -1,
|
|
||||||
}),
|
|
||||||
expected_type: Some(EndpointType::Url),
|
|
||||||
expected_err: None,
|
|
||||||
},
|
|
||||||
TestCase {
|
|
||||||
arg: "http://192.168.253.200/path",
|
|
||||||
expected_endpoint: Some(Endpoint {
|
|
||||||
url: u4,
|
|
||||||
is_local: false,
|
|
||||||
pool_idx: -1,
|
|
||||||
set_idx: -1,
|
|
||||||
disk_idx: -1,
|
|
||||||
}),
|
|
||||||
expected_type: Some(EndpointType::Url),
|
|
||||||
expected_err: None,
|
|
||||||
},
|
|
||||||
TestCase {
|
|
||||||
arg: "",
|
|
||||||
expected_endpoint: None,
|
|
||||||
expected_type: None,
|
|
||||||
expected_err: Some(Error::other("empty or root endpoint is not supported")),
|
|
||||||
},
|
|
||||||
TestCase {
|
|
||||||
arg: "/",
|
|
||||||
expected_endpoint: None,
|
|
||||||
expected_type: None,
|
|
||||||
expected_err: Some(Error::other("empty or root endpoint is not supported")),
|
|
||||||
},
|
|
||||||
TestCase {
|
|
||||||
arg: "\\",
|
|
||||||
expected_endpoint: None,
|
|
||||||
expected_type: None,
|
|
||||||
expected_err: Some(Error::other("empty or root endpoint is not supported")),
|
|
||||||
},
|
|
||||||
TestCase {
|
|
||||||
arg: "c://foo",
|
|
||||||
expected_endpoint: None,
|
|
||||||
expected_type: None,
|
|
||||||
expected_err: Some(Error::other("invalid URL endpoint format")),
|
|
||||||
},
|
|
||||||
TestCase {
|
|
||||||
arg: "ftp://foo",
|
|
||||||
expected_endpoint: None,
|
|
||||||
expected_type: None,
|
|
||||||
expected_err: Some(Error::other("invalid URL endpoint format")),
|
|
||||||
},
|
|
||||||
TestCase {
|
|
||||||
arg: "http://server/path?location",
|
|
||||||
expected_endpoint: None,
|
|
||||||
expected_type: None,
|
|
||||||
expected_err: Some(Error::other("invalid URL endpoint format")),
|
|
||||||
},
|
|
||||||
TestCase {
|
|
||||||
arg: "http://:/path",
|
|
||||||
expected_endpoint: None,
|
|
||||||
expected_type: None,
|
|
||||||
expected_err: Some(Error::other("invalid URL endpoint format: empty host name")),
|
|
||||||
},
|
|
||||||
TestCase {
|
|
||||||
arg: "http://:8080/path",
|
|
||||||
expected_endpoint: None,
|
|
||||||
expected_type: None,
|
|
||||||
expected_err: Some(Error::other("invalid URL endpoint format: empty host name")),
|
|
||||||
},
|
|
||||||
TestCase {
|
|
||||||
arg: "http://server:/path",
|
|
||||||
expected_endpoint: Some(Endpoint {
|
|
||||||
url: u6,
|
|
||||||
is_local: false,
|
|
||||||
pool_idx: -1,
|
|
||||||
set_idx: -1,
|
|
||||||
disk_idx: -1,
|
|
||||||
}),
|
|
||||||
expected_type: Some(EndpointType::Url),
|
|
||||||
expected_err: None,
|
|
||||||
},
|
|
||||||
TestCase {
|
|
||||||
arg: "https://93.184.216.34:808080/path",
|
|
||||||
expected_endpoint: None,
|
|
||||||
expected_type: None,
|
|
||||||
expected_err: Some(Error::other("invalid URL endpoint format: port number must be between 1 to 65535")),
|
|
||||||
},
|
|
||||||
TestCase {
|
|
||||||
arg: "http://server:8080//",
|
|
||||||
expected_endpoint: None,
|
|
||||||
expected_type: None,
|
|
||||||
expected_err: Some(Error::other("empty or root path is not supported in URL endpoint")),
|
|
||||||
},
|
|
||||||
TestCase {
|
|
||||||
arg: "http://server:8080/",
|
|
||||||
expected_endpoint: None,
|
|
||||||
expected_type: None,
|
|
||||||
expected_err: Some(Error::other("empty or root path is not supported in URL endpoint")),
|
|
||||||
},
|
|
||||||
TestCase {
|
|
||||||
arg: "192.168.1.210:9000",
|
|
||||||
expected_endpoint: None,
|
|
||||||
expected_type: None,
|
|
||||||
expected_err: Some(Error::other("invalid URL endpoint format: missing scheme http or https")),
|
|
||||||
},
|
|
||||||
];
|
|
||||||
|
|
||||||
for test_case in test_cases {
|
|
||||||
let ret = Endpoint::try_from(test_case.arg);
|
|
||||||
if test_case.expected_err.is_none() && ret.is_err() {
|
|
||||||
panic!("{}: error: expected = <nil>, got = {:?}", test_case.arg, ret);
|
|
||||||
}
|
|
||||||
if test_case.expected_err.is_some() && ret.is_ok() {
|
|
||||||
panic!("{}: error: expected = {:?}, got = <nil>", test_case.arg, test_case.expected_err);
|
|
||||||
}
|
|
||||||
match (test_case.expected_err, ret) {
|
|
||||||
(None, Err(e)) => panic!("{}: error: expected = <nil>, got = {}", test_case.arg, e),
|
|
||||||
(None, Ok(mut ep)) => {
|
|
||||||
let _ = ep.update_is_local(9000);
|
|
||||||
if test_case.expected_type != Some(ep.get_type()) {
|
|
||||||
panic!(
|
|
||||||
"{}: type: expected = {:?}, got = {:?}",
|
|
||||||
test_case.arg,
|
|
||||||
test_case.expected_type,
|
|
||||||
ep.get_type()
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
assert_eq!(test_case.expected_endpoint, Some(ep), "{}: endpoint", test_case.arg);
|
|
||||||
}
|
|
||||||
(Some(e), Ok(_)) => panic!("{}: error: expected = {}, got = <nil>", test_case.arg, e),
|
|
||||||
(Some(e), Err(e2)) => {
|
|
||||||
assert_eq!(e.to_string(), e2.to_string(), "{}: error: expected = {}, got = {}", test_case.arg, e, e2)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn test_endpoint_display() {
|
|
||||||
// Test file path display
|
|
||||||
let file_endpoint = Endpoint::try_from("/tmp/data").unwrap();
|
|
||||||
let display_str = format!("{file_endpoint}");
|
|
||||||
assert_eq!(display_str, expected_file_path("/tmp/data"));
|
|
||||||
|
|
||||||
// Test URL display
|
|
||||||
let url_endpoint = Endpoint::try_from("http://example.com:9000/path").unwrap();
|
|
||||||
let display_str = format!("{url_endpoint}");
|
|
||||||
assert_eq!(display_str, "http://example.com:9000/path");
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn test_endpoint_type() {
|
|
||||||
let file_endpoint = Endpoint::try_from("/tmp/data").unwrap();
|
|
||||||
assert_eq!(file_endpoint.get_type(), EndpointType::Path);
|
|
||||||
|
|
||||||
let url_endpoint = Endpoint::try_from("http://example.com:9000/path").unwrap();
|
|
||||||
assert_eq!(url_endpoint.get_type(), EndpointType::Url);
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn test_endpoint_indexes() {
|
|
||||||
let mut endpoint = Endpoint::try_from("/tmp/data").unwrap();
|
|
||||||
|
|
||||||
// Test initial values
|
|
||||||
assert_eq!(endpoint.pool_idx, -1);
|
|
||||||
assert_eq!(endpoint.set_idx, -1);
|
|
||||||
assert_eq!(endpoint.disk_idx, -1);
|
|
||||||
|
|
||||||
// Test setting indexes
|
|
||||||
endpoint.set_pool_index(2);
|
|
||||||
endpoint.set_set_index(3);
|
|
||||||
endpoint.set_disk_index(4);
|
|
||||||
|
|
||||||
assert_eq!(endpoint.pool_idx, 2);
|
|
||||||
assert_eq!(endpoint.set_idx, 3);
|
|
||||||
assert_eq!(endpoint.disk_idx, 4);
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn test_endpoint_grid_host() {
|
|
||||||
let endpoint = Endpoint::try_from("http://example.com:9000/path").unwrap();
|
|
||||||
assert_eq!(endpoint.grid_host(), "http://example.com:9000");
|
|
||||||
|
|
||||||
let endpoint_no_port = Endpoint::try_from("https://example.com/path").unwrap();
|
|
||||||
assert_eq!(endpoint_no_port.grid_host(), "https://example.com");
|
|
||||||
|
|
||||||
let file_endpoint = Endpoint::try_from("/tmp/data").unwrap();
|
|
||||||
assert_eq!(file_endpoint.grid_host(), "");
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn test_endpoint_host_port() {
|
|
||||||
let endpoint = Endpoint::try_from("http://example.com:9000/path").unwrap();
|
|
||||||
assert_eq!(endpoint.host_port(), "example.com:9000");
|
|
||||||
|
|
||||||
let endpoint_no_port = Endpoint::try_from("https://example.com/path").unwrap();
|
|
||||||
assert_eq!(endpoint_no_port.host_port(), "example.com");
|
|
||||||
|
|
||||||
let file_endpoint = Endpoint::try_from("/tmp/data").unwrap();
|
|
||||||
assert_eq!(file_endpoint.host_port(), "");
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn test_endpoint_get_file_path() {
|
|
||||||
let file_endpoint = Endpoint::try_from("/tmp/data").unwrap();
|
|
||||||
assert_eq!(file_endpoint.get_file_path(), expected_file_path("/tmp/data"));
|
|
||||||
|
|
||||||
let url_endpoint = Endpoint::try_from("http://example.com:9000/path/to/data").unwrap();
|
|
||||||
assert_eq!(url_endpoint.get_file_path(), "/path/to/data");
|
|
||||||
}
|
|
||||||
|
|
||||||
#[cfg(windows)]
|
|
||||||
#[test]
|
|
||||||
fn test_windows_url_drive_path_requires_separator_after_colon() {
|
|
||||||
let drive_path_endpoint = Endpoint::try_from("http://host/C:/data").unwrap();
|
|
||||||
assert_eq!(drive_path_endpoint.get_type(), EndpointType::Url);
|
|
||||||
assert!(has_leading_slash_windows_drive(Url::parse("http://host/C:/data").unwrap().path()));
|
|
||||||
|
|
||||||
let url_path_endpoint = Endpoint::try_from("http://host/C:foo").unwrap();
|
|
||||||
assert_eq!(url_path_endpoint.get_type(), EndpointType::Url);
|
|
||||||
assert!(!has_leading_slash_windows_drive(Url::parse("http://host/C:foo").unwrap().path()));
|
|
||||||
assert_eq!(url_path_endpoint.get_file_path(), "/C:foo");
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn test_endpoint_clone_and_equality() {
|
|
||||||
let endpoint1 = Endpoint::try_from("/tmp/data").unwrap();
|
|
||||||
let endpoint2 = endpoint1.clone();
|
|
||||||
|
|
||||||
assert_eq!(endpoint1, endpoint2);
|
|
||||||
assert_eq!(endpoint1.url, endpoint2.url);
|
|
||||||
assert_eq!(endpoint1.is_local, endpoint2.is_local);
|
|
||||||
assert_eq!(endpoint1.pool_idx, endpoint2.pool_idx);
|
|
||||||
assert_eq!(endpoint1.set_idx, endpoint2.set_idx);
|
|
||||||
assert_eq!(endpoint1.disk_idx, endpoint2.disk_idx);
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn test_endpoint_with_special_paths() {
|
|
||||||
// Test with complex paths
|
|
||||||
let complex_path = "/var/lib/rustfs/data/bucket1";
|
|
||||||
let endpoint = Endpoint::try_from(complex_path).unwrap();
|
|
||||||
assert_eq!(endpoint.get_file_path(), expected_file_path(complex_path));
|
|
||||||
assert!(endpoint.is_local);
|
|
||||||
assert_eq!(endpoint.get_type(), EndpointType::Path);
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn test_endpoint_with_spaces_in_path() {
|
|
||||||
let path_with_spaces = "/Users/test/Library/Application Support/rustfs/data";
|
|
||||||
let endpoint = Endpoint::try_from(path_with_spaces).unwrap();
|
|
||||||
assert_eq!(endpoint.get_file_path(), expected_file_path(path_with_spaces));
|
|
||||||
assert!(endpoint.is_local);
|
|
||||||
assert_eq!(endpoint.get_type(), EndpointType::Path);
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn test_endpoint_percent_encoding_roundtrip() {
|
|
||||||
let path_with_spaces = "/Users/test/Library/Application Support/rustfs/data";
|
|
||||||
let endpoint = Endpoint::try_from(path_with_spaces).unwrap();
|
|
||||||
|
|
||||||
// Verify that the URL internally stores percent-encoded path
|
|
||||||
assert!(
|
|
||||||
endpoint.url.path().contains("%20"),
|
|
||||||
"URL path should contain percent-encoded spaces: {}",
|
|
||||||
endpoint.url.path()
|
|
||||||
);
|
|
||||||
|
|
||||||
// Verify that get_file_path() decodes the percent-encoded path correctly
|
|
||||||
assert_eq!(
|
|
||||||
endpoint.get_file_path(),
|
|
||||||
expected_file_path("/Users/test/Library/Application Support/rustfs/data"),
|
|
||||||
"get_file_path() should decode percent-encoded spaces"
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn test_endpoint_with_various_special_characters() {
|
|
||||||
// Test path with multiple special characters that get percent-encoded
|
|
||||||
let path_with_special = "/tmp/test path/data[1]/file+name&more";
|
|
||||||
let endpoint = Endpoint::try_from(path_with_special).unwrap();
|
|
||||||
|
|
||||||
// get_file_path() should return the original path with decoded characters
|
|
||||||
assert_eq!(endpoint.get_file_path(), expected_file_path(path_with_special));
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn test_endpoint_update_is_local() {
|
|
||||||
let mut endpoint = Endpoint::try_from("http://localhost:9000/path").unwrap();
|
|
||||||
let result = endpoint.update_is_local(9000);
|
|
||||||
assert!(result.is_ok());
|
|
||||||
|
|
||||||
let mut file_endpoint = Endpoint::try_from("/tmp/data").unwrap();
|
|
||||||
let result = file_endpoint.update_is_local(9000);
|
|
||||||
assert!(result.is_ok());
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn test_url_parse_from_file_path() {
|
|
||||||
let result = url_parse_from_file_path("/tmp/test");
|
|
||||||
assert!(result.is_ok());
|
|
||||||
|
|
||||||
let url = result.unwrap();
|
|
||||||
assert_eq!(url.scheme(), "file");
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn test_endpoint_hash() {
|
|
||||||
use std::collections::HashSet;
|
|
||||||
|
|
||||||
let endpoint1 = Endpoint::try_from("/tmp/data1").unwrap();
|
|
||||||
let endpoint2 = Endpoint::try_from("/tmp/data2").unwrap();
|
|
||||||
let endpoint3 = endpoint1.clone();
|
|
||||||
|
|
||||||
let mut set = HashSet::new();
|
|
||||||
set.insert(endpoint1);
|
|
||||||
set.insert(endpoint2);
|
|
||||||
set.insert(endpoint3); // Should not be added as it's equal to endpoint1
|
|
||||||
|
|
||||||
assert_eq!(set.len(), 2);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|||||||
+1
-1696
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,634 @@
|
|||||||
|
// Copyright 2024 RustFS Team
|
||||||
|
//
|
||||||
|
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
// you may not use this file except in compliance with the License.
|
||||||
|
// You may obtain a copy of the License at
|
||||||
|
//
|
||||||
|
// http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
//
|
||||||
|
// Unless required by applicable law or agreed to in writing, software
|
||||||
|
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
// See the License for the specific language governing permissions and
|
||||||
|
// limitations under the License.
|
||||||
|
|
||||||
|
use crate::disk::error::{Error, Result};
|
||||||
|
use path_absolutize::Absolutize;
|
||||||
|
use rustfs_utils::{is_local_host, is_socket_addr};
|
||||||
|
use std::{fmt::Display, path::Path};
|
||||||
|
use tracing::debug;
|
||||||
|
use url::{ParseError, Url};
|
||||||
|
|
||||||
|
#[cfg(windows)]
|
||||||
|
pub(crate) fn windows_fallback_local_path(
|
||||||
|
path: &str,
|
||||||
|
canonicalize_error: &std::io::Error,
|
||||||
|
context: &'static str,
|
||||||
|
) -> std::io::Result<std::path::PathBuf> {
|
||||||
|
let absolute = Path::new(path).absolutize()?.to_path_buf();
|
||||||
|
tracing::warn!(
|
||||||
|
path = %path,
|
||||||
|
canonicalize_error = %canonicalize_error,
|
||||||
|
resolved = ?absolute,
|
||||||
|
context = context,
|
||||||
|
"using windows fallback path resolution for local endpoint"
|
||||||
|
);
|
||||||
|
Ok(absolute)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// enum for endpoint type.
|
||||||
|
#[derive(PartialEq, Eq, Debug)]
|
||||||
|
pub enum EndpointType {
|
||||||
|
/// path style endpoint type enum.
|
||||||
|
Path,
|
||||||
|
|
||||||
|
/// URL style endpoint type enum.
|
||||||
|
Url,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// any type of endpoint.
|
||||||
|
#[derive(Debug, PartialEq, Eq, Clone, Hash)]
|
||||||
|
pub struct Endpoint {
|
||||||
|
pub url: Url,
|
||||||
|
pub is_local: bool,
|
||||||
|
|
||||||
|
pub pool_idx: i32,
|
||||||
|
pub set_idx: i32,
|
||||||
|
pub disk_idx: i32,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Display for Endpoint {
|
||||||
|
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||||
|
if self.url.scheme() == "file" {
|
||||||
|
write!(f, "{}", self.get_file_path())
|
||||||
|
} else {
|
||||||
|
write!(f, "{}", self.url)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl TryFrom<&str> for Endpoint {
|
||||||
|
/// The type returned in the event of a conversion error.
|
||||||
|
type Error = Error;
|
||||||
|
|
||||||
|
/// Performs the conversion.
|
||||||
|
fn try_from(value: &str) -> std::result::Result<Self, Self::Error> {
|
||||||
|
// check whether given path is not empty.
|
||||||
|
if ["", "/", "\\"].iter().any(|&v| v.eq(value)) {
|
||||||
|
return Err(Error::other("empty or root endpoint is not supported"));
|
||||||
|
}
|
||||||
|
|
||||||
|
let mut is_local = false;
|
||||||
|
let url = match Url::parse(value) {
|
||||||
|
#[allow(unused_mut)]
|
||||||
|
Ok(mut url) if url.has_host() => {
|
||||||
|
// URL style of endpoint.
|
||||||
|
// Valid URL style endpoint is
|
||||||
|
// - Scheme field must contain "http" or "https"
|
||||||
|
// - All field should be empty except Host and Path.
|
||||||
|
if !((url.scheme() == "http" || url.scheme() == "https")
|
||||||
|
&& url.username().is_empty()
|
||||||
|
&& url.fragment().is_none()
|
||||||
|
&& url.query().is_none())
|
||||||
|
{
|
||||||
|
return Err(Error::other("invalid URL endpoint format"));
|
||||||
|
}
|
||||||
|
|
||||||
|
let path = url.path().to_string();
|
||||||
|
|
||||||
|
#[cfg(not(windows))]
|
||||||
|
let path = Path::new(&path).absolutize()?;
|
||||||
|
|
||||||
|
#[cfg(windows)]
|
||||||
|
let path = if has_leading_slash_windows_drive(&path) {
|
||||||
|
// Url::path() exposes file-like Windows paths as `/C:/...`.
|
||||||
|
// Strip only that synthetic leading slash; plain URL paths
|
||||||
|
// such as `/export1` must stay URL paths, not become
|
||||||
|
// relative paths under the current drive.
|
||||||
|
Path::new(&path[1..]).absolutize()?.to_string_lossy().into_owned()
|
||||||
|
} else {
|
||||||
|
path
|
||||||
|
};
|
||||||
|
#[cfg(windows)]
|
||||||
|
let path = Path::new(&path);
|
||||||
|
|
||||||
|
debug!("endpoint try_from: path={}", path.display());
|
||||||
|
|
||||||
|
if path.parent().is_none() || path.as_os_str().is_empty() {
|
||||||
|
return Err(Error::other("empty or root path is not supported in URL endpoint"));
|
||||||
|
}
|
||||||
|
|
||||||
|
match path.to_str() {
|
||||||
|
Some(v) => url.set_path(v),
|
||||||
|
None => return Err(Error::other("invalid path")),
|
||||||
|
}
|
||||||
|
|
||||||
|
url
|
||||||
|
}
|
||||||
|
Ok(_) => {
|
||||||
|
// like d:/foo
|
||||||
|
is_local = true;
|
||||||
|
url_parse_from_file_path(value)?
|
||||||
|
}
|
||||||
|
Err(e) => match e {
|
||||||
|
ParseError::InvalidPort => {
|
||||||
|
return Err(Error::other("invalid URL endpoint format: port number must be between 1 to 65535"));
|
||||||
|
}
|
||||||
|
ParseError::EmptyHost => return Err(Error::other("invalid URL endpoint format: empty host name")),
|
||||||
|
ParseError::RelativeUrlWithoutBase => {
|
||||||
|
// like /foo
|
||||||
|
is_local = true;
|
||||||
|
url_parse_from_file_path(value)?
|
||||||
|
}
|
||||||
|
_ => return Err(Error::other(format!("invalid URL endpoint format: {e}"))),
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
Ok(Endpoint {
|
||||||
|
url,
|
||||||
|
is_local,
|
||||||
|
pool_idx: -1,
|
||||||
|
set_idx: -1,
|
||||||
|
disk_idx: -1,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Endpoint {
|
||||||
|
/// returns type of endpoint.
|
||||||
|
pub fn get_type(&self) -> EndpointType {
|
||||||
|
if self.url.scheme() == "file" {
|
||||||
|
EndpointType::Path
|
||||||
|
} else {
|
||||||
|
EndpointType::Url
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// sets a specific pool number to this node
|
||||||
|
pub fn set_pool_index(&mut self, idx: usize) {
|
||||||
|
self.pool_idx = idx as i32
|
||||||
|
}
|
||||||
|
|
||||||
|
/// sets a specific set number to this node
|
||||||
|
pub fn set_set_index(&mut self, idx: usize) {
|
||||||
|
self.set_idx = idx as i32
|
||||||
|
}
|
||||||
|
|
||||||
|
/// sets a specific disk number to this node
|
||||||
|
pub fn set_disk_index(&mut self, idx: usize) {
|
||||||
|
self.disk_idx = idx as i32
|
||||||
|
}
|
||||||
|
|
||||||
|
/// resolves the host and updates if it is local or not.
|
||||||
|
pub fn update_is_local(&mut self, local_port: u16) -> Result<()> {
|
||||||
|
match (self.url.scheme(), self.url.host()) {
|
||||||
|
(v, Some(host)) if v != "file" => {
|
||||||
|
self.is_local = is_local_host(host, self.url.port().unwrap_or_default(), local_port)?;
|
||||||
|
}
|
||||||
|
_ => {}
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
/// returns the host to be used for grid connections.
|
||||||
|
pub fn grid_host(&self) -> String {
|
||||||
|
match (self.url.host(), self.url.port()) {
|
||||||
|
(Some(host), Some(port)) => {
|
||||||
|
debug!("grid_host scheme={}: host={}, port={}", self.url.scheme(), host, port);
|
||||||
|
format!("{}://{}:{}", self.url.scheme(), host, port)
|
||||||
|
}
|
||||||
|
(Some(host), None) => {
|
||||||
|
debug!("grid_host scheme={}: host={}", self.url.scheme(), host);
|
||||||
|
format!("{}://{}", self.url.scheme(), host)
|
||||||
|
}
|
||||||
|
_ => String::new(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn host_port(&self) -> String {
|
||||||
|
match (self.url.host(), self.url.port()) {
|
||||||
|
(Some(host), Some(port)) => {
|
||||||
|
debug!("host_port host={}, port={}", host, port);
|
||||||
|
format!("{host}:{port}")
|
||||||
|
}
|
||||||
|
(Some(host), None) => {
|
||||||
|
debug!("host_port host={}, port={}", host, self.url.port().unwrap_or(0));
|
||||||
|
format!("{host}")
|
||||||
|
}
|
||||||
|
_ => String::new(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn get_file_path(&self) -> String {
|
||||||
|
let path: &str = self.url.path();
|
||||||
|
let decoded: std::borrow::Cow<'_, str> = match urlencoding::decode(path) {
|
||||||
|
Ok(decoded) => decoded,
|
||||||
|
Err(e) => {
|
||||||
|
debug!("Failed to decode path '{}': {}, using original path", path, e);
|
||||||
|
std::borrow::Cow::Borrowed(path)
|
||||||
|
}
|
||||||
|
};
|
||||||
|
#[cfg(windows)]
|
||||||
|
if self.url.scheme() == "file" {
|
||||||
|
let stripped: &str = decoded.strip_prefix('/').unwrap_or(&decoded);
|
||||||
|
debug!("get_file_path windows: path={}", stripped);
|
||||||
|
return stripped.to_string();
|
||||||
|
}
|
||||||
|
decoded.into_owned()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(windows)]
|
||||||
|
fn has_leading_slash_windows_drive(path: &str) -> bool {
|
||||||
|
let bytes = path.as_bytes();
|
||||||
|
bytes.len() >= 4 && bytes[0] == b'/' && bytes[1].is_ascii_alphabetic() && bytes[2] == b':' && bytes[3] == b'/'
|
||||||
|
}
|
||||||
|
|
||||||
|
/// parse a file path into a URL.
|
||||||
|
fn url_parse_from_file_path(value: &str) -> Result<Url> {
|
||||||
|
// Only check if the arg is an ip address and ask for scheme since its absent.
|
||||||
|
// localhost, example.com, any FQDN cannot be disambiguated from a regular file path such as
|
||||||
|
// /mnt/export1. So we go ahead and start the rustfs server in FS modes in these cases.
|
||||||
|
let addr: Vec<&str> = value.splitn(2, '/').collect();
|
||||||
|
if is_socket_addr(addr[0]) {
|
||||||
|
return Err(Error::other("invalid URL endpoint format: missing scheme http or https"));
|
||||||
|
}
|
||||||
|
|
||||||
|
let file_path = match Path::new(value).absolutize() {
|
||||||
|
Ok(path) => path,
|
||||||
|
Err(err) => return Err(Error::other(format!("absolute path failed: {err}"))),
|
||||||
|
};
|
||||||
|
|
||||||
|
match Url::from_file_path(file_path) {
|
||||||
|
Ok(url) => Ok(url),
|
||||||
|
Err(_) => Err(Error::other("Convert a file path into an URL failed")),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod test {
|
||||||
|
use super::*;
|
||||||
|
|
||||||
|
fn expected_file_path(path: &str) -> String {
|
||||||
|
Path::new(path).absolutize().unwrap().to_string_lossy().replace('\\', "/")
|
||||||
|
}
|
||||||
|
|
||||||
|
fn expected_file_url(path: &str) -> Url {
|
||||||
|
url_parse_from_file_path(path).unwrap()
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_new_endpoint() {
|
||||||
|
#[derive(Default)]
|
||||||
|
struct TestCase<'a> {
|
||||||
|
arg: &'a str,
|
||||||
|
expected_endpoint: Option<Endpoint>,
|
||||||
|
expected_type: Option<EndpointType>,
|
||||||
|
expected_err: Option<Error>,
|
||||||
|
}
|
||||||
|
|
||||||
|
let u2 = Url::parse("https://example.org/path").unwrap();
|
||||||
|
let u4 = Url::parse("http://192.168.253.200/path").unwrap();
|
||||||
|
let u6 = Url::parse("http://server:/path").unwrap();
|
||||||
|
let root_slash_foo = expected_file_url("/foo");
|
||||||
|
|
||||||
|
let test_cases = [
|
||||||
|
TestCase {
|
||||||
|
arg: "/foo",
|
||||||
|
expected_endpoint: Some(Endpoint {
|
||||||
|
url: root_slash_foo,
|
||||||
|
is_local: true,
|
||||||
|
pool_idx: -1,
|
||||||
|
set_idx: -1,
|
||||||
|
disk_idx: -1,
|
||||||
|
}),
|
||||||
|
expected_type: Some(EndpointType::Path),
|
||||||
|
expected_err: None,
|
||||||
|
},
|
||||||
|
TestCase {
|
||||||
|
arg: "https://example.org/path",
|
||||||
|
expected_endpoint: Some(Endpoint {
|
||||||
|
url: u2,
|
||||||
|
is_local: false,
|
||||||
|
pool_idx: -1,
|
||||||
|
set_idx: -1,
|
||||||
|
disk_idx: -1,
|
||||||
|
}),
|
||||||
|
expected_type: Some(EndpointType::Url),
|
||||||
|
expected_err: None,
|
||||||
|
},
|
||||||
|
TestCase {
|
||||||
|
arg: "http://192.168.253.200/path",
|
||||||
|
expected_endpoint: Some(Endpoint {
|
||||||
|
url: u4,
|
||||||
|
is_local: false,
|
||||||
|
pool_idx: -1,
|
||||||
|
set_idx: -1,
|
||||||
|
disk_idx: -1,
|
||||||
|
}),
|
||||||
|
expected_type: Some(EndpointType::Url),
|
||||||
|
expected_err: None,
|
||||||
|
},
|
||||||
|
TestCase {
|
||||||
|
arg: "",
|
||||||
|
expected_endpoint: None,
|
||||||
|
expected_type: None,
|
||||||
|
expected_err: Some(Error::other("empty or root endpoint is not supported")),
|
||||||
|
},
|
||||||
|
TestCase {
|
||||||
|
arg: "/",
|
||||||
|
expected_endpoint: None,
|
||||||
|
expected_type: None,
|
||||||
|
expected_err: Some(Error::other("empty or root endpoint is not supported")),
|
||||||
|
},
|
||||||
|
TestCase {
|
||||||
|
arg: "\\",
|
||||||
|
expected_endpoint: None,
|
||||||
|
expected_type: None,
|
||||||
|
expected_err: Some(Error::other("empty or root endpoint is not supported")),
|
||||||
|
},
|
||||||
|
TestCase {
|
||||||
|
arg: "c://foo",
|
||||||
|
expected_endpoint: None,
|
||||||
|
expected_type: None,
|
||||||
|
expected_err: Some(Error::other("invalid URL endpoint format")),
|
||||||
|
},
|
||||||
|
TestCase {
|
||||||
|
arg: "ftp://foo",
|
||||||
|
expected_endpoint: None,
|
||||||
|
expected_type: None,
|
||||||
|
expected_err: Some(Error::other("invalid URL endpoint format")),
|
||||||
|
},
|
||||||
|
TestCase {
|
||||||
|
arg: "http://server/path?location",
|
||||||
|
expected_endpoint: None,
|
||||||
|
expected_type: None,
|
||||||
|
expected_err: Some(Error::other("invalid URL endpoint format")),
|
||||||
|
},
|
||||||
|
TestCase {
|
||||||
|
arg: "http://:/path",
|
||||||
|
expected_endpoint: None,
|
||||||
|
expected_type: None,
|
||||||
|
expected_err: Some(Error::other("invalid URL endpoint format: empty host name")),
|
||||||
|
},
|
||||||
|
TestCase {
|
||||||
|
arg: "http://:8080/path",
|
||||||
|
expected_endpoint: None,
|
||||||
|
expected_type: None,
|
||||||
|
expected_err: Some(Error::other("invalid URL endpoint format: empty host name")),
|
||||||
|
},
|
||||||
|
TestCase {
|
||||||
|
arg: "http://server:/path",
|
||||||
|
expected_endpoint: Some(Endpoint {
|
||||||
|
url: u6,
|
||||||
|
is_local: false,
|
||||||
|
pool_idx: -1,
|
||||||
|
set_idx: -1,
|
||||||
|
disk_idx: -1,
|
||||||
|
}),
|
||||||
|
expected_type: Some(EndpointType::Url),
|
||||||
|
expected_err: None,
|
||||||
|
},
|
||||||
|
TestCase {
|
||||||
|
arg: "https://93.184.216.34:808080/path",
|
||||||
|
expected_endpoint: None,
|
||||||
|
expected_type: None,
|
||||||
|
expected_err: Some(Error::other("invalid URL endpoint format: port number must be between 1 to 65535")),
|
||||||
|
},
|
||||||
|
TestCase {
|
||||||
|
arg: "http://server:8080//",
|
||||||
|
expected_endpoint: None,
|
||||||
|
expected_type: None,
|
||||||
|
expected_err: Some(Error::other("empty or root path is not supported in URL endpoint")),
|
||||||
|
},
|
||||||
|
TestCase {
|
||||||
|
arg: "http://server:8080/",
|
||||||
|
expected_endpoint: None,
|
||||||
|
expected_type: None,
|
||||||
|
expected_err: Some(Error::other("empty or root path is not supported in URL endpoint")),
|
||||||
|
},
|
||||||
|
TestCase {
|
||||||
|
arg: "192.168.1.210:9000",
|
||||||
|
expected_endpoint: None,
|
||||||
|
expected_type: None,
|
||||||
|
expected_err: Some(Error::other("invalid URL endpoint format: missing scheme http or https")),
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
for test_case in test_cases {
|
||||||
|
let ret = Endpoint::try_from(test_case.arg);
|
||||||
|
if test_case.expected_err.is_none() && ret.is_err() {
|
||||||
|
panic!("{}: error: expected = <nil>, got = {:?}", test_case.arg, ret);
|
||||||
|
}
|
||||||
|
if test_case.expected_err.is_some() && ret.is_ok() {
|
||||||
|
panic!("{}: error: expected = {:?}, got = <nil>", test_case.arg, test_case.expected_err);
|
||||||
|
}
|
||||||
|
match (test_case.expected_err, ret) {
|
||||||
|
(None, Err(e)) => panic!("{}: error: expected = <nil>, got = {}", test_case.arg, e),
|
||||||
|
(None, Ok(mut ep)) => {
|
||||||
|
let _ = ep.update_is_local(9000);
|
||||||
|
if test_case.expected_type != Some(ep.get_type()) {
|
||||||
|
panic!(
|
||||||
|
"{}: type: expected = {:?}, got = {:?}",
|
||||||
|
test_case.arg,
|
||||||
|
test_case.expected_type,
|
||||||
|
ep.get_type()
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
assert_eq!(test_case.expected_endpoint, Some(ep), "{}: endpoint", test_case.arg);
|
||||||
|
}
|
||||||
|
(Some(e), Ok(_)) => panic!("{}: error: expected = {}, got = <nil>", test_case.arg, e),
|
||||||
|
(Some(e), Err(e2)) => {
|
||||||
|
assert_eq!(e.to_string(), e2.to_string(), "{}: error: expected = {}, got = {}", test_case.arg, e, e2)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_endpoint_display() {
|
||||||
|
// Test file path display
|
||||||
|
let file_endpoint = Endpoint::try_from("/tmp/data").unwrap();
|
||||||
|
let display_str = format!("{file_endpoint}");
|
||||||
|
assert_eq!(display_str, expected_file_path("/tmp/data"));
|
||||||
|
|
||||||
|
// Test URL display
|
||||||
|
let url_endpoint = Endpoint::try_from("http://example.com:9000/path").unwrap();
|
||||||
|
let display_str = format!("{url_endpoint}");
|
||||||
|
assert_eq!(display_str, "http://example.com:9000/path");
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_endpoint_type() {
|
||||||
|
let file_endpoint = Endpoint::try_from("/tmp/data").unwrap();
|
||||||
|
assert_eq!(file_endpoint.get_type(), EndpointType::Path);
|
||||||
|
|
||||||
|
let url_endpoint = Endpoint::try_from("http://example.com:9000/path").unwrap();
|
||||||
|
assert_eq!(url_endpoint.get_type(), EndpointType::Url);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_endpoint_indexes() {
|
||||||
|
let mut endpoint = Endpoint::try_from("/tmp/data").unwrap();
|
||||||
|
|
||||||
|
// Test initial values
|
||||||
|
assert_eq!(endpoint.pool_idx, -1);
|
||||||
|
assert_eq!(endpoint.set_idx, -1);
|
||||||
|
assert_eq!(endpoint.disk_idx, -1);
|
||||||
|
|
||||||
|
// Test setting indexes
|
||||||
|
endpoint.set_pool_index(2);
|
||||||
|
endpoint.set_set_index(3);
|
||||||
|
endpoint.set_disk_index(4);
|
||||||
|
|
||||||
|
assert_eq!(endpoint.pool_idx, 2);
|
||||||
|
assert_eq!(endpoint.set_idx, 3);
|
||||||
|
assert_eq!(endpoint.disk_idx, 4);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_endpoint_grid_host() {
|
||||||
|
let endpoint = Endpoint::try_from("http://example.com:9000/path").unwrap();
|
||||||
|
assert_eq!(endpoint.grid_host(), "http://example.com:9000");
|
||||||
|
|
||||||
|
let endpoint_no_port = Endpoint::try_from("https://example.com/path").unwrap();
|
||||||
|
assert_eq!(endpoint_no_port.grid_host(), "https://example.com");
|
||||||
|
|
||||||
|
let file_endpoint = Endpoint::try_from("/tmp/data").unwrap();
|
||||||
|
assert_eq!(file_endpoint.grid_host(), "");
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_endpoint_host_port() {
|
||||||
|
let endpoint = Endpoint::try_from("http://example.com:9000/path").unwrap();
|
||||||
|
assert_eq!(endpoint.host_port(), "example.com:9000");
|
||||||
|
|
||||||
|
let endpoint_no_port = Endpoint::try_from("https://example.com/path").unwrap();
|
||||||
|
assert_eq!(endpoint_no_port.host_port(), "example.com");
|
||||||
|
|
||||||
|
let file_endpoint = Endpoint::try_from("/tmp/data").unwrap();
|
||||||
|
assert_eq!(file_endpoint.host_port(), "");
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_endpoint_get_file_path() {
|
||||||
|
let file_endpoint = Endpoint::try_from("/tmp/data").unwrap();
|
||||||
|
assert_eq!(file_endpoint.get_file_path(), expected_file_path("/tmp/data"));
|
||||||
|
|
||||||
|
let url_endpoint = Endpoint::try_from("http://example.com:9000/path/to/data").unwrap();
|
||||||
|
assert_eq!(url_endpoint.get_file_path(), "/path/to/data");
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(windows)]
|
||||||
|
#[test]
|
||||||
|
fn test_windows_url_drive_path_requires_separator_after_colon() {
|
||||||
|
let drive_path_endpoint = Endpoint::try_from("http://host/C:/data").unwrap();
|
||||||
|
assert_eq!(drive_path_endpoint.get_type(), EndpointType::Url);
|
||||||
|
assert!(has_leading_slash_windows_drive(Url::parse("http://host/C:/data").unwrap().path()));
|
||||||
|
|
||||||
|
let url_path_endpoint = Endpoint::try_from("http://host/C:foo").unwrap();
|
||||||
|
assert_eq!(url_path_endpoint.get_type(), EndpointType::Url);
|
||||||
|
assert!(!has_leading_slash_windows_drive(Url::parse("http://host/C:foo").unwrap().path()));
|
||||||
|
assert_eq!(url_path_endpoint.get_file_path(), "/C:foo");
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_endpoint_clone_and_equality() {
|
||||||
|
let endpoint1 = Endpoint::try_from("/tmp/data").unwrap();
|
||||||
|
let endpoint2 = endpoint1.clone();
|
||||||
|
|
||||||
|
assert_eq!(endpoint1, endpoint2);
|
||||||
|
assert_eq!(endpoint1.url, endpoint2.url);
|
||||||
|
assert_eq!(endpoint1.is_local, endpoint2.is_local);
|
||||||
|
assert_eq!(endpoint1.pool_idx, endpoint2.pool_idx);
|
||||||
|
assert_eq!(endpoint1.set_idx, endpoint2.set_idx);
|
||||||
|
assert_eq!(endpoint1.disk_idx, endpoint2.disk_idx);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_endpoint_with_special_paths() {
|
||||||
|
// Test with complex paths
|
||||||
|
let complex_path = "/var/lib/rustfs/data/bucket1";
|
||||||
|
let endpoint = Endpoint::try_from(complex_path).unwrap();
|
||||||
|
assert_eq!(endpoint.get_file_path(), expected_file_path(complex_path));
|
||||||
|
assert!(endpoint.is_local);
|
||||||
|
assert_eq!(endpoint.get_type(), EndpointType::Path);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_endpoint_with_spaces_in_path() {
|
||||||
|
let path_with_spaces = "/Users/test/Library/Application Support/rustfs/data";
|
||||||
|
let endpoint = Endpoint::try_from(path_with_spaces).unwrap();
|
||||||
|
assert_eq!(endpoint.get_file_path(), expected_file_path(path_with_spaces));
|
||||||
|
assert!(endpoint.is_local);
|
||||||
|
assert_eq!(endpoint.get_type(), EndpointType::Path);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_endpoint_percent_encoding_roundtrip() {
|
||||||
|
let path_with_spaces = "/Users/test/Library/Application Support/rustfs/data";
|
||||||
|
let endpoint = Endpoint::try_from(path_with_spaces).unwrap();
|
||||||
|
|
||||||
|
// Verify that the URL internally stores percent-encoded path
|
||||||
|
assert!(
|
||||||
|
endpoint.url.path().contains("%20"),
|
||||||
|
"URL path should contain percent-encoded spaces: {}",
|
||||||
|
endpoint.url.path()
|
||||||
|
);
|
||||||
|
|
||||||
|
// Verify that get_file_path() decodes the percent-encoded path correctly
|
||||||
|
assert_eq!(
|
||||||
|
endpoint.get_file_path(),
|
||||||
|
expected_file_path("/Users/test/Library/Application Support/rustfs/data"),
|
||||||
|
"get_file_path() should decode percent-encoded spaces"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_endpoint_with_various_special_characters() {
|
||||||
|
// Test path with multiple special characters that get percent-encoded
|
||||||
|
let path_with_special = "/tmp/test path/data[1]/file+name&more";
|
||||||
|
let endpoint = Endpoint::try_from(path_with_special).unwrap();
|
||||||
|
|
||||||
|
// get_file_path() should return the original path with decoded characters
|
||||||
|
assert_eq!(endpoint.get_file_path(), expected_file_path(path_with_special));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_endpoint_update_is_local() {
|
||||||
|
let mut endpoint = Endpoint::try_from("http://localhost:9000/path").unwrap();
|
||||||
|
let result = endpoint.update_is_local(9000);
|
||||||
|
assert!(result.is_ok());
|
||||||
|
|
||||||
|
let mut file_endpoint = Endpoint::try_from("/tmp/data").unwrap();
|
||||||
|
let result = file_endpoint.update_is_local(9000);
|
||||||
|
assert!(result.is_ok());
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_url_parse_from_file_path() {
|
||||||
|
let result = url_parse_from_file_path("/tmp/test");
|
||||||
|
assert!(result.is_ok());
|
||||||
|
|
||||||
|
let url = result.unwrap();
|
||||||
|
assert_eq!(url.scheme(), "file");
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_endpoint_hash() {
|
||||||
|
use std::collections::HashSet;
|
||||||
|
|
||||||
|
let endpoint1 = Endpoint::try_from("/tmp/data1").unwrap();
|
||||||
|
let endpoint2 = Endpoint::try_from("/tmp/data2").unwrap();
|
||||||
|
let endpoint3 = endpoint1.clone();
|
||||||
|
|
||||||
|
let mut set = HashSet::new();
|
||||||
|
set.insert(endpoint1);
|
||||||
|
set.insert(endpoint2);
|
||||||
|
set.insert(endpoint3); // Should not be added as it's equal to endpoint1
|
||||||
|
|
||||||
|
assert_eq!(set.len(), 2);
|
||||||
|
}
|
||||||
|
}
|
||||||
File diff suppressed because it is too large
Load Diff
@@ -5,5 +5,7 @@
|
|||||||
//! file moves happen.
|
//! file moves happen.
|
||||||
|
|
||||||
pub(crate) mod disks_layout;
|
pub(crate) mod disks_layout;
|
||||||
|
pub(crate) mod endpoint;
|
||||||
|
pub(crate) mod endpoints;
|
||||||
pub(crate) mod format;
|
pub(crate) mod format;
|
||||||
pub(crate) mod set_layout;
|
pub(crate) mod set_layout;
|
||||||
|
|||||||
@@ -5,17 +5,16 @@ Status values: `[ ]` not started, `[~]` in progress, `[x]` complete, `[!]` block
|
|||||||
## Current Context
|
## Current Context
|
||||||
|
|
||||||
- Issue: [`rustfs/backlog#660`](https://github.com/rustfs/backlog/issues/660)
|
- Issue: [`rustfs/backlog#660`](https://github.com/rustfs/backlog/issues/660)
|
||||||
- Branch: `overtrue/arch-ecstore-layout-format-move`
|
- Branch: `overtrue/arch-ecstore-layout-endpoints-move`
|
||||||
- Baseline: `overtrue/arch-ecstore-layout-foundation`
|
- Baseline: merged `E-002/E-LAYOUT-001`.
|
||||||
(`2e10661d088a665c789fdb092871c6b52675aa1e`).
|
- Stacked on: merged ECStore layout foundation and format layout ownership
|
||||||
- Stacked on: local E-001/E-SET-001, which is stacked on local R-033 and
|
slices.
|
||||||
local R-032.
|
|
||||||
- PR type for this branch: `pure-move`
|
- PR type for this branch: `pure-move`
|
||||||
- Runtime behavior changes: none.
|
- Runtime behavior changes: none.
|
||||||
- Rust code changes: pure-move ECStore format and disk-layout expansion modules
|
- Rust code changes: pure-move ECStore endpoint parsing and endpoint grouping
|
||||||
into the internal layout bucket while preserving old public paths.
|
modules into the internal layout bucket while preserving old public paths.
|
||||||
- CI/script changes: none.
|
- CI/script changes: none.
|
||||||
- Docs changes: record the ECStore format/disk-layout pure move slice.
|
- Docs changes: record the ECStore endpoint pure move slice.
|
||||||
|
|
||||||
## Phase 0 Tasks
|
## Phase 0 Tasks
|
||||||
|
|
||||||
@@ -2241,10 +2240,23 @@ Status values: `[ ]` not started, `[~]` in progress, `[x]` complete, `[!]` block
|
|||||||
diff hygiene, Rust risk scan, branch freshness check, pre-commit quality
|
diff hygiene, Rust risk scan, branch freshness check, pre-commit quality
|
||||||
gate, and three-expert review.
|
gate, and three-expert review.
|
||||||
|
|
||||||
|
- [x] `E-003/E-LAYOUT-002` Move ECStore endpoint layout owners.
|
||||||
|
- Do: pure-move endpoint parsing and endpoint grouping into the ECStore
|
||||||
|
layout bucket while keeping compatibility stubs at the old public paths.
|
||||||
|
- Acceptance: `crate::disk::endpoint::*` and `crate::endpoints::*` remain
|
||||||
|
usable, `layout::endpoint` owns `Endpoint`, and `layout::endpoints` owns
|
||||||
|
`EndpointServerPools` and endpoint grouping.
|
||||||
|
- Must preserve: endpoint string parsing, URL/path validation, local-host
|
||||||
|
detection, pool/set/disk indexes, endpoint grouping, disk independence
|
||||||
|
checks, setup type classification, and old public module paths.
|
||||||
|
- Verification: focused ECStore endpoint tests, ECStore/RustFS/Heal compile
|
||||||
|
checks, migration/layer guards, formatting, diff hygiene, Rust risk scan,
|
||||||
|
branch freshness check, pre-commit quality gate, and three-expert review.
|
||||||
|
|
||||||
## Next PRs
|
## Next PRs
|
||||||
|
|
||||||
1. `pure-move`: continue moving endpoint grouping and runtime-neutral layout
|
1. `pure-move`: continue moving runtime-neutral pool/set layout helpers once
|
||||||
helpers once E-002/E-LAYOUT-001 lands.
|
E-003/E-LAYOUT-002 lands.
|
||||||
2. `pure-move`: continue pruning residual embedded startup-only orchestration
|
2. `pure-move`: continue pruning residual embedded startup-only orchestration
|
||||||
once the lifecycle helpers are merged.
|
once the lifecycle helpers are merged.
|
||||||
|
|
||||||
@@ -2252,9 +2264,9 @@ Status values: `[ ]` not started, `[~]` in progress, `[x]` complete, `[!]` block
|
|||||||
|
|
||||||
| Expert | Status | Notes |
|
| Expert | Status | Notes |
|
||||||
|---|---|---|
|
|---|---|---|
|
||||||
| Quality/architecture | passed | E-002/E-LAYOUT-001 is a pure move into ECStore layout with compatibility stubs at old public paths; no new runtime owner or dependency boundary is introduced. |
|
| Quality/architecture | passed | E-003/E-LAYOUT-002 is a pure move into ECStore layout with compatibility stubs at old public paths; no new runtime owner or dependency boundary is introduced. |
|
||||||
| Migration preservation | passed | Format JSON, disk UUID lookup, distribution algorithm, disk-layout expansion, and old public module paths remain preserved. |
|
| Migration preservation | passed | Endpoint parsing, local-host detection, pool/set/disk indexes, endpoint grouping, disk independence checks, setup type classification, and old public module paths remain preserved. |
|
||||||
| Testing/verification | passed | Focused format/layout checks, compile checks, guards, formatting, diff hygiene, Rust risk scan, and full pre-commit passed. |
|
| Testing/verification | passed | Focused endpoint/layout checks, compile checks, guards, formatting, diff hygiene, Rust risk scan, and full pre-commit passed. |
|
||||||
|
|
||||||
## Verification Notes
|
## Verification Notes
|
||||||
|
|
||||||
@@ -2329,6 +2341,19 @@ Passed before push:
|
|||||||
- `make pre-commit`: passed.
|
- `make pre-commit`: passed.
|
||||||
- Three-expert review: passed.
|
- Three-expert review: passed.
|
||||||
|
|
||||||
|
- Issue #660 E-003/E-LAYOUT-002 current slice:
|
||||||
|
- `cargo test -p rustfs-ecstore layout::endpoint -- --nocapture`: passed.
|
||||||
|
- `cargo test -p rustfs-ecstore layout::endpoints -- --nocapture`: passed.
|
||||||
|
- `cargo check -p rustfs-ecstore -p rustfs -p rustfs-heal`: passed.
|
||||||
|
- `./scripts/check_architecture_migration_rules.sh`: passed.
|
||||||
|
- `./scripts/check_layer_dependencies.sh`: passed.
|
||||||
|
- `cargo fmt --all --check`: passed.
|
||||||
|
- `git diff --check`: passed.
|
||||||
|
- Rust risk scan on changed Rust files: passed; only existing endpoint
|
||||||
|
production/test unwrap and expectation paths were moved.
|
||||||
|
- `make pre-commit`: passed.
|
||||||
|
- Three-expert review: passed.
|
||||||
|
|
||||||
- Issue #660 X-012 current slice:
|
- Issue #660 X-012 current slice:
|
||||||
- `cargo test -p rustfs-extension-schema`: passed.
|
- `cargo test -p rustfs-extension-schema`: passed.
|
||||||
- `cargo check -p rustfs-extension-schema`: passed.
|
- `cargo check -p rustfs-extension-schema`: passed.
|
||||||
|
|||||||
Reference in New Issue
Block a user