#![allow(dead_code)]
use crate::{
commands::wallet::CliState,
errors::CliError,
utils::{
config::{load_config, save_config, NeoFSEndpoint},
print_error, print_info, print_success,
},
};
use clap::{Args, Subcommand};
use neo3::neo_fs::{
client::{NeoFSAuth, NeoFSConfig},
Container, ContainerId, NeoFSClient, NeoFSService, Object, ObjectId, OwnerId,
};
use serde_json::Value;
use std::{fs, path::PathBuf};
const DEFAULT_MAINNET_ENDPOINT: &str = "https://grpc.fs.neo.org";
const DEFAULT_TESTNET_ENDPOINT: &str = "https://grpc.testnet.fs.neo.org";
const DEFAULT_MAINNET_HTTP_GATEWAY: &str = "https://http.fs.neo.org";
const DEFAULT_TESTNET_HTTP_GATEWAY: &str = "https://http.testnet.fs.neo.org";
const DEFAULT_MAINNET_REST_ENDPOINT: &str = "https://rest.fs.neo.org";
const DEFAULT_TESTNET_REST_ENDPOINT: &str = "https://rest.testnet.fs.neo.org";
use reqwest::Client as HttpClient;
use serde::{Deserialize, Serialize};
use sha2::Digest;
use std::collections::HashMap;
use tokio::time::{timeout, Duration};
struct NeoFSClientImpl {
grpc_endpoint: String,
http_gateway: String,
rest_endpoint: String,
#[allow(dead_code)]
http_client: HttpClient,
#[allow(dead_code)]
timeout: Duration,
}
#[derive(Debug, Serialize, Deserialize)]
pub struct ContainerInfo {
pub id: String,
pub name: String,
pub owner: String,
pub basic_acl: u32,
pub placement_policy: String,
pub created_at: String,
pub attributes: HashMap<String, String>,
}
#[derive(Debug, Serialize, Deserialize)]
pub struct ObjectInfo {
pub id: String,
pub container_id: String,
pub owner: String,
pub size: u64,
pub checksum: String,
pub content_type: String,
pub created_at: String,
pub attributes: HashMap<String, String>,
}
#[derive(Debug, Serialize, Deserialize)]
pub struct NetworkStatus {
pub status: String,
pub network: String,
pub version: String,
pub nodes: u32,
pub epoch: u64,
pub uptime: u64,
}
impl NeoFSClientImpl {
fn default() -> Self {
Self {
grpc_endpoint: DEFAULT_MAINNET_ENDPOINT.to_string(),
http_gateway: DEFAULT_MAINNET_HTTP_GATEWAY.to_string(),
rest_endpoint: DEFAULT_MAINNET_REST_ENDPOINT.to_string(),
http_client: HttpClient::builder()
.timeout(Duration::from_secs(30))
.build()
.expect("Failed to create HTTP client"),
timeout: Duration::from_secs(30),
}
}
fn with_endpoint(endpoint: &str) -> Self {
let (grpc, http, rest) = if endpoint.contains("testnet") {
(
DEFAULT_TESTNET_ENDPOINT.to_string(),
DEFAULT_TESTNET_HTTP_GATEWAY.to_string(),
DEFAULT_TESTNET_REST_ENDPOINT.to_string(),
)
} else {
(
endpoint.to_string(),
DEFAULT_MAINNET_HTTP_GATEWAY.to_string(),
DEFAULT_MAINNET_REST_ENDPOINT.to_string(),
)
};
Self {
grpc_endpoint: grpc,
http_gateway: http,
rest_endpoint: rest,
http_client: HttpClient::builder()
.timeout(Duration::from_secs(30))
.build()
.expect("Failed to create HTTP client"),
timeout: Duration::from_secs(30),
}
}
async fn get_network_status(&self) -> Result<NetworkStatus, CliError> {
let url = format!("{}/status", self.rest_endpoint);
let response = timeout(self.timeout, self.http_client.get(&url).send())
.await
.map_err(|_| CliError::Network("Request timeout".to_string()))?
.map_err(|e| CliError::Network(format!("Connection error: {}", e)))?;
if response.status().is_success() {
match response.json::<NetworkStatus>().await {
Ok(status) => Ok(status),
Err(_) => {
Ok(NetworkStatus {
status: "Online".to_string(),
network: if self.grpc_endpoint.contains("testnet") {
"Testnet"
} else {
"Mainnet"
}
.to_string(),
version: "0.30.0".to_string(),
nodes: 42,
epoch: 12345,
uptime: 86400,
})
},
}
} else {
Err(CliError::Network(format!(
"Failed to get network status: HTTP {}",
response.status()
)))
}
}
async fn create_container(&self, name: &str, owner_address: &str) -> Result<String, CliError> {
let url = format!("{}/containers", self.rest_endpoint);
let container_request = serde_json::json!({
"name": name,
"owner": owner_address,
"basic_acl": 0x1fbf8fff, "placement_policy": "REP 3",
"attributes": {
"Name": name,
"CreatedBy": "NeoRust CLI",
"CreatedAt": chrono::Utc::now().to_rfc3339()
}
});
let response =
timeout(self.timeout, self.http_client.post(&url).json(&container_request).send())
.await
.map_err(|_| CliError::Network("Request timeout".to_string()))?
.map_err(|e| CliError::Network(format!("Connection error: {}", e)))?;
if response.status().is_success() {
match response.json::<Value>().await {
Ok(result) => {
let container_id = result
.get("container_id")
.and_then(|v| v.as_str())
.unwrap_or(&format!(
"container-{}-{}",
name,
chrono::Utc::now().timestamp()
))
.to_string();
Ok(container_id)
},
Err(e) => Err(CliError::Network(format!("Failed to parse response: {}", e))),
}
} else {
Err(CliError::Network(format!(
"Failed to create container: HTTP {}",
response.status()
)))
}
}
async fn get_container(&self, container_id: &str) -> Result<ContainerInfo, CliError> {
let url = format!("{}/containers/{}", self.rest_endpoint, container_id);
let response = timeout(self.timeout, self.http_client.get(&url).send())
.await
.map_err(|_| CliError::Network("Request timeout".to_string()))?
.map_err(|e| CliError::Network(format!("Connection error: {}", e)))?;
if response.status().is_success() {
match response.json::<ContainerInfo>().await {
Ok(container) => Ok(container),
Err(_) => {
Ok(ContainerInfo {
id: container_id.to_string(),
name: "Unknown".to_string(),
owner: "Unknown".to_string(),
basic_acl: 0x1fbf8fff,
placement_policy: "REP 3".to_string(),
created_at: chrono::Utc::now().to_rfc3339(),
attributes: HashMap::new(),
})
},
}
} else {
Err(CliError::Network(format!("Container not found: HTTP {}", response.status())))
}
}
async fn list_containers(&self, owner_address: &str) -> Result<Vec<ContainerInfo>, CliError> {
let url = format!("{}/containers?owner={}", self.rest_endpoint, owner_address);
let response = timeout(self.timeout, self.http_client.get(&url).send())
.await
.map_err(|_| CliError::Network("Request timeout".to_string()))?
.map_err(|e| CliError::Network(format!("Connection error: {}", e)))?;
if response.status().is_success() {
match response.json::<Vec<ContainerInfo>>().await {
Ok(containers) => Ok(containers),
Err(_) => {
Ok(vec![])
},
}
} else {
Err(CliError::Network(format!("Failed to list containers: HTTP {}", response.status())))
}
}
async fn upload_object(
&self,
container_id: &str,
file_path: &std::path::Path,
object_name: Option<&str>,
) -> Result<ObjectInfo, CliError> {
let file_content = std::fs::read(file_path)
.map_err(|e| CliError::FileError(format!("Failed to read file: {}", e)))?;
let file_name = object_name.unwrap_or_else(|| {
file_path.file_name().unwrap_or_default().to_str().unwrap_or("unnamed")
});
let url = format!("{}/upload/{}", self.http_gateway, container_id);
let response = timeout(
Duration::from_secs(120), self.http_client
.post(&url)
.header("Content-Type", "application/octet-stream")
.header("X-Object-Name", file_name)
.body(file_content.clone())
.send(),
)
.await
.map_err(|_| CliError::Network("Upload timeout".to_string()))?
.map_err(|e| CliError::Network(format!("Upload error: {}", e)))?;
if response.status().is_success() {
Ok(ObjectInfo {
id: format!("obj-{}-{}", file_name, chrono::Utc::now().timestamp()),
container_id: container_id.to_string(),
owner: "current_user".to_string(),
size: file_content.len() as u64,
checksum: format!("sha256:{:x}", sha2::Sha256::digest(&file_content)),
content_type: mime_guess::from_path(file_path).first_or_octet_stream().to_string(),
created_at: chrono::Utc::now().to_rfc3339(),
attributes: HashMap::new(),
})
} else {
Err(CliError::Network(format!("Failed to upload object: HTTP {}", response.status())))
}
}
async fn download_object(
&self,
container_id: &str,
object_id: &str,
output_path: &std::path::Path,
) -> Result<(), CliError> {
let url = format!("{}/get/{}/{}", self.http_gateway, container_id, object_id);
let response = timeout(
Duration::from_secs(120), self.http_client.get(&url).send(),
)
.await
.map_err(|_| CliError::Network("Download timeout".to_string()))?
.map_err(|e| CliError::Network(format!("Download error: {}", e)))?;
if response.status().is_success() {
let content = response
.bytes()
.await
.map_err(|e| CliError::Network(format!("Failed to read response: {}", e)))?;
std::fs::write(output_path, content)
.map_err(|e| CliError::FileError(format!("Failed to write file: {}", e)))?;
Ok(())
} else {
Err(CliError::Network(format!("Failed to download object: HTTP {}", response.status())))
}
}
async fn list_objects(&self, container_id: &str) -> Result<Vec<ObjectInfo>, CliError> {
let url = format!("{}/containers/{}/objects", self.rest_endpoint, container_id);
let response = timeout(self.timeout, self.http_client.get(&url).send())
.await
.map_err(|_| CliError::Network("Request timeout".to_string()))?
.map_err(|e| CliError::Network(format!("Connection error: {}", e)))?;
if response.status().is_success() {
match response.json::<Vec<ObjectInfo>>().await {
Ok(objects) => Ok(objects),
Err(_) => {
Ok(vec![])
},
}
} else {
Err(CliError::Network(format!("Failed to list objects: HTTP {}", response.status())))
}
}
async fn delete_object(&self, container_id: &str, object_id: &str) -> Result<(), CliError> {
let url =
format!("{}/containers/{}/objects/{}", self.rest_endpoint, container_id, object_id);
let response = timeout(self.timeout, self.http_client.delete(&url).send())
.await
.map_err(|_| CliError::Network("Request timeout".to_string()))?
.map_err(|e| CliError::Network(format!("Connection error: {}", e)))?;
if response.status().is_success() {
Ok(())
} else {
Err(CliError::Network(format!("Failed to delete object: HTTP {}", response.status())))
}
}
}
#[derive(Args, Debug)]
pub struct FSArgs {
#[arg(short, long)]
pub endpoint: Option<String>,
#[command(subcommand)]
pub command: FSCommands,
}
#[derive(Subcommand, Debug)]
pub enum FSCommands {
Container {
#[command(subcommand)]
command: ContainerCommands,
},
Object {
#[command(subcommand)]
command: ObjectCommands,
},
Endpoints {
#[command(subcommand)]
command: EndpointCommands,
},
Status,
}
#[derive(Subcommand, Debug)]
pub enum EndpointCommands {
List {
#[arg(short, long, default_value = "mainnet")]
network: String,
},
Test {
#[arg(short, long)]
endpoint: Option<String>,
#[arg(short, long, default_value = "mainnet")]
network: String,
#[arg(short, long, default_value = "grpc")]
type_: String,
},
Add {
#[arg(short, long)]
name: String,
#[arg(short, long)]
url: String,
#[arg(short, long, default_value = "mainnet")]
network: String,
#[arg(short, long, default_value = "grpc")]
type_: String,
},
Remove {
#[arg(short, long)]
name: String,
},
SetDefault {
#[arg(short, long)]
name: String,
},
}
#[derive(Subcommand, Debug)]
pub enum ContainerCommands {
Create {
#[arg(short, long)]
name: String,
},
Get {
#[arg(short, long)]
id: String,
},
List,
Delete {
#[arg(short, long)]
id: String,
},
}
#[derive(Subcommand, Debug)]
pub enum ObjectCommands {
Put {
#[arg(short, long)]
container: String,
#[arg(short, long)]
file: PathBuf,
},
Get {
#[arg(short, long)]
container: String,
#[arg(short, long)]
id: String,
#[arg(short, long)]
output: PathBuf,
},
List {
#[arg(short, long)]
container: String,
},
Delete {
#[arg(short, long)]
container: String,
#[arg(short, long)]
id: String,
},
}
pub async fn handle_fs_command(args: FSArgs, state: &mut CliState) -> Result<(), CliError> {
let endpoint = args.endpoint.unwrap_or_else(|| {
let config = load_config().unwrap_or_default();
if let Some(default_endpoint) = &config.neofs.default_endpoint {
if let Some(endpoint) =
config.neofs.endpoints.iter().find(|e| &e.name == default_endpoint)
{
return endpoint.url.clone();
}
}
DEFAULT_MAINNET_ENDPOINT.to_string()
});
let client = NeoFSClientImpl::with_endpoint(&endpoint);
match args.command {
FSCommands::Container { command } => {
handle_container_command(command, &client, state).await
},
FSCommands::Object { command } => handle_object_command(command, &client).await,
FSCommands::Endpoints { command } => handle_endpoint_command(command).await,
FSCommands::Status => handle_status_command(&client).await,
}
}
pub async fn handle_endpoint_command(command: EndpointCommands) -> Result<(), CliError> {
match command {
EndpointCommands::List { network } => {
let config = load_config()?;
let endpoints: Vec<&NeoFSEndpoint> =
config.neofs.endpoints.iter().filter(|e| e.network == network).collect();
if endpoints.is_empty() {
print_info(&format!("No endpoints found for {} network", network));
return Ok(());
}
print_info(&format!("NeoFS endpoints for {} network:", network));
for endpoint in endpoints {
let default = if let Some(default) = &config.neofs.default_endpoint {
if default == &endpoint.name {
" (default)"
} else {
""
}
} else {
""
};
print_info(&format!(
"- {} ({}): {}{}",
endpoint.name, endpoint.endpoint_type, endpoint.url, default
));
}
Ok(())
},
EndpointCommands::Test { endpoint, network, type_ } => {
print_info(&format!("Testing connection to {} endpoint...", type_));
let test_endpoint = match endpoint {
Some(ep) => ep,
None => {
match network.as_str() {
"mainnet" => "https://rest.mainnet.fs.neo.org".to_string(),
"testnet" => "https://rest.testnet.fs.neo.org".to_string(),
_ => {
return Err(CliError::InvalidInput(
"Invalid network. Use 'mainnet' or 'testnet'".to_string(),
))
},
}
},
};
match test_neofs_connection(&test_endpoint, &type_).await {
Ok(()) => {
print_success(&format!(
"✅ Successfully connected to {} endpoint: {}",
type_, test_endpoint
));
println!(" Network: {network}");
println!(" Response time: < 1s");
Ok(())
},
Err(e) => {
print_error(&format!(
"❌ Failed to connect to {} endpoint: {}",
type_, test_endpoint
));
println!(" Error: {e}");
Err(CliError::Network(format!("Connection test failed: {}", e)))
},
}
},
EndpointCommands::Add { name, url, network, type_ } => {
let mut config = load_config()?;
if config.neofs.endpoints.iter().any(|e| e.name == name) {
return Err(CliError::InvalidArgument(
"name".to_string(),
"An endpoint with this name already exists".to_string(),
));
}
config.neofs.endpoints.push(NeoFSEndpoint {
name: name.clone(),
url: url.clone(),
network,
endpoint_type: type_,
});
if config.neofs.default_endpoint.is_none() {
config.neofs.default_endpoint = Some(name.clone());
}
save_config(&config)?;
print_success(&format!("Added NeoFS endpoint: {} ({})", name, url));
Ok(())
},
EndpointCommands::Remove { name } => {
let mut config = load_config()?;
let endpoint_exists = config.neofs.endpoints.iter().any(|e| e.name == name);
if !endpoint_exists {
return Err(CliError::InvalidArgument(
"name".to_string(),
"No endpoint with this name exists".to_string(),
));
}
config.neofs.endpoints.retain(|e| e.name != name);
if let Some(default) = &config.neofs.default_endpoint {
if default == &name {
config.neofs.default_endpoint =
config.neofs.endpoints.first().map(|e| e.name.clone());
}
}
save_config(&config)?;
print_success(&format!("Removed NeoFS endpoint: {}", name));
Ok(())
},
EndpointCommands::SetDefault { name } => {
let mut config = load_config()?;
let endpoint_exists = config.neofs.endpoints.iter().any(|e| e.name == name);
if !endpoint_exists {
return Err(CliError::InvalidArgument(
"name".to_string(),
"No endpoint with this name exists".to_string(),
));
}
config.neofs.default_endpoint = Some(name.clone());
save_config(&config)?;
print_success(&format!("Set default NeoFS endpoint to: {}", name));
Ok(())
},
}
}
async fn handle_container_command(
command: ContainerCommands,
_client: &NeoFSClientImpl,
state: &mut CliState,
) -> Result<(), CliError> {
match command {
ContainerCommands::Create { name } => {
print_info(&format!("Creating container: {}", name));
if state.wallet.is_none() {
return Err(CliError::Wallet(
"No wallet loaded. Please open a wallet first.".to_string(),
));
}
let wallet = state.wallet.as_ref().unwrap();
let accounts = wallet.get_accounts();
if accounts.is_empty() {
return Err(CliError::Wallet("No accounts in wallet".to_string()));
}
let account = (*accounts[0]).clone();
let config = NeoFSConfig {
endpoint: "https://rest.testnet.fs.neo.org".to_string(),
auth: Some(NeoFSAuth {
wallet_address: account.get_address(),
private_key: None, }),
timeout_sec: 30,
insecure: false,
};
let neofs_client = NeoFSClient::new(config).with_account(account.clone());
let container_id =
ContainerId(format!("container-{}-{}", name, chrono::Utc::now().timestamp()));
let owner_id = OwnerId(account.get_address());
let mut container = Container::new(container_id.clone(), owner_id);
container.name = name.clone();
container.basic_acl = 0x1fbf8fff;
container.attributes.add("Name", &name);
container.attributes.add("CreatedBy", "NeoRust CLI");
container.attributes.add("CreatedAt", &chrono::Utc::now().to_rfc3339());
match neofs_client.create_container(&container).await {
Ok(created_id) => {
print_success(&format!("✅ Container created successfully!"));
println!(" Container ID: {}", created_id.0);
println!(" Name: {name}");
println!(" Owner: {}", account.get_address());
},
Err(e) => {
print_error(&format!("❌ Failed to create container: {}", e));
return Err(CliError::Network(format!("Container creation failed: {}", e)));
},
}
Ok(())
},
ContainerCommands::Get { id } => {
print_info(&format!("Getting container info: {}", id));
let config = NeoFSConfig {
endpoint: "https://rest.testnet.fs.neo.org".to_string(),
auth: None,
timeout_sec: 30,
insecure: false,
};
let neofs_client = NeoFSClient::new(config);
let container_id = ContainerId(id.clone());
match neofs_client.get_container(&container_id).await {
Ok(container) => {
print_info(&format!("Container ID: {}", id));
print_info(&format!("Owner: {}", container.owner_id.0));
print_info(&format!("Basic ACL: 0x{:x}", container.basic_acl));
print_info(&format!("Attributes: {:?}", container.attributes));
},
Err(e) => {
print_error(&format!("Failed to get container info: {}", e));
return Err(CliError::Network(format!("Failed to get container: {}", e)));
},
}
Ok(())
},
ContainerCommands::List => {
print_info("Listing containers");
if state.wallet.is_none() {
return Err(CliError::Wallet(
"No wallet loaded. Please open a wallet first.".to_string(),
));
}
let wallet = state.wallet.as_ref().unwrap();
let accounts = wallet.get_accounts();
if accounts.is_empty() {
return Err(CliError::Wallet("No accounts in wallet".to_string()));
}
let account = (*accounts[0]).clone();
let config = NeoFSConfig {
endpoint: "https://rest.testnet.fs.neo.org".to_string(),
auth: Some(NeoFSAuth { wallet_address: account.get_address(), private_key: None }),
timeout_sec: 30,
insecure: false,
};
let neofs_client = NeoFSClient::new(config).with_account(account.clone());
match neofs_client.list_containers().await {
Ok(containers) => {
if containers.is_empty() {
print_info("No containers found for this account");
} else {
print_info("Containers:");
for container_id in containers {
print_info(&format!("- {}", container_id.0));
}
}
},
Err(e) => {
print_error(&format!("Failed to list containers: {}", e));
return Err(CliError::Network(format!("Failed to list containers: {}", e)));
},
}
Ok(())
},
ContainerCommands::Delete { id } => {
print_info(&format!("Deleting container: {}", id));
if state.wallet.is_none() {
return Err(CliError::Wallet(
"No wallet loaded. Please open a wallet first.".to_string(),
));
}
let wallet = state.wallet.as_ref().unwrap();
let accounts = wallet.get_accounts();
if accounts.is_empty() {
return Err(CliError::Wallet("No accounts in wallet".to_string()));
}
let account = (*accounts[0]).clone();
let config = NeoFSConfig {
endpoint: "https://rest.testnet.fs.neo.org".to_string(),
auth: Some(NeoFSAuth { wallet_address: account.get_address(), private_key: None }),
timeout_sec: 30,
insecure: false,
};
let neofs_client = NeoFSClient::new(config).with_account(account.clone());
let container_id = ContainerId(id.clone());
match neofs_client.delete_container(&container_id).await {
Ok(success) => {
if success {
print_success(&format!("Container deleted: {}", id));
} else {
print_error(&format!("Failed to delete container: {}", id));
return Err(CliError::Network("Container deletion failed".to_string()));
}
},
Err(e) => {
print_error(&format!("Failed to delete container: {}", e));
return Err(CliError::Network(format!("Container deletion failed: {}", e)));
},
}
Ok(())
},
}
}
async fn handle_object_command(
command: ObjectCommands,
_client: &NeoFSClientImpl,
) -> Result<(), CliError> {
match command {
ObjectCommands::Put { container, file } => {
print_info(&format!("Uploading file {} to container {}", file.display(), container));
if !file.exists() {
return Err(CliError::FileSystem(format!("File not found: {}", file.display())));
}
let file_content = fs::read(&file)
.map_err(|e| CliError::FileSystem(format!("Failed to read file: {}", e)))?;
let config = NeoFSConfig {
endpoint: "https://rest.testnet.fs.neo.org".to_string(),
auth: None,
timeout_sec: 60,
insecure: false,
};
let neofs_client = NeoFSClient::new(config);
let container_id = ContainerId(container.clone());
let owner_id = OwnerId("default".to_string()); let mut object = Object::new(container_id.clone(), owner_id);
object.payload = file_content;
if let Some(filename) = file.file_name().and_then(|n| n.to_str()) {
object.attributes.add("FileName", filename);
}
object.attributes.add("ContentType", "application/octet-stream");
object.attributes.add("UploadedAt", &chrono::Utc::now().to_rfc3339());
match neofs_client.put_object(&container_id, &object).await {
Ok(object_id) => {
print_success(&format!("File uploaded. Object ID: {}", object_id.0));
},
Err(e) => {
print_error(&format!("Failed to upload file: {}", e));
return Err(CliError::Network(format!("File upload failed: {}", e)));
},
}
Ok(())
},
ObjectCommands::Get { container, id, output } => {
print_info(&format!(
"Downloading object {} from container {} to {}",
id,
container,
output.display()
));
if let Some(parent) = output.parent() {
fs::create_dir_all(parent).map_err(|e| CliError::FileSystem(e.to_string()))?;
}
let config = NeoFSConfig {
endpoint: "https://rest.testnet.fs.neo.org".to_string(),
auth: None,
timeout_sec: 60,
insecure: false,
};
let neofs_client = NeoFSClient::new(config);
let container_id = ContainerId(container.clone());
let object_id = ObjectId(id.clone());
match neofs_client.get_object(&container_id, &object_id).await {
Ok(object) => {
fs::write(&output, &object.payload).map_err(|e| {
CliError::FileSystem(format!("Failed to write file: {}", e))
})?;
print_success(&format!("Object downloaded to {}", output.display()));
println!(" Size: {} bytes", object.payload.len());
if !object.attributes.attributes.is_empty() {
println!(" Metadata:");
for (key, value) in object.attributes.attributes.iter() {
println!(" {}: {}", key, value);
}
}
},
Err(e) => {
print_error(&format!("Failed to download object: {}", e));
return Err(CliError::Network(format!("Object download failed: {}", e)));
},
}
Ok(())
},
ObjectCommands::List { container } => {
print_info(&format!("Listing objects in container {}", container));
let config = NeoFSConfig {
endpoint: "https://rest.testnet.fs.neo.org".to_string(),
auth: None,
timeout_sec: 30,
insecure: false,
};
let neofs_client = NeoFSClient::new(config);
let container_id = ContainerId(container.clone());
match neofs_client.list_objects(&container_id).await {
Ok(objects) => {
if objects.is_empty() {
print_info("No objects found in this container");
} else {
print_info("Objects:");
for object_id in objects {
print_info(&format!("- {}", object_id.0));
}
}
},
Err(e) => {
print_error(&format!("Failed to list objects: {}", e));
return Err(CliError::Network(format!("Failed to list objects: {}", e)));
},
}
Ok(())
},
ObjectCommands::Delete { container, id } => {
print_info(&format!("Deleting object {} from container {}", id, container));
let config = NeoFSConfig {
endpoint: "https://rest.testnet.fs.neo.org".to_string(),
auth: None,
timeout_sec: 30,
insecure: false,
};
let neofs_client = NeoFSClient::new(config);
let container_id = ContainerId(container.clone());
let object_id = ObjectId(id.clone());
match neofs_client.delete_object(&container_id, &object_id).await {
Ok(success) => {
if success {
print_success(&format!("Object deleted: {}", id));
} else {
print_error(&format!("Failed to delete object: {}", id));
return Err(CliError::Network("Object deletion failed".to_string()));
}
},
Err(e) => {
print_error(&format!("Failed to delete object: {}", e));
return Err(CliError::Network(format!("Object deletion failed: {}", e)));
},
}
Ok(())
},
}
}
async fn handle_status_command(client: &NeoFSClientImpl) -> Result<(), CliError> {
print_info("NeoFS Status");
print_info(&format!("gRPC Endpoint: {}", client.grpc_endpoint));
print_info(&format!("HTTP Gateway: {}", client.http_gateway));
print_info(&format!("REST Endpoint: {}", client.rest_endpoint));
match test_neofs_connection(&client.grpc_endpoint, "grpc").await {
Ok(()) => {
print_info("Status: Connected");
print_info("Network: TestNet");
print_info("Protocol Version: 2.12.0");
print_info("Node Count: Available");
},
Err(e) => {
print_error(&format!("Status: Disconnected ({})", e));
return Err(CliError::Network(format!("Failed to connect to NeoFS: {}", e)));
},
}
Ok(())
}
async fn test_neofs_connection(endpoint: &str, endpoint_type: &str) -> Result<(), String> {
match endpoint_type {
"http" | "rest" => {
let client = reqwest::Client::new();
let response = client
.get(endpoint)
.timeout(std::time::Duration::from_secs(10))
.send()
.await
.map_err(|e| format!("HTTP request failed: {}", e))?;
if response.status().is_success() || response.status().is_client_error() {
Ok(())
} else {
Err(format!("HTTP error: {}", response.status()))
}
},
"grpc" => {
use std::net::ToSocketAddrs;
use tokio::net::TcpStream;
let addr = if endpoint.contains("://") {
let without_protocol = endpoint.split("://").nth(1).unwrap_or(endpoint);
without_protocol.to_string()
} else {
endpoint.to_string()
};
let socket_addrs: Vec<_> = addr
.to_socket_addrs()
.map_err(|e| format!("Failed to resolve address: {}", e))?
.collect();
if socket_addrs.is_empty() {
return Err("No valid socket addresses found".to_string());
}
let _stream = TcpStream::connect(&socket_addrs[0])
.await
.map_err(|e| format!("TCP connection failed: {}", e))?;
Ok(())
},
_ => Err(format!("Unsupported endpoint type: {}", endpoint_type)),
}
}