1pub use crate::error::{Error, Result};
10
11pub mod error;
12
13use async_trait::async_trait;
14use lazy_static::lazy_static;
15use reqwest::Client;
16use semver::Version;
17use serde::{Deserialize, Serialize};
18use serde_json::Value;
19use std::collections::HashMap;
20use std::env::consts::{ARCH, OS};
21use std::fmt;
22use std::path::{Path, PathBuf};
23use tar::Archive;
24use tokio::fs::File;
25use tokio::io::AsyncWriteExt;
26use zip::ZipArchive;
27
28const ANTCTL_S3_BASE_URL: &str = "https://antctl.s3.eu-west-2.amazonaws.com";
29const ANTNODE_S3_BASE_URL: &str = "https://antnode.s3.eu-west-2.amazonaws.com";
30const ANTNODE_RPC_CLIENT_S3_BASE_URL: &str =
31 "https://antnode-rpc-client.s3.eu-west-2.amazonaws.com";
32const ANT_S3_BASE_URL: &str = "https://autonomi-cli.s3.eu-west-2.amazonaws.com";
33const GITHUB_API_URL: &str = "https://api.github.com";
34const NAT_DETECTION_S3_BASE_URL: &str = "https://nat-detection.s3.eu-west-2.amazonaws.com";
35const NODE_LAUNCHPAD_S3_BASE_URL: &str = "https://node-launchpad.s3.eu-west-2.amazonaws.com";
36const WINSW_URL: &str = "https://sn-node-manager.s3.eu-west-2.amazonaws.com/WinSW-x64.exe";
37
38#[derive(Clone, Debug, Eq, Hash, PartialEq)]
39pub enum ReleaseType {
40 Ant,
41 AntCtl,
42 AntCtlDaemon,
43 AntNode,
44 AntNodeRpcClient,
45 NatDetection,
46 NodeLaunchpad,
47}
48
49impl fmt::Display for ReleaseType {
50 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
51 write!(
52 f,
53 "{}",
54 match self {
55 ReleaseType::Ant => "ant",
56 ReleaseType::AntCtl => "antctl",
57 ReleaseType::AntCtlDaemon => "antctld",
58 ReleaseType::AntNode => "antnode",
59 ReleaseType::AntNodeRpcClient => "antnode_rpc_client",
60 ReleaseType::NatDetection => "nat-detection",
61 ReleaseType::NodeLaunchpad => "node-launchpad",
62 }
63 )
64 }
65}
66
67lazy_static! {
68 static ref RELEASE_TYPE_CRATE_NAME_MAP: HashMap<ReleaseType, &'static str> = {
69 let mut m = HashMap::new();
70 m.insert(ReleaseType::Ant, "ant-cli");
71 m.insert(ReleaseType::AntCtl, "ant-node-manager");
72 m.insert(ReleaseType::AntCtlDaemon, "ant-node-manager");
73 m.insert(ReleaseType::AntNode, "ant-node");
74 m.insert(ReleaseType::AntNodeRpcClient, "ant-node-rpc-client");
75 m.insert(ReleaseType::NatDetection, "nat-detection");
76 m.insert(ReleaseType::NodeLaunchpad, "node-launchpad");
77 m
78 };
79}
80
81#[derive(Clone, Debug, Deserialize, Eq, Hash, PartialEq, Serialize)]
82pub enum Platform {
83 LinuxMusl,
84 LinuxMuslAarch64,
85 LinuxMuslArm,
86 LinuxMuslArmV7,
87 MacOs,
88 MacOsAarch64,
89 Windows,
90}
91
92impl fmt::Display for Platform {
93 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
94 match self {
95 Platform::LinuxMusl => write!(f, "x86_64-unknown-linux-musl"),
96 Platform::LinuxMuslAarch64 => write!(f, "aarch64-unknown-linux-musl"),
97 Platform::LinuxMuslArm => write!(f, "arm-unknown-linux-musleabi"),
98 Platform::LinuxMuslArmV7 => write!(f, "armv7-unknown-linux-musleabihf"),
99 Platform::MacOs => write!(f, "x86_64-apple-darwin"),
100 Platform::MacOsAarch64 => write!(f, "aarch64-apple-darwin"),
101 Platform::Windows => write!(f, "x86_64-pc-windows-msvc"), }
103 }
104}
105
106impl Platform {
107 pub fn from_release_string(s: &str) -> Result<Self> {
109 match s {
110 "x86_64-unknown-linux-musl" => Ok(Platform::LinuxMusl),
111 "aarch64-unknown-linux-musl" => Ok(Platform::LinuxMuslAarch64),
112 "arm-unknown-linux-musleabi" => Ok(Platform::LinuxMuslArm),
113 "armv7-unknown-linux-musleabihf" => Ok(Platform::LinuxMuslArmV7),
114 "x86_64-apple-darwin" => Ok(Platform::MacOs),
115 "aarch64-apple-darwin" => Ok(Platform::MacOsAarch64),
116 "x86_64-pc-windows-msvc" => Ok(Platform::Windows),
117 _ => Err(Error::UnknownPlatform(s.to_string())),
118 }
119 }
120}
121
122#[derive(Clone, Debug, Eq, Hash, PartialEq)]
123pub enum ArchiveType {
124 TarGz,
125 Zip,
126}
127
128impl fmt::Display for ArchiveType {
129 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
130 match self {
131 ArchiveType::TarGz => write!(f, "tar.gz"),
132 ArchiveType::Zip => write!(f, "zip"),
133 }
134 }
135}
136
137pub type ProgressCallback = dyn Fn(u64, u64) + Send + Sync;
138
139#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
141pub struct BinaryInfo {
142 pub name: String,
143 pub version: String,
144 pub sha256: String,
145}
146
147#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
149pub struct PlatformBinaries {
150 pub platform: Platform,
151 pub binaries: Vec<BinaryInfo>,
152}
153
154#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
156pub struct AutonomiReleaseInfo {
157 pub commit_hash: String,
158 pub name: String,
159 pub platform_binaries: Vec<PlatformBinaries>,
160}
161
162#[async_trait]
163pub trait AntReleaseRepoActions: Send + Sync {
164 async fn get_latest_version(&self, release_type: &ReleaseType) -> Result<Version>;
165 async fn download_release_from_s3(
166 &self,
167 release_type: &ReleaseType,
168 version: &Version,
169 platform: &Platform,
170 archive_type: &ArchiveType,
171 dest_path: &Path,
172 callback: &ProgressCallback,
173 ) -> Result<PathBuf>;
174 async fn download_release(
175 &self,
176 url: &str,
177 dest_dir_path: &Path,
178 callback: &ProgressCallback,
179 ) -> Result<PathBuf>;
180 async fn download_winsw(&self, dest_path: &Path, callback: &ProgressCallback) -> Result<()>;
181 fn extract_release_archive(&self, archive_path: &Path, dest_dir_path: &Path)
182 -> Result<PathBuf>;
183 async fn get_latest_autonomi_release_info(&self) -> Result<AutonomiReleaseInfo>;
184 async fn get_autonomi_release_info(&self, tag_name: &str) -> Result<AutonomiReleaseInfo>;
185}
186
187impl dyn AntReleaseRepoActions {
188 pub fn default_config() -> Box<dyn AntReleaseRepoActions> {
189 Box::new(AntReleaseRepository {
190 github_api_base_url: GITHUB_API_URL.to_string(),
191 nat_detection_base_url: NAT_DETECTION_S3_BASE_URL.to_string(),
192 node_launchpad_base_url: NODE_LAUNCHPAD_S3_BASE_URL.to_string(),
193 ant_base_url: ANT_S3_BASE_URL.to_string(),
194 antnode_base_url: ANTNODE_S3_BASE_URL.to_string(),
195 antctl_base_url: ANTCTL_S3_BASE_URL.to_string(),
196 antnode_rpc_client_base_url: ANTNODE_RPC_CLIENT_S3_BASE_URL.to_string(),
197 })
198 }
199}
200
201pub struct AntReleaseRepository {
202 pub ant_base_url: String,
203 pub antctl_base_url: String,
204 pub antnode_base_url: String,
205 pub antnode_rpc_client_base_url: String,
206 pub github_api_base_url: String,
207 pub nat_detection_base_url: String,
208 pub node_launchpad_base_url: String,
209}
210
211impl AntReleaseRepository {
212 fn get_base_url(&self, release_type: &ReleaseType) -> String {
213 match release_type {
214 ReleaseType::Ant => self.ant_base_url.clone(),
215 ReleaseType::AntCtl => self.antctl_base_url.clone(),
216 ReleaseType::AntCtlDaemon => self.antctl_base_url.clone(),
217 ReleaseType::AntNode => self.antnode_base_url.clone(),
218 ReleaseType::AntNodeRpcClient => self.antnode_rpc_client_base_url.clone(),
219 ReleaseType::NatDetection => self.nat_detection_base_url.clone(),
220 ReleaseType::NodeLaunchpad => self.node_launchpad_base_url.clone(),
221 }
222 }
223
224 fn parse_release_body(&self, body: &str) -> Result<Vec<PlatformBinaries>> {
226 use regex::Regex;
227
228 let version_regex = Regex::new(r"\* `([^`]+)`: v?([0-9.]+(?:-[a-zA-Z0-9.]+)?)")
230 .map_err(|_| Error::RegexError)?;
231 let mut binary_versions = HashMap::new();
232 for cap in version_regex.captures_iter(body) {
233 let name = cap[1].to_string();
234 let version = cap[2].to_string();
235 binary_versions.insert(name, version);
236 }
237
238 let lines: Vec<&str> = body.lines().collect();
240 let mut platform_binaries = Vec::new();
241 let mut current_platform: Option<Platform> = None;
242 let mut current_binaries: Vec<BinaryInfo> = Vec::new();
243 let mut in_hash_table = false;
244
245 let hash_row_regex =
246 Regex::new(r"^\| ([^ ]+) \| `([a-f0-9]{64})` \|$").map_err(|_| Error::RegexError)?;
247
248 for line in lines {
249 let trimmed = line.trim();
250
251 if trimmed.starts_with("### ") {
253 if let Some(platform) = current_platform.take() {
255 if !current_binaries.is_empty() {
256 platform_binaries.push(PlatformBinaries {
257 platform,
258 binaries: current_binaries.clone(),
259 });
260 current_binaries.clear();
261 }
262 }
263
264 let platform_str = trimmed.trim_start_matches("### ");
265 if let Ok(platform) = Platform::from_release_string(platform_str) {
266 current_platform = Some(platform);
267 in_hash_table = false;
268 }
269 } else if trimmed == "| Binary | SHA256 Hash |" {
270 in_hash_table = true;
271 } else if trimmed.starts_with("|--------") {
272 } else if in_hash_table && current_platform.is_some() {
274 if let Some(cap) = hash_row_regex.captures(trimmed) {
275 let name = cap[1].to_string();
276 let sha256 = cap[2].to_string();
277 let version = binary_versions
278 .get(&name)
279 .cloned()
280 .unwrap_or_else(|| "unknown".to_string());
281
282 current_binaries.push(BinaryInfo {
283 name,
284 version,
285 sha256,
286 });
287 } else if !trimmed.is_empty() && !trimmed.starts_with("|") {
288 in_hash_table = false;
290 }
291 }
292 }
293
294 if let Some(platform) = current_platform {
296 if !current_binaries.is_empty() {
297 platform_binaries.push(PlatformBinaries {
298 platform,
299 binaries: current_binaries,
300 });
301 }
302 }
303
304 Ok(platform_binaries)
305 }
306
307 async fn download_url(
308 &self,
309 url: &str,
310 dest_path: &Path,
311 callback: &ProgressCallback,
312 ) -> Result<()> {
313 let client = Client::new();
314 let mut response = client.get(url).send().await?;
315 if !response.status().is_success() {
316 return Err(Error::ReleaseBinaryNotFound(url.to_string()));
317 }
318
319 let total_size = response
320 .headers()
321 .get("content-length")
322 .and_then(|ct_len| ct_len.to_str().ok())
323 .and_then(|ct_len| ct_len.parse::<u64>().ok())
324 .unwrap_or(0);
325
326 let mut downloaded: u64 = 0;
327 let mut out_file = File::create(&dest_path).await?;
328
329 while let Some(chunk) = response.chunk().await.unwrap() {
330 downloaded += chunk.len() as u64;
331 out_file.write_all(&chunk).await?;
332 callback(downloaded, total_size);
333 }
334
335 Ok(())
336 }
337
338 async fn fetch_autonomi_release(&self, url: &str) -> Result<AutonomiReleaseInfo> {
340 let client = Client::new();
341 let response = client
342 .get(url)
343 .header("Accept", "application/vnd.github+json")
344 .header("X-GitHub-Api-Version", "2022-11-28")
345 .header("User-Agent", "ant-releases")
346 .send()
347 .await?;
348 if !response.status().is_success() {
349 return Err(Error::LatestReleaseNotFound("autonomi".to_string()));
350 }
351
352 let json: Value = response.json().await?;
353 let commit_hash = json["target_commitish"]
354 .as_str()
355 .ok_or_else(|| Error::LatestReleaseNotFound("commit hash not found".to_string()))?
356 .to_string();
357 let name = json["name"]
358 .as_str()
359 .ok_or_else(|| Error::LatestReleaseNotFound("release name not found".to_string()))?
360 .to_string();
361 let body = json["body"]
362 .as_str()
363 .ok_or_else(|| Error::LatestReleaseNotFound("release body not found".to_string()))?;
364
365 let platform_binaries = self.parse_release_body(body)?;
366
367 Ok(AutonomiReleaseInfo {
368 commit_hash,
369 name,
370 platform_binaries,
371 })
372 }
373}
374
375#[async_trait]
376impl AntReleaseRepoActions for AntReleaseRepository {
377 async fn get_latest_version(&self, release_type: &ReleaseType) -> Result<Version> {
394 let crate_name = *RELEASE_TYPE_CRATE_NAME_MAP.get(release_type).unwrap();
395 let url = format!("https://crates.io/api/v1/crates/{crate_name}");
396
397 let client = reqwest::Client::new();
398 let response = client
399 .get(url)
400 .header("User-Agent", "reqwest")
401 .send()
402 .await?;
403 if !response.status().is_success() {
404 return Err(Error::CratesIoResponseError(response.status().as_u16()));
405 }
406
407 let body = response.text().await?;
408 let json: Value = serde_json::from_str(&body)?;
409
410 if let Some(version) = json["crate"]["newest_version"].as_str() {
411 return Ok(Version::parse(version)?);
412 }
413
414 Err(Error::LatestReleaseNotFound(release_type.to_string()))
415 }
416
417 async fn download_release_from_s3(
433 &self,
434 release_type: &ReleaseType,
435 version: &Version,
436 platform: &Platform,
437 archive_type: &ArchiveType,
438 dest_path: &Path,
439 callback: &ProgressCallback,
440 ) -> Result<PathBuf> {
441 let archive_ext = archive_type.to_string();
442 let url = format!(
443 "{}/{}-{}-{}.{}",
444 self.get_base_url(release_type),
445 release_type.to_string().to_lowercase(),
446 version,
447 platform,
448 archive_type
449 );
450
451 let archive_name = format!(
452 "{}-{}-{}.{}",
453 release_type.to_string().to_lowercase(),
454 version,
455 platform,
456 archive_ext
457 );
458 let archive_path = dest_path.join(archive_name);
459
460 self.download_url(&url, &archive_path, callback).await?;
461
462 Ok(archive_path)
463 }
464
465 async fn download_release(
466 &self,
467 url: &str,
468 dest_dir_path: &Path,
469 callback: &ProgressCallback,
470 ) -> Result<PathBuf> {
471 if !url.ends_with(".tar.gz") && !url.ends_with(".zip") {
472 return Err(Error::UrlIsNotArchive);
473 }
474
475 let file_name = url
476 .split('/')
477 .next_back()
478 .ok_or_else(|| Error::CannotParseFilenameFromUrl)?;
479 let dest_path = dest_dir_path.join(file_name);
480
481 self.download_url(url, &dest_path, callback).await?;
482
483 Ok(dest_path)
484 }
485
486 async fn download_winsw(&self, dest_path: &Path, callback: &ProgressCallback) -> Result<()> {
487 self.download_url(WINSW_URL, dest_path, callback).await?;
488 Ok(())
489 }
490
491 fn extract_release_archive(
504 &self,
505 archive_path: &Path,
506 dest_dir_path: &Path,
507 ) -> Result<PathBuf> {
508 if !archive_path.exists() {
509 return Err(Error::Io(std::io::Error::new(
510 std::io::ErrorKind::NotFound,
511 format!("Archive not found at: {:?}", archive_path),
512 )));
513 }
514
515 if archive_path.extension() == Some(std::ffi::OsStr::new("gz")) {
516 let archive_file = std::fs::File::open(archive_path)?;
517 let tarball = flate2::read::GzDecoder::new(archive_file);
518 let mut archive = Archive::new(tarball);
519 if let Some(file) = (archive.entries()?).next() {
520 let mut file = file?;
521 let out_path = dest_dir_path.join(file.path()?);
522 file.unpack(&out_path)?;
523 return Ok(out_path);
524 }
525 } else if archive_path.extension() == Some(std::ffi::OsStr::new("zip")) {
526 let archive_file = std::fs::File::open(archive_path)?;
527 let mut archive = ZipArchive::new(archive_file)?;
528 if let Some(i) = (0..archive.len()).next() {
529 let mut file = archive.by_index(i)?;
530 let out_path = dest_dir_path.join(file.name());
531 if file.name().ends_with('/') {
532 std::fs::create_dir_all(&out_path)?;
533 } else {
534 let mut outfile = std::fs::File::create(&out_path)?;
535 std::io::copy(&mut file, &mut outfile)?;
536 }
537 return Ok(out_path);
538 }
539 } else {
540 return Err(Error::Io(std::io::Error::new(
541 std::io::ErrorKind::InvalidInput,
542 "Unsupported archive format",
543 )));
544 }
545
546 Err(Error::Io(std::io::Error::other(
547 "Failed to extract archive",
548 )))
549 }
550
551 async fn get_latest_autonomi_release_info(&self) -> Result<AutonomiReleaseInfo> {
552 let url = format!(
553 "{}/repos/maidsafe/autonomi/releases/latest",
554 self.github_api_base_url
555 );
556 self.fetch_autonomi_release(&url).await
557 }
558
559 async fn get_autonomi_release_info(&self, tag_name: &str) -> Result<AutonomiReleaseInfo> {
560 let url = format!(
561 "{}/repos/maidsafe/autonomi/releases/tags/{}",
562 self.github_api_base_url, tag_name
563 );
564 self.fetch_autonomi_release(&url).await
565 }
566}
567
568pub fn get_running_platform() -> Result<Platform> {
569 match OS {
570 "linux" => match ARCH {
571 "x86_64" => Ok(Platform::LinuxMusl),
572 "armv7" => Ok(Platform::LinuxMuslArmV7),
573 "arm" => Ok(Platform::LinuxMuslArm),
574 "aarch64" => Ok(Platform::LinuxMuslAarch64),
575 &_ => Err(Error::PlatformNotSupported(format!(
576 "We currently do not have binaries for the {OS}/{ARCH} combination"
577 ))),
578 },
579 "windows" => {
580 if ARCH != "x86_64" {
581 return Err(Error::PlatformNotSupported(
582 "We currently only have x86_64 binaries available for Windows".to_string(),
583 ));
584 }
585 Ok(Platform::Windows)
586 }
587 "macos" => match ARCH {
588 "x86_64" => Ok(Platform::MacOs),
589 "aarch64" => Ok(Platform::MacOsAarch64),
590 &_ => Err(Error::PlatformNotSupported(format!(
591 "We currently do not have binaries for the {OS}/{ARCH} combination"
592 ))),
593 },
594 &_ => Err(Error::PlatformNotSupported(format!(
595 "{OS} is not currently supported"
596 ))),
597 }
598}