Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

21 changes: 20 additions & 1 deletion ant-cli/src/cli.rs
Original file line number Diff line number Diff line change
Expand Up @@ -4,9 +4,26 @@ use std::path::PathBuf;

use crate::commands::data::{ChunkAction, FileAction, WalletAction};
use crate::commands::node::NodeCommand;
use crate::commands::update::UpdateArgs;

fn long_version() -> &'static str {
concat!(
env!("CARGO_PKG_VERSION"),
"\n",
"Autonomi network client: file operations and node management for the Autonomi decentralised network\n",
"\n",
"Repository: https://github.com/WithAutonomi/ant-client\n",
"License: MIT or Apache-2.0",
)
}

#[derive(Parser)]
#[command(name = "ant", about = "Autonomi network client")]
#[command(
name = "ant",
version,
long_version = long_version(),
about = "Autonomi network client"
)]
pub struct Cli {
/// Output structured JSON instead of human-readable text
#[arg(long, global = true)]
Expand Down Expand Up @@ -67,4 +84,6 @@ pub enum Commands {
#[command(subcommand)]
action: ChunkAction,
},
/// Update the ant binary to the latest version
Update(UpdateArgs),
}
1 change: 1 addition & 0 deletions ant-cli/src/commands/mod.rs
Original file line number Diff line number Diff line change
@@ -1,2 +1,3 @@
pub mod data;
pub mod node;
pub mod update;
95 changes: 95 additions & 0 deletions ant-cli/src/commands/update.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,95 @@
use clap::Args;
use colored::Colorize;

use ant_core::node::binary::NoopProgress;
use ant_core::update;

/// Progress reporter that prints to the terminal.
struct CliUpdateProgress;

impl ant_core::node::binary::ProgressReporter for CliUpdateProgress {
fn report_started(&self, message: &str) {
eprintln!("{}", message.dimmed());
}

fn report_progress(&self, bytes: u64, total: u64) {
if total > 0 {
let pct = (bytes as f64 / total as f64 * 100.0) as u64;
eprint!("\r{}", format!(" Downloading... {pct}%").dimmed());
}
}

fn report_complete(&self, message: &str) {
eprintln!("\r{}", message.green());
}
}

#[derive(Args)]
pub struct UpdateArgs {
/// Force re-download even if already on the latest version.
#[arg(long)]
pub force: bool,
}

impl UpdateArgs {
pub async fn execute(self, json_output: bool) -> anyhow::Result<()> {
let current_version = env!("CARGO_PKG_VERSION");

if !json_output {
eprintln!("{}", format!("Current version: {current_version}").dimmed());
eprintln!("{}", "Checking for updates...".dimmed());
}

let mut check = update::check_for_update(current_version).await?;

if !check.update_available && self.force {
check.force()?;
}

if !check.update_available {
if json_output {
println!("{}", serde_json::to_string_pretty(&check)?);
} else {
println!(
"{}",
format!("Already up to date (v{}).", check.current_version).green()
);
}
return Ok(());
}

if !json_output {
eprintln!(
"{}",
format!(
"Update available: v{} -> v{}",
check.current_version, check.latest_version
)
.cyan()
);
}

let progress: Box<dyn ant_core::node::binary::ProgressReporter> = if json_output {
Box::new(NoopProgress)
} else {
Box::new(CliUpdateProgress)
};

let result = update::perform_update(&check, progress.as_ref()).await?;

if json_output {
println!("{}", serde_json::to_string_pretty(&result)?);
} else {
println!(
"{}",
format!(
"Updated successfully: v{} -> v{}",
result.previous_version, result.new_version
)
.green()
);
}

Ok(())
}
}
3 changes: 3 additions & 0 deletions ant-cli/src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -117,6 +117,9 @@ async fn run() -> anyhow::Result<()> {
let client = build_data_client(&data_ctx, needs_wallet).await?;
action.execute(&client).await?;
}
Commands::Update(args) => {
args.execute(json).await?;
}
}

Ok(())
Expand Down
4 changes: 3 additions & 1 deletion ant-core/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -11,10 +11,13 @@ flate2 = "1"
fs2 = "0.4"
futures-core = "0.3"
futures-util = "0.3"
self-replace = "1"
semver = "1"
serde = { version = "1", features = ["derive"] }
serde_json = "1"
reqwest = { version = "0.12", features = ["json", "stream"] }
tar = "0.4"
tempfile = "3"
toml = "0.8"
thiserror = "2"
tokio = { version = "1", features = ["full"] }
Expand Down Expand Up @@ -50,7 +53,6 @@ openssl = { version = "0.10", features = ["vendored"] }
windows-sys = { version = "0.61", features = ["Win32_Foundation", "Win32_System_Console", "Win32_System_Threading"] }

[dev-dependencies]
tempfile = "3"
serial_test = "3"
anyhow = "1"
alloy = { version = "1.6", features = ["node-bindings"] }
Expand Down
3 changes: 3 additions & 0 deletions ant-core/src/error.rs
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,9 @@ pub enum Error {
#[error("Could not determine home directory (HOME/USERPROFILE not set)")]
HomeDirNotFound,

#[error("Update failed: {0}")]
UpdateFailed(String),

#[error("Failed to parse bootstrap_peers.toml: {0}")]
BootstrapConfigParse(String),

Expand Down
1 change: 1 addition & 0 deletions ant-core/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2,3 +2,4 @@ pub mod config;
pub mod data;
pub mod error;
pub mod node;
pub mod update;
34 changes: 20 additions & 14 deletions ant-core/src/node/binary.rs
Original file line number Diff line number Diff line change
Expand Up @@ -180,10 +180,10 @@ async fn download_and_extract(

// Extract based on file extension
let binary_path = if url.ends_with(".zip") {
extract_zip(&bytes, install_dir)?
extract_zip(&bytes, install_dir, BINARY_NAME)?
} else {
// Assume .tar.gz
extract_tar_gz(&bytes, install_dir)?
extract_tar_gz(&bytes, install_dir, BINARY_NAME)?
};

// Determine the actual version from the binary
Expand Down Expand Up @@ -211,8 +211,11 @@ async fn download_and_extract(
Ok((cached_path, actual_version))
}

/// Extract a .tar.gz archive and return the path to the node binary.
fn extract_tar_gz(data: &[u8], install_dir: &Path) -> Result<PathBuf> {
/// Extract a .tar.gz archive and return the path to a named binary.
///
/// Searches the archive for an entry whose file name matches `binary_name`
/// and writes it to `install_dir/<binary_name>`.
pub fn extract_tar_gz(data: &[u8], install_dir: &Path, binary_name: &str) -> Result<PathBuf> {
let decoder = flate2::read::GzDecoder::new(data);
let mut archive = tar::Archive::new(decoder);

Expand Down Expand Up @@ -244,8 +247,8 @@ fn extract_tar_gz(data: &[u8], install_dir: &Path) -> Result<PathBuf> {
.and_then(|n| n.to_str())
.unwrap_or_default();

if file_name == BINARY_NAME {
let dest = install_dir.join(BINARY_NAME);
if file_name == binary_name {
let dest = install_dir.join(binary_name);
let mut file = std::fs::File::create(&dest)?;
std::io::copy(&mut entry, &mut file)?;

Expand All @@ -261,11 +264,14 @@ fn extract_tar_gz(data: &[u8], install_dir: &Path) -> Result<PathBuf> {
}

binary_path
.ok_or_else(|| Error::BinaryResolution(format!("'{BINARY_NAME}' not found in archive")))
.ok_or_else(|| Error::BinaryResolution(format!("'{binary_name}' not found in archive")))
}

/// Extract a .zip archive and return the path to the node binary.
fn extract_zip(data: &[u8], install_dir: &Path) -> Result<PathBuf> {
/// Extract a .zip archive and return the path to a named binary.
///
/// Searches the archive for an entry whose file name matches `binary_name`
/// (or `binary_name.exe` on Windows) and writes it to `install_dir/`.
pub fn extract_zip(data: &[u8], install_dir: &Path, binary_name: &str) -> Result<PathBuf> {
let cursor = std::io::Cursor::new(data);
let mut archive = zip::ZipArchive::new(cursor)
.map_err(|e| Error::BinaryResolution(format!("failed to open zip archive: {e}")))?;
Expand All @@ -282,7 +288,7 @@ fn extract_zip(data: &[u8], install_dir: &Path) -> Result<PathBuf> {
.and_then(|p| p.file_name().map(|n| n.to_string_lossy().to_string()))
.unwrap_or_default();

if file_name == BINARY_NAME || file_name == format!("{BINARY_NAME}.exe") {
if file_name == binary_name || file_name == format!("{binary_name}.exe") {
let dest = install_dir.join(&file_name);
let mut out = std::fs::File::create(&dest)?;
std::io::copy(&mut file, &mut out)?;
Expand All @@ -298,7 +304,7 @@ fn extract_zip(data: &[u8], install_dir: &Path) -> Result<PathBuf> {
}

binary_path
.ok_or_else(|| Error::BinaryResolution(format!("'{BINARY_NAME}' not found in archive")))
.ok_or_else(|| Error::BinaryResolution(format!("'{binary_name}' not found in archive")))
}

/// Extract the version string from a node binary by running `<binary> --version`.
Expand Down Expand Up @@ -419,7 +425,7 @@ mod tests {
std::io::Write::write_all(&mut encoder, &tar_data).unwrap();
let gz_data = encoder.finish().unwrap();

let result = extract_tar_gz(&gz_data, tmp.path());
let result = extract_tar_gz(&gz_data, tmp.path(), BINARY_NAME);
assert!(result.is_ok());
let path = result.unwrap();
assert!(path.exists());
Expand All @@ -436,7 +442,7 @@ mod tests {
std::io::Write::write_all(&mut encoder, &tar_data).unwrap();
let gz_data = encoder.finish().unwrap();

let result = extract_tar_gz(&gz_data, tmp.path());
let result = extract_tar_gz(&gz_data, tmp.path(), BINARY_NAME);
assert!(result.is_err());
}

Expand Down Expand Up @@ -468,7 +474,7 @@ mod tests {
std::io::Write::write_all(&mut encoder, &tar_data).unwrap();
let gz_data = encoder.finish().unwrap();

let result = extract_tar_gz(&gz_data, tmp.path());
let result = extract_tar_gz(&gz_data, tmp.path(), BINARY_NAME);
assert!(result.is_err());
let err = result.unwrap_err().to_string();
assert!(
Expand Down
Loading
Loading