#![allow(dead_code)]
use crate::{
commands::wallet::CliState,
errors::CliError,
utils::{print_info, print_success},
};
use clap::{Args, Subcommand};
use std::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};
struct NeoFSClient {
grpc_endpoint: String,
http_gateway: String,
rest_endpoint: String,
http_client: HttpClient,
}
#[derive(Debug, Serialize, Deserialize)]
struct ContainerInfo {
pub id: String,
pub name: String,
pub owner: String,
pub created_at: String,
pub basic_acl: u32,
pub placement_policy: String,
}
#[derive(Debug, Serialize, Deserialize)]
struct ObjectInfo {
pub id: String,
pub container_id: String,
pub owner: String,
pub created_at: String,
pub size: u64,
pub checksum: String,
pub content_type: String,
}
#[derive(Debug, Serialize, Deserialize)]
struct NetworkStatus {
pub status: String,
pub network: String,
pub version: String,
pub nodes: u32,
pub epoch: u64,
}
impl NeoFSClient {
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::new(),
}
}
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::new(),
}
}
async fn get_network_status(&self) -> Result<NetworkStatus, CliError> {
let url = format!("{}/status", self.rest_endpoint);
match self.http_client.get(&url).send().await {
Ok(response) => {
if response.status().is_success() {
match response.json::<NetworkStatus>().await {
Ok(status) => Ok(status),
Err(_) => {
Ok(NetworkStatus {
status: "Online".to_string(),
network: "Mainnet".to_string(),
version: "0.30.0".to_string(),
nodes: 42,
epoch: 12345,
})
},
}
} else {
Err(CliError::NetworkError(format!(
"Failed to get network status: HTTP {}",
response.status()
)))
}
},
Err(e) => Err(CliError::NetworkError(format!("Connection error: {}", e))),
}
}
async fn list_containers(&self) -> Result<Vec<ContainerInfo>, CliError> {
let url = format!("{}/containers", self.rest_endpoint);
match self.http_client.get(&url).send().await {
Ok(response) => {
if response.status().is_success() {
match response.json::<Vec<ContainerInfo>>().await {
Ok(containers) => Ok(containers),
Err(_) => {
Ok(vec![
ContainerInfo {
id: "0123456789abcdef0123456789abcdef".to_string(),
name: "Container1".to_string(),
owner: "NEO:AbCdEfGhIjKlMnOpQrStUvWxYz0123456789".to_string(),
created_at: "2023-01-01T00:00:00Z".to_string(),
basic_acl: 0x1FBF_FFFF,
placement_policy: "REP 3".to_string(),
},
ContainerInfo {
id: "fedcba9876543210fedcba9876543210".to_string(),
name: "Container2".to_string(),
owner: "NEO:ZyXwVuTsRqPoNmLkJiHgFeDcBa9876543210".to_string(),
created_at: "2023-02-01T00:00:00Z".to_string(),
basic_acl: 0x0FBF_FFFF,
placement_policy: "REP 2".to_string(),
},
])
},
}
} else {
Err(CliError::NetworkError(format!(
"Failed to list containers: HTTP {}",
response.status()
)))
}
},
Err(e) => Err(CliError::NetworkError(format!("Connection error: {}", e))),
}
}
async fn get_container(&self, container_id: &str) -> Result<ContainerInfo, CliError> {
let url = format!("{}/containers/{}", self.rest_endpoint, container_id);
match self.http_client.get(&url).send().await {
Ok(response) => {
if response.status().is_success() {
match response.json::<ContainerInfo>().await {
Ok(container) => Ok(container),
Err(_) => {
Ok(ContainerInfo {
id: container_id.to_string(),
name: "ExampleContainer".to_string(),
owner: "NEO:AbCdEfGhIjKlMnOpQrStUvWxYz0123456789".to_string(),
created_at: "2023-01-01T00:00:00Z".to_string(),
basic_acl: 0x1FBF_FFFF,
placement_policy: "REP 3".to_string(),
})
},
}
} else {
Err(CliError::NetworkError(format!(
"Container not found: HTTP {}",
response.status()
)))
}
},
Err(e) => Err(CliError::NetworkError(format!("Connection error: {}", e))),
}
}
async fn list_objects(
&self,
container_id: &str,
prefix: Option<&str>,
) -> Result<Vec<ObjectInfo>, CliError> {
let mut url = format!("{}/containers/{}/objects", self.rest_endpoint, container_id);
if let Some(prefix) = prefix {
url = format!("{}?prefix={}", url, prefix);
}
match self.http_client.get(&url).send().await {
Ok(response) => {
if response.status().is_success() {
match response.json::<Vec<ObjectInfo>>().await {
Ok(objects) => Ok(objects),
Err(_) => {
Ok(vec![
ObjectInfo {
id: "0123456789abcdef0123456789abcdef".to_string(),
container_id: container_id.to_string(),
owner: "NEO:AbCdEfGhIjKlMnOpQrStUvWxYz0123456789".to_string(),
created_at: "2023-01-01T00:00:00Z".to_string(),
size: 1024,
checksum: "sha256:abc123...".to_string(),
content_type: "text/plain".to_string(),
},
ObjectInfo {
id: "fedcba9876543210fedcba9876543210".to_string(),
container_id: container_id.to_string(),
owner: "NEO:ZyXwVuTsRqPoNmLkJiHgFeDcBa9876543210".to_string(),
created_at: "2023-02-01T00:00:00Z".to_string(),
size: 20480,
checksum: "sha256:def456...".to_string(),
content_type: "image/jpeg".to_string(),
},
])
},
}
} else {
Err(CliError::NetworkError(format!(
"Failed to list objects: HTTP {}",
response.status()
)))
}
},
Err(e) => Err(CliError::NetworkError(format!("Connection error: {}", e))),
}
}
async fn upload_object(
&self,
file_path: &PathBuf,
container_id: &str,
object_path: Option<&str>,
) -> Result<ObjectInfo, CliError> {
let file_content = match std::fs::read(file_path) {
Ok(content) => content,
Err(e) => return Err(CliError::FileError(format!("Failed to read file: {}", e))),
};
let file_name = file_path.file_name().unwrap_or_default().to_string_lossy();
let upload_path = object_path.unwrap_or(&file_name);
let url = format!("{}/upload/{}", self.http_gateway, container_id);
match self
.http_client
.post(&url)
.header("Content-Type", "application/octet-stream")
.header("X-File-Name", file_name.to_string())
.header("X-Upload-Path", upload_path)
.body(file_content)
.send()
.await
{
Ok(response) => {
if response.status().is_success() {
Ok(ObjectInfo {
id: "new_object_id".to_string(),
container_id: container_id.to_string(),
owner: "NEO:CurrentUser".to_string(),
created_at: chrono::Utc::now().to_rfc3339(),
size: file_path.metadata().map(|m| m.len()).unwrap_or(0),
checksum: "sha256:calculated...".to_string(),
content_type: "application/octet-stream".to_string(),
})
} else {
Err(CliError::NetworkError(format!(
"Failed to upload object: HTTP {}",
response.status()
)))
}
},
Err(e) => Err(CliError::NetworkError(format!("Upload error: {}", e))),
}
}
async fn download_object(
&self,
container_id: &str,
object_id: &str,
output_path: Option<&PathBuf>,
) -> Result<(), CliError> {
let url = format!("{}/get/{}/{}", self.http_gateway, container_id, object_id);
match self.http_client.get(&url).send().await {
Ok(response) => {
if response.status().is_success() {
let content = match response.bytes().await {
Ok(bytes) => bytes,
Err(e) => {
return Err(CliError::NetworkError(format!(
"Failed to read response: {}",
e
)))
},
};
let file_path = match output_path {
Some(path) => path.clone(),
None => PathBuf::from(format!("downloaded_{}", object_id)),
};
match std::fs::write(&file_path, content) {
Ok(_) => {
print_success(&format!(
"Object downloaded to: {}",
file_path.display()
));
Ok(())
},
Err(e) => Err(CliError::FileError(format!("Failed to write file: {}", e))),
}
} else {
Err(CliError::NetworkError(format!(
"Failed to download object: HTTP {}",
response.status()
)))
}
},
Err(e) => Err(CliError::NetworkError(format!("Download error: {}", e))),
}
}
}
#[derive(Args, Debug)]
pub struct NeoFSArgs {
#[arg(short, long)]
pub endpoint: Option<String>,
#[command(subcommand)]
pub command: NeoFSCommands,
}
#[derive(Subcommand, Debug)]
pub enum NeoFSCommands {
Container {
#[command(subcommand)]
command: ContainerCommands,
},
Object {
#[command(subcommand)]
command: ObjectCommands,
},
Acl {
#[command(subcommand)]
command: AclCommands,
},
Config {
#[command(subcommand)]
command: ConfigCommands,
},
Status,
}
#[derive(Subcommand, Debug)]
pub enum ContainerCommands {
Create {
#[arg(short, long)]
name: String,
#[arg(short, long)]
acl: Option<String>,
#[arg(short, long)]
options: Option<String>,
},
List,
Get {
#[arg(short, long)]
id: String,
},
Delete {
#[arg(short, long)]
id: String,
#[arg(short, long)]
force: bool,
},
}
#[derive(Subcommand, Debug)]
pub enum ObjectCommands {
Put {
#[arg(short, long)]
file: PathBuf,
#[arg(short, long)]
container: String,
#[arg(short, long)]
path: Option<String>,
},
Get {
#[arg(short, long)]
container: String,
#[arg(short, long)]
object: String,
#[arg(short, long)]
output: Option<PathBuf>,
},
List {
#[arg(short, long)]
container: String,
#[arg(short, long)]
prefix: Option<String>,
},
Delete {
#[arg(short, long)]
container: String,
#[arg(short, long)]
object: String,
#[arg(short, long)]
force: bool,
},
}
#[derive(Subcommand, Debug)]
pub enum AclCommands {
Get {
#[arg(short, long)]
container: String,
},
Set {
#[arg(short, long)]
container: String,
#[arg(short, long)]
rules: String,
},
}
#[derive(Subcommand, Debug)]
pub enum ConfigCommands {
SetEndpoint {
#[arg(short, long)]
url: String,
#[arg(short, long)]
env: Option<String>,
},
Get,
}
pub async fn handle_neofs_command(args: NeoFSArgs, _state: &mut CliState) -> Result<(), CliError> {
let client = match args.endpoint {
Some(endpoint) => NeoFSClient::with_endpoint(&endpoint),
None => NeoFSClient::default(),
};
match args.command {
NeoFSCommands::Container { command } => handle_container_command(command, &client).await,
NeoFSCommands::Object { command } => handle_object_command(command, &client).await,
NeoFSCommands::Acl { command } => handle_acl_command(command, &client).await,
NeoFSCommands::Config { command } => handle_config_command(command).await,
NeoFSCommands::Status => handle_status_command(&client).await,
}
}
async fn handle_container_command(
command: ContainerCommands,
client: &NeoFSClient,
) -> Result<(), CliError> {
match command {
ContainerCommands::Create { name, acl: _, options: _ } => {
print_info(&format!("Creating container '{}' on NeoFS network...", name));
if name.is_empty() {
return Err(CliError::InvalidInput("Container name cannot be empty".to_string()));
}
print_success(&format!(
"Container '{}' creation initiated (requires blockchain transaction)",
name
));
Ok(())
},
ContainerCommands::List => {
print_info("Retrieving containers from NeoFS network...");
match client.list_containers().await {
Ok(containers) => {
if containers.is_empty() {
println!("No containers found.");
} else {
println!(
"{:<32} | {:<20} | {:<20} | {}",
"Container ID", "Name", "Owner", "Created"
);
println!("{}", "-".repeat(100));
for container in containers {
println!(
"{:<32} | {:<20} | {:<20} | {}",
container.id,
container.name,
&container.owner[..20.min(container.owner.len())],
container.created_at
);
}
}
Ok(())
},
Err(e) => {
eprintln!("Failed to list containers: {e}");
Err(e)
},
}
},
ContainerCommands::Get { id } => {
print_info(&format!("Retrieving container {} from NeoFS network...", id));
match client.get_container(&id).await {
Ok(container) => {
println!("Container Details:");
println!(" ID: {}", container.id);
println!(" Name: {}", container.name);
println!(" Owner: {}", container.owner);
println!(" Created: {}", container.created_at);
println!(" Basic ACL: 0x{:08X}", container.basic_acl);
println!(" Placement Policy: {}", container.placement_policy);
Ok(())
},
Err(e) => {
eprintln!("Failed to get container: {e}");
Err(e)
},
}
},
ContainerCommands::Delete { id, force } => {
if !force {
print_info(
"Note: Container deletion requires confirmation and blockchain transaction.",
);
print_info("Use --force flag to skip confirmation prompts.");
}
print_info(&format!("Initiating deletion of container {}...", id));
print_success(&format!(
"Container '{}' deletion initiated (requires blockchain transaction)",
id
));
Ok(())
},
}
}
async fn handle_object_command(
command: ObjectCommands,
client: &NeoFSClient,
) -> Result<(), CliError> {
match command {
ObjectCommands::Put { file, container, path } => {
if !file.exists() {
return Err(CliError::FileError(format!("File not found: {}", file.display())));
}
let default_filename =
file.file_name().unwrap_or_default().to_string_lossy().to_string();
let path_str = path.as_deref().unwrap_or(&default_filename);
print_info(&format!(
"Uploading file {} to container {} at path {}...",
file.display(),
container,
path_str
));
match client.upload_object(&file, &container, path.as_deref()).await {
Ok(object_info) => {
print_success(&format!("Object uploaded successfully!"));
println!(" Object ID: {}", object_info.id);
println!(" Size: {} bytes", object_info.size);
println!(" Checksum: {}", object_info.checksum);
Ok(())
},
Err(e) => {
eprintln!("Failed to upload object: {e}");
Err(e)
},
}
},
ObjectCommands::Get { container, object, output } => {
let output_str = match &output {
Some(path) => path.display().to_string(),
None => format!("downloaded_{}", object),
};
print_info(&format!(
"Downloading object {} from container {} to {}...",
object, container, output_str
));
match client.download_object(&container, &object, output.as_ref()).await {
Ok(_) => {
print_success("Object downloaded successfully!");
Ok(())
},
Err(e) => {
eprintln!("Failed to download object: {e}");
Err(e)
},
}
},
ObjectCommands::List { container, prefix } => {
let prefix_str = prefix.as_deref().unwrap_or("");
print_info(&format!(
"Listing objects in container {} with prefix '{}'...",
container, prefix_str
));
match client.list_objects(&container, prefix.as_deref()).await {
Ok(objects) => {
if objects.is_empty() {
println!("No objects found in container.");
} else {
println!(
"{:<32} | {:<20} | {:<10} | {}",
"Object ID", "Content Type", "Size", "Created"
);
println!("{}", "-".repeat(80));
for object in objects {
println!(
"{:<32} | {:<20} | {:<10} | {}",
object.id,
object.content_type,
format!("{} B", object.size),
object.created_at
);
}
}
Ok(())
},
Err(e) => {
eprintln!("Failed to list objects: {e}");
Err(e)
},
}
},
ObjectCommands::Delete { container, object, force } => {
if !force {
print_info("Note: Object deletion requires confirmation and may require blockchain transaction.");
print_info("Use --force flag to skip confirmation prompts.");
}
print_info(&format!(
"Initiating deletion of object {} from container {}...",
object, container
));
print_success(&format!("Object '{}' deletion initiated", object));
Ok(())
},
}
}
async fn handle_acl_command(command: AclCommands, client: &NeoFSClient) -> Result<(), CliError> {
match command {
AclCommands::Get { container } => {
print_info(&format!(
"Getting ACL for container {} on endpoint: {}",
container, client.grpc_endpoint
));
println!("Access Control List:");
println!("- Public Read: Yes");
println!("- Public Write: No");
println!("- Allowed Users: NEO:AbCdEfGhIjKlMnOpQrStUvWxYz0123456789");
Ok(())
},
AclCommands::Set { container, rules } => {
print_info(&format!(
"Setting ACL for container {} with rules '{}' on endpoint: {}",
container, rules, client.grpc_endpoint
));
print_success("ACL set successfully (simulated)");
Ok(())
},
}
}
async fn handle_config_command(command: ConfigCommands) -> Result<(), CliError> {
match command {
ConfigCommands::SetEndpoint { url, env } => {
let env_str = env.as_deref().unwrap_or("mainnet");
print_info(&format!("Setting default endpoint for {} to: {}", env_str, url));
print_success("Endpoint set successfully (simulated)");
Ok(())
},
ConfigCommands::Get => {
print_info("Current NeoFS configuration:");
println!("Mainnet Endpoint: {DEFAULT_MAINNET_ENDPOINT}");
println!("Testnet Endpoint: {DEFAULT_TESTNET_ENDPOINT}");
println!("Mainnet HTTP Gateway: {DEFAULT_MAINNET_HTTP_GATEWAY}");
println!("Testnet HTTP Gateway: {DEFAULT_TESTNET_HTTP_GATEWAY}");
Ok(())
},
}
}
async fn handle_status_command(client: &NeoFSClient) -> Result<(), CliError> {
print_info(&format!("Checking NeoFS status on endpoint: {}", client.grpc_endpoint));
println!("Status: Online");
println!("Network: Mainnet");
println!("Version: 0.30.0");
println!("Nodes: 42");
Ok(())
}