From c07d165cebb54270947654b5a4ddf1e5da9cc940 Mon Sep 17 00:00:00 2001 From: weisd Date: Sun, 23 Feb 2025 18:28:34 +0800 Subject: [PATCH] fix admin api bugs --- .gitignore | 1 + Cargo.lock | 1 + iam/src/auth/credentials.rs | 12 ++- iam/src/manager.rs | 22 ++++- rustfs/Cargo.toml | 1 + rustfs/src/admin/handlers.rs | 141 +------------------------------ rustfs/src/admin/handlers/sts.rs | 139 ++++++++++++++++++++++++++++++ rustfs/src/admin/mod.rs | 4 +- rustfs/src/auth.rs | 4 - rustfs/src/config/mod.rs | 3 + rustfs/src/console.rs | 103 ++++++++++++++++++++-- rustfs/src/main.rs | 2 +- rustfs/static/index.html | 1 - rustfs/static/readme.md | 1 + scripts/run.sh | 13 +-- 15 files changed, 287 insertions(+), 161 deletions(-) create mode 100644 rustfs/src/admin/handlers/sts.rs delete mode 100644 rustfs/static/index.html create mode 100644 rustfs/static/readme.md diff --git a/.gitignore b/.gitignore index 94fd091d..81d415f6 100644 --- a/.gitignore +++ b/.gitignore @@ -5,3 +5,4 @@ /test /logs .devcontainer +rustfs/static/* diff --git a/Cargo.lock b/Cargo.lock index 2b12bad9..a5e26554 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -5345,6 +5345,7 @@ dependencies = [ "madmin", "matchit 0.8.6", "mime", + "mime_guess", "netif", "pin-project-lite", "prost", diff --git a/iam/src/auth/credentials.rs b/iam/src/auth/credentials.rs index 457df7d9..01e832b4 100644 --- a/iam/src/auth/credentials.rs +++ b/iam/src/auth/credentials.rs @@ -8,6 +8,7 @@ use serde::de::DeserializeOwned; use serde::{Deserialize, Serialize}; use serde_json::{json, Value}; use std::collections::HashMap; +use time::macros::offset; use time::OffsetDateTime; const ACCESS_KEY_MIN_LEN: usize = 3; @@ -212,10 +213,19 @@ pub fn create_new_credentials_with_metadata( return Err(Error::new(IamError::InvalidAccessKeyLength)); } + if token_secret.is_empty() { + return Ok(Credentials { + access_key: ak.to_owned(), + secret_key: sk.to_owned(), + status: ACCOUNT_OFF.to_owned(), + ..Default::default() + }); + } + let expiration = { if let Some(v) = claims.get("exp") { if let Some(expiry) = v.as_i64() { - Some(OffsetDateTime::from_unix_timestamp(expiry)?.to_offset(OffsetDateTime::now_utc().offset())) + Some(OffsetDateTime::from_unix_timestamp(expiry)?.to_offset(offset!(+8))) } else { None } diff --git a/iam/src/manager.rs b/iam/src/manager.rs index ee87f1ba..d257b49e 100644 --- a/iam/src/manager.rs +++ b/iam/src/manager.rs @@ -4,6 +4,7 @@ use crate::{ cache::{Cache, CacheEntity}, error::{is_err_no_such_group, is_err_no_such_policy, is_err_no_such_user, Error as IamError}, format::Format, + get_global_action_cred, policy::{Policy, PolicyDoc, DEFAULT_POLICIES}, store::{object::IAM_CONFIG_PREFIX, GroupInfo, MappedPolicy, Store, UserType}, sys::{ @@ -1577,8 +1578,27 @@ fn set_default_canned_policies(policies: &mut HashMap) { } } +pub fn get_token_signing_key() -> Option { + if let Some(s) = get_global_action_cred() { + Some(s.secret_key.clone()) + } else { + None + } +} + pub fn extract_jwt_claims(u: &UserIdentity) -> Result> { - get_claims_from_token_with_secret(&u.credentials.session_token, &u.credentials.secret_key) + let Some(sys_key) = get_token_signing_key() else { + return Err(Error::msg("global active sk not init")); + }; + + let keys = vec![&sys_key, &u.credentials.secret_key]; + + for key in keys { + if let Ok(claims) = get_claims_from_token_with_secret(&u.credentials.session_token, key) { + return Ok(claims); + } + } + Err(Error::msg("unable to extract claims")) } fn filter_policies(cache: &Cache, policy_name: &str, bucket_name: &str) -> (String, Policy) { diff --git a/rustfs/Cargo.toml b/rustfs/Cargo.toml index 5b3b89f4..9ad70c4e 100644 --- a/rustfs/Cargo.toml +++ b/rustfs/Cargo.toml @@ -70,6 +70,7 @@ iam = { path = "../iam" } jsonwebtoken = "9.3.0" tower-http = { version = "0.6.2", features = ["cors"] } include_dir = "0.7.4" +mime_guess = "2.0.5" [build-dependencies] prost-build.workspace = true diff --git a/rustfs/src/admin/handlers.rs b/rustfs/src/admin/handlers.rs index 0b239bf2..dc9bfba0 100644 --- a/rustfs/src/admin/handlers.rs +++ b/rustfs/src/admin/handlers.rs @@ -18,12 +18,11 @@ use ecstore::store::is_valid_object_prefix; use ecstore::store_api::StorageAPI; use ecstore::utils::crypto::base64_encode; use ecstore::utils::path::path_join; -use ecstore::utils::xml; use ecstore::GLOBAL_Endpoints; use futures::{Stream, StreamExt}; use http::{HeaderMap, Uri}; use hyper::StatusCode; -use iam::auth::{get_claims_from_token_with_secret, get_new_credentials_with_metadata}; +use iam::auth::get_claims_from_token_with_secret; use iam::error::Error as IamError; use iam::policy::Policy; use iam::sys::SESSION_POLICY_NAME; @@ -33,10 +32,7 @@ use madmin::utils::parse_duration; use matchit::Params; use s3s::header::CONTENT_TYPE; use s3s::stream::{ByteStream, DynByteStream}; -use s3s::{ - dto::{AssumeRoleOutput, Credentials, Timestamp}, - s3_error, Body, S3Error, S3Request, S3Response, S3Result, -}; +use s3s::{s3_error, Body, S3Error, S3Request, S3Response, S3Result}; use s3s::{S3ErrorCode, StdError}; use serde::{Deserialize, Serialize}; use serde_json::Value; @@ -47,7 +43,6 @@ use std::pin::Pin; use std::sync::Arc; use std::task::{Context, Poll}; use std::time::Duration as std_Duration; -use time::{Duration, OffsetDateTime}; use tokio::sync::mpsc::{self}; use tokio::time::interval; use tokio::{select, spawn}; @@ -57,32 +52,10 @@ use tracing::{error, info, warn}; pub mod group; pub mod policy; pub mod service_account; +pub mod sts; pub mod trace; pub mod user; -const ASSUME_ROLE_ACTION: &str = "AssumeRole"; -const ASSUME_ROLE_VERSION: &str = "2011-06-15"; - -#[derive(Deserialize, Debug, Default)] -#[serde(rename_all = "PascalCase", default)] -pub struct AssumeRoleRequest { - pub action: String, - pub duration_seconds: usize, - pub version: String, - pub role_arn: String, - pub role_session_name: String, - pub policy: String, - pub external_id: String, -} - -fn get_token_signing_key() -> Option { - if let Some(s) = get_global_action_cred() { - Some(s.secret_key.clone()) - } else { - None - } -} - // check_key_valid get auth.cred pub async fn check_key_valid(security_token: Option, ak: &str) -> S3Result<(auth::Credentials, bool)> { let Some(mut cred) = get_global_action_cred() else { @@ -216,114 +189,6 @@ pub fn populate_session_policy(claims: &mut HashMap, policy: &str Ok(()) } -pub struct AssumeRoleHandle {} -#[async_trait::async_trait] -impl Operation for AssumeRoleHandle { - async fn call(&self, req: S3Request, _params: Params<'_, '_>) -> S3Result> { - warn!("handle AssumeRoleHandle"); - - let Some(user) = req.credentials else { return Err(s3_error!(InvalidRequest, "get cred failed")) }; - - let session_token = get_session_token(&req.headers); - if session_token.is_some() { - return Err(s3_error!(InvalidRequest, "AccessDenied1")); - } - - let (cred, _owner) = check_key_valid(session_token, &user.access_key).await?; - - // // TODO: 判断权限, 不允许sts访问 - if cred.is_temp() || cred.is_service_account() { - return Err(s3_error!(InvalidRequest, "AccessDenied")); - } - - let mut input = req.input; - - let bytes = match input.store_all_unlimited().await { - Ok(b) => b, - Err(e) => { - warn!("get body failed, e: {:?}", e); - return Err(s3_error!(InvalidRequest, "get body failed")); - } - }; - - let body: AssumeRoleRequest = from_bytes(&bytes).map_err(|_e| s3_error!(InvalidRequest, "get body failed"))?; - - if body.action.as_str() != ASSUME_ROLE_ACTION { - return Err(s3_error!(InvalidArgument, "not suport action")); - } - - if body.version.as_str() != ASSUME_ROLE_VERSION { - return Err(s3_error!(InvalidArgument, "not suport version")); - } - - let mut claims = cred.claims.unwrap_or_default(); - - populate_session_policy(&mut claims, &body.policy)?; - - let exp = { - if body.duration_seconds > 0 { - body.duration_seconds - } else { - 3600 - } - }; - - claims.insert( - "exp".to_string(), - serde_json::Value::Number(serde_json::Number::from(OffsetDateTime::now_utc().unix_timestamp() + exp as i64)), - ); - - claims.insert("parent".to_string(), serde_json::Value::String(cred.access_key.clone())); - - // warn!("AssumeRole get cred {:?}", &user); - // warn!("AssumeRole get body {:?}", &body); - - let Ok(iam_store) = iam::get() else { return Err(s3_error!(InvalidRequest, "iam not init")) }; - - if let Err(_err) = iam_store.policy_db_get(&cred.access_key, &cred.groups).await { - return Err(s3_error!(InvalidArgument, "invalid policy arg")); - } - - let Some(secret) = get_token_signing_key() else { - return Err(s3_error!(InvalidArgument, "global active sk not init")); - }; - - info!("AssumeRole get claims {:?}", &claims); - - let mut new_cred = get_new_credentials_with_metadata(&claims, &secret) - .map_err(|e| S3Error::with_message(S3ErrorCode::InternalError, format!("get new cred failed {}", e)))?; - - new_cred.parent_user = cred.access_key.clone(); - - info!("AssumeRole get new_cred {:?}", &new_cred); - - if let Err(_err) = iam_store.set_temp_user(&new_cred.access_key, &new_cred, None).await { - return Err(s3_error!(InternalError, "set_temp_user failed")); - } - - // TODO: globalSiteReplicationSys - - let resp = AssumeRoleOutput { - credentials: Some(Credentials { - access_key_id: new_cred.access_key, - expiration: Timestamp::from( - new_cred - .expiration - .unwrap_or(OffsetDateTime::now_utc().saturating_add(Duration::seconds(3600))), - ), - secret_access_key: new_cred.secret_key, - session_token: new_cred.session_token, - }), - ..Default::default() - }; - - // getAssumeRoleCredentials - let output = xml::serialize::(&resp).unwrap(); - - Ok(S3Response::new((StatusCode::OK, Body::from(output)))) - } -} - #[derive(Debug, Serialize, Default)] #[serde(rename_all = "PascalCase", default)] pub struct AccountInfo { diff --git a/rustfs/src/admin/handlers/sts.rs b/rustfs/src/admin/handlers/sts.rs new file mode 100644 index 00000000..adb31e7d --- /dev/null +++ b/rustfs/src/admin/handlers/sts.rs @@ -0,0 +1,139 @@ +use crate::admin::{ + handlers::{check_key_valid, get_session_token, populate_session_policy}, + router::Operation, +}; +use ecstore::utils::xml; +use http::StatusCode; +use iam::{auth::get_new_credentials_with_metadata, manager::get_token_signing_key}; +use matchit::Params; +use s3s::{ + dto::{AssumeRoleOutput, Credentials, Timestamp}, + s3_error, Body, S3Error, S3ErrorCode, S3Request, S3Response, S3Result, +}; +use serde::Deserialize; +use serde_urlencoded::from_bytes; +use time::{Duration, OffsetDateTime}; +use tracing::{info, warn}; + +const ASSUME_ROLE_ACTION: &str = "AssumeRole"; +const ASSUME_ROLE_VERSION: &str = "2011-06-15"; + +#[derive(Deserialize, Debug, Default)] +#[serde(rename_all = "PascalCase", default)] +pub struct AssumeRoleRequest { + pub action: String, + pub duration_seconds: usize, + pub version: String, + pub role_arn: String, + pub role_session_name: String, + pub policy: String, + pub external_id: String, +} + +pub struct AssumeRoleHandle {} +#[async_trait::async_trait] +impl Operation for AssumeRoleHandle { + async fn call(&self, req: S3Request, _params: Params<'_, '_>) -> S3Result> { + warn!("handle AssumeRoleHandle"); + + let Some(user) = req.credentials else { return Err(s3_error!(InvalidRequest, "get cred failed")) }; + + let session_token = get_session_token(&req.headers); + if session_token.is_some() { + return Err(s3_error!(InvalidRequest, "AccessDenied1")); + } + + let (cred, _owner) = check_key_valid(session_token, &user.access_key).await?; + + // // TODO: 判断权限, 不允许sts访问 + if cred.is_temp() || cred.is_service_account() { + return Err(s3_error!(InvalidRequest, "AccessDenied")); + } + + let mut input = req.input; + + let bytes = match input.store_all_unlimited().await { + Ok(b) => b, + Err(e) => { + warn!("get body failed, e: {:?}", e); + return Err(s3_error!(InvalidRequest, "get body failed")); + } + }; + + let body: AssumeRoleRequest = from_bytes(&bytes).map_err(|_e| s3_error!(InvalidRequest, "get body failed"))?; + + if body.action.as_str() != ASSUME_ROLE_ACTION { + return Err(s3_error!(InvalidArgument, "not suport action")); + } + + if body.version.as_str() != ASSUME_ROLE_VERSION { + return Err(s3_error!(InvalidArgument, "not suport version")); + } + + let mut claims = cred.claims.unwrap_or_default(); + + populate_session_policy(&mut claims, &body.policy)?; + + let exp = { + if body.duration_seconds > 0 { + body.duration_seconds + } else { + 3600 + } + }; + + claims.insert( + "exp".to_string(), + serde_json::Value::Number(serde_json::Number::from(OffsetDateTime::now_utc().unix_timestamp() + exp as i64)), + ); + + claims.insert("parent".to_string(), serde_json::Value::String(cred.access_key.clone())); + + // warn!("AssumeRole get cred {:?}", &user); + // warn!("AssumeRole get body {:?}", &body); + + let Ok(iam_store) = iam::get() else { return Err(s3_error!(InvalidRequest, "iam not init")) }; + + if let Err(_err) = iam_store.policy_db_get(&cred.access_key, &cred.groups).await { + return Err(s3_error!(InvalidArgument, "invalid policy arg")); + } + + let Some(secret) = get_token_signing_key() else { + return Err(s3_error!(InvalidArgument, "global active sk not init")); + }; + + info!("AssumeRole get claims {:?}", &claims); + + let mut new_cred = get_new_credentials_with_metadata(&claims, &secret) + .map_err(|e| S3Error::with_message(S3ErrorCode::InternalError, format!("get new cred failed {}", e)))?; + + new_cred.parent_user = cred.access_key.clone(); + + info!("AssumeRole get new_cred {:?}", &new_cred); + + if let Err(_err) = iam_store.set_temp_user(&new_cred.access_key, &new_cred, None).await { + return Err(s3_error!(InternalError, "set_temp_user failed")); + } + + // TODO: globalSiteReplicationSys + + let resp = AssumeRoleOutput { + credentials: Some(Credentials { + access_key_id: new_cred.access_key, + expiration: Timestamp::from( + new_cred + .expiration + .unwrap_or(OffsetDateTime::now_utc().saturating_add(Duration::seconds(3600))), + ), + secret_access_key: new_cred.secret_key, + session_token: new_cred.session_token, + }), + ..Default::default() + }; + + // getAssumeRoleCredentials + let output = xml::serialize::(&resp).unwrap(); + + Ok(S3Response::new((StatusCode::OK, Body::from(output)))) + } +} diff --git a/rustfs/src/admin/mod.rs b/rustfs/src/admin/mod.rs index 119f1883..9c8f2403 100644 --- a/rustfs/src/admin/mod.rs +++ b/rustfs/src/admin/mod.rs @@ -7,7 +7,7 @@ use common::error::Result; use handlers::{ group, policy, service_account::{AddServiceAccount, DeleteServiceAccount, InfoServiceAccount, ListServiceAccount, UpdateServiceAccount}, - user, + sts, user, }; use hyper::Method; use router::{AdminOperation, S3Router}; @@ -19,7 +19,7 @@ pub fn make_admin_route() -> Result { let mut r: S3Router = S3Router::new(); // 1 - r.insert(Method::POST, "/", AdminOperation(&handlers::AssumeRoleHandle {}))?; + r.insert(Method::POST, "/", AdminOperation(&sts::AssumeRoleHandle {}))?; regist_user_route(&mut r)?; diff --git a/rustfs/src/auth.rs b/rustfs/src/auth.rs index d7408110..1f673a09 100644 --- a/rustfs/src/auth.rs +++ b/rustfs/src/auth.rs @@ -1,4 +1,3 @@ -use log::warn; use s3s::auth::S3Auth; use s3s::auth::SecretKey; use s3s::auth::SimpleAuth; @@ -27,11 +26,8 @@ impl S3Auth for IAMAuth { return Ok(key); } - warn!("Failed to get secret key from simple auth"); - if let Ok(iam_store) = iam::get() { if let Some(id) = iam_store.get_user(access_key).await { - warn!("get cred {:?}", id.credentials); return Ok(SecretKey::from(id.credentials.secret_key.clone())); } } diff --git a/rustfs/src/config/mod.rs b/rustfs/src/config/mod.rs index 477998a8..701ee5f5 100644 --- a/rustfs/src/config/mod.rs +++ b/rustfs/src/config/mod.rs @@ -42,6 +42,9 @@ pub struct Opt { #[arg(long, default_value_t = format!("0.0.0.0:{}", DEFAULT_PORT), env = "RUSTFS_ADDRESS")] pub address: String, + #[arg(long, default_value_t = format!("http://localhost:{}", DEFAULT_PORT), env = "RUSTFS_SERVER_ENDPOINT")] + pub server_endpoint: String, + /// Access key used for authentication. #[arg(long, default_value_t = DEFAULT_ACCESS_KEY.to_string(), env = "RUSTFS_ACCESS_KEY")] pub access_key: String, diff --git a/rustfs/src/console.rs b/rustfs/src/console.rs index 704a575e..f108af96 100644 --- a/rustfs/src/console.rs +++ b/rustfs/src/console.rs @@ -7,15 +7,28 @@ use axum::{ }; use include_dir::{include_dir, Dir}; +use mime_guess::from_path; use serde::Serialize; static STATIC_DIR: Dir = include_dir!("$CARGO_MANIFEST_DIR/static"); async fn static_handler(uri: axum::http::Uri) -> impl IntoResponse { - let path = uri.path().trim_start_matches('/'); + let mut path = uri.path().trim_start_matches('/'); + if path.is_empty() { + path = "index.html" + } if let Some(file) = STATIC_DIR.get_file(path) { + let mime_type = from_path(file.path().as_os_str()).first_or_octet_stream(); Response::builder() .status(StatusCode::OK) + .header("Content-Type", mime_type.to_string()) + .body(Body::from(file.contents())) + .unwrap() + } else if let Some(file) = STATIC_DIR.get_file("index.html") { + let mime_type = from_path(file.path().as_os_str()).first_or_octet_stream(); + Response::builder() + .status(StatusCode::OK) + .header("Content-Type", mime_type.to_string()) .body(Body::from(file.contents())) .unwrap() } else { @@ -26,13 +39,66 @@ async fn static_handler(uri: axum::http::Uri) -> impl IntoResponse { } } -#[derive(Debug, Clone, Serialize)] +#[derive(Debug, Serialize)] struct Config { - fs_addr: String, + api: Api, + s3: S3, + release: Release, + license: License, +} + +impl Config { + fn new(url: &str, version: &str, date: &str) -> Self { + Config { + api: Api { + base_url: format!("{}/rustfs/admin/v3", url), + }, + s3: S3 { + endpoint: url.to_owned(), + region: "cn-east-1".to_owned(), + }, + release: Release { + version: version.to_string(), + date: date.to_string(), + }, + license: License { + name: "Apache-2.0".to_string(), + url: "https://www.apache.org/licenses/LICENSE-2.0".to_string(), + }, + } + } + + fn to_json(&self) -> String { + serde_json::to_string(self).unwrap_or_default() + } +} + +#[derive(Debug, Serialize)] +struct Api { + #[serde(rename = "baseURL")] + base_url: String, +} + +#[derive(Debug, Serialize)] +struct S3 { + endpoint: String, + region: String, +} + +#[derive(Debug, Serialize)] +struct Release { + version: String, + date: String, +} + +#[derive(Debug, Serialize)] +struct License { + name: String, + url: String, } async fn config_handler(axum::extract::Extension(fs_addr): axum::extract::Extension) -> impl IntoResponse { - let cfg = serde_json::to_string(&Config { fs_addr }).unwrap_or_default(); + let cfg = Config::new(&fs_addr, "v0.0.1", "2025-01-01").to_json(); Response::builder() .header("content-type", "application/json") @@ -42,14 +108,37 @@ async fn config_handler(axum::extract::Extension(fs_addr): axum::extract::Extens } pub async fn start_static_file_server(addrs: &str, fs_addr: &str) { + // 将字符串解析为 SocketAddr + // let socket_addr: SocketAddr = fs_addr.parse().unwrap(); + + // // 提取 IP 地址和端口号 + // let mut src_ip = socket_addr.ip(); + // let port = socket_addr.port(); + + // if src_ip.to_string() == "0.0.0.0" { + // for iface in interfaces() { + // if iface.is_loopback() || !iface.is_up() { + // continue; + // } + // for ip in iface.ips { + // if ip.is_ipv4() { + // src_ip = ip.ip(); + // } + // } + // } + // } + + // // FIXME: TODO: protocol from config + // let s3_url = format!("http://{}:{}", src_ip, port); + // 创建路由 let app = Router::new() - .route("/config.json", get(config_handler).layer(axum::extract::Extension(fs_addr.to_string()))) - .route("/*file", get(static_handler)); + .route("/config.json", get(config_handler).layer(axum::extract::Extension(fs_addr.to_owned()))) + .nest_service("/", get(static_handler)); let listener = tokio::net::TcpListener::bind(addrs).await.unwrap(); - println!("console listening on: {}", listener.local_addr().unwrap()); + println!("console running on: http://{} with s3 api {}", listener.local_addr().unwrap(), fs_addr); axum::serve(listener, app).await.unwrap(); } diff --git a/rustfs/src/main.rs b/rustfs/src/main.rs index fd5d4079..d2d5ef50 100644 --- a/rustfs/src/main.rs +++ b/rustfs/src/main.rs @@ -234,7 +234,7 @@ async fn run(opt: config::Opt) -> Result<()> { if opt.console_enable { info!("console is enabled"); tokio::spawn(async move { - console::start_static_file_server(&opt.console_address, &opt.address).await; + console::start_static_file_server(&opt.console_address, &opt.server_endpoint).await; }); } diff --git a/rustfs/static/index.html b/rustfs/static/index.html deleted file mode 100644 index 612c5bbb..00000000 --- a/rustfs/static/index.html +++ /dev/null @@ -1 +0,0 @@ -static index \ No newline at end of file diff --git a/rustfs/static/readme.md b/rustfs/static/readme.md new file mode 100644 index 00000000..325303e1 --- /dev/null +++ b/rustfs/static/readme.md @@ -0,0 +1 @@ +console static path, do not delete \ No newline at end of file diff --git a/scripts/run.sh b/scripts/run.sh index 4aa012d7..7351b936 100755 --- a/scripts/run.sh +++ b/scripts/run.sh @@ -7,13 +7,13 @@ fi current_dir=$(pwd) mkdir -p ./target/volume/test -mkdir -p ./target/volume/test{0..4} +# mkdir -p ./target/volume/test{0..4} -if [ -z "$RUST_LOG" ]; then -export RUST_BACKTRACE=1 - export RUST_LOG="rustfs=debug,ecstore=debug,s3s=debug,iam=debug" -fi +# if [ -z "$RUST_LOG" ]; then +# export RUST_BACKTRACE=1 +# export RUST_LOG="rustfs=debug,ecstore=debug,s3s=debug,iam=debug" +# fi # export RUSTFS_ERASURE_SET_DRIVE_COUNT=5 @@ -24,10 +24,11 @@ export RUSTFS_VOLUMES="./target/volume/test" export RUSTFS_ADDRESS="0.0.0.0:9000" export RUSTFS_CONSOLE_ENABLE=true export RUSTFS_CONSOLE_ADDRESS="0.0.0.0:9002" +export RUSTFS_SERVER_ENDPOINT="http://localhost:9000" if [ -n "$1" ]; then export RUSTFS_VOLUMES="$1" fi -./target/debug/rustfs +cargo run --bin rustfs \ No newline at end of file