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_json::Value;
18use std::collections::HashMap;
19use std::env::consts::{ARCH, OS};
20use std::fmt;
21use std::path::{Path, PathBuf};
22use tar::Archive;
23use tokio::fs::File;
24use tokio::io::AsyncWriteExt;
25use zip::ZipArchive;
26
27const AUTONOMI_S3_BASE_URL: &str = "https://autonomi-cli.s3.eu-west-2.amazonaws.com";
28const GITHUB_API_URL: &str = "https://api.github.com";
29const NAT_DETECTION_S3_BASE_URL: &str = "https://nat-detection.s3.eu-west-2.amazonaws.com";
30const NODE_LAUNCHPAD_S3_BASE_URL: &str = "https://node-launchpad.s3.eu-west-2.amazonaws.com";
31const SAFENODE_MANAGER_S3_BASE_URL: &str = "https://sn-node-manager.s3.eu-west-2.amazonaws.com";
32const SAFENODE_RPC_CLIENT_S3_BASE_URL: &str =
33 "https://sn-node-rpc-client.s3.eu-west-2.amazonaws.com";
34const SAFENODE_S3_BASE_URL: &str = "https://sn-node.s3.eu-west-2.amazonaws.com";
35const WINSW_URL: &str = "https://sn-node-manager.s3.eu-west-2.amazonaws.com/WinSW-x64.exe";
36
37#[derive(Clone, Debug, Eq, Hash, PartialEq)]
38pub enum ReleaseType {
39 Autonomi,
40 NatDetection,
41 NodeLaunchpad,
42 Safenode,
43 SafenodeManager,
44 SafenodeManagerDaemon,
45 SafenodeRpcClient,
46}
47
48impl fmt::Display for ReleaseType {
49 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
50 write!(
51 f,
52 "{}",
53 match self {
54 ReleaseType::Autonomi => "autonomi",
55 ReleaseType::NatDetection => "nat-detection",
56 ReleaseType::NodeLaunchpad => "node-launchpad",
57 ReleaseType::Safenode => "safenode",
58 ReleaseType::SafenodeManager => "safenode-manager",
59 ReleaseType::SafenodeManagerDaemon => "safenodemand",
60 ReleaseType::SafenodeRpcClient => "safenode_rpc_client",
61 }
62 )
63 }
64}
65
66lazy_static! {
67 static ref RELEASE_TYPE_CRATE_NAME_MAP: HashMap<ReleaseType, &'static str> = {
68 let mut m = HashMap::new();
69 m.insert(ReleaseType::Autonomi, "autonomi-cli");
70 m.insert(ReleaseType::NatDetection, "nat-detection");
71 m.insert(ReleaseType::NodeLaunchpad, "node-launchpad");
72 m.insert(ReleaseType::Safenode, "sn_node");
73 m.insert(ReleaseType::SafenodeManager, "sn-node-manager");
74 m.insert(ReleaseType::SafenodeManagerDaemon, "sn-node-manager");
75 m.insert(ReleaseType::SafenodeRpcClient, "sn_node_rpc_client");
76 m
77 };
78}
79
80#[derive(Clone, Eq, Hash, PartialEq)]
81pub enum Platform {
82 LinuxMusl,
83 LinuxMuslAarch64,
84 LinuxMuslArm,
85 LinuxMuslArmV7,
86 MacOs,
87 MacOsAarch64,
88 Windows,
89}
90
91impl fmt::Display for Platform {
92 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
93 match self {
94 Platform::LinuxMusl => write!(f, "x86_64-unknown-linux-musl"),
95 Platform::LinuxMuslAarch64 => write!(f, "aarch64-unknown-linux-musl"),
96 Platform::LinuxMuslArm => write!(f, "arm-unknown-linux-musleabi"),
97 Platform::LinuxMuslArmV7 => write!(f, "armv7-unknown-linux-musleabihf"),
98 Platform::MacOs => write!(f, "x86_64-apple-darwin"),
99 Platform::MacOsAarch64 => write!(f, "aarch64-apple-darwin"),
100 Platform::Windows => write!(f, "x86_64-pc-windows-msvc"), }
102 }
103}
104
105#[derive(Clone, Debug, Eq, Hash, PartialEq)]
106pub enum ArchiveType {
107 TarGz,
108 Zip,
109}
110
111impl fmt::Display for ArchiveType {
112 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
113 match self {
114 ArchiveType::TarGz => write!(f, "tar.gz"),
115 ArchiveType::Zip => write!(f, "zip"),
116 }
117 }
118}
119
120pub type ProgressCallback = dyn Fn(u64, u64) + Send + Sync;
121
122#[async_trait]
123pub trait SafeReleaseRepoActions {
124 async fn get_latest_version(&self, release_type: &ReleaseType) -> Result<Version>;
125 async fn download_release_from_s3(
126 &self,
127 release_type: &ReleaseType,
128 version: &Version,
129 platform: &Platform,
130 archive_type: &ArchiveType,
131 dest_path: &Path,
132 callback: &ProgressCallback,
133 ) -> Result<PathBuf>;
134 async fn download_release(
135 &self,
136 url: &str,
137 dest_dir_path: &Path,
138 callback: &ProgressCallback,
139 ) -> Result<PathBuf>;
140 async fn download_winsw(&self, dest_path: &Path, callback: &ProgressCallback) -> Result<()>;
141 fn extract_release_archive(&self, archive_path: &Path, dest_dir_path: &Path)
142 -> Result<PathBuf>;
143}
144
145impl dyn SafeReleaseRepoActions {
146 pub fn default_config() -> Box<dyn SafeReleaseRepoActions> {
147 Box::new(SafeReleaseRepository {
148 github_api_base_url: GITHUB_API_URL.to_string(),
149 nat_detection_base_url: NAT_DETECTION_S3_BASE_URL.to_string(),
150 node_launchpad_base_url: NODE_LAUNCHPAD_S3_BASE_URL.to_string(),
151 autonomi_base_url: AUTONOMI_S3_BASE_URL.to_string(),
152 safenode_base_url: SAFENODE_S3_BASE_URL.to_string(),
153 safenode_manager_base_url: SAFENODE_MANAGER_S3_BASE_URL.to_string(),
154 safenode_rpc_client_base_url: SAFENODE_RPC_CLIENT_S3_BASE_URL.to_string(),
155 })
156 }
157}
158
159pub struct SafeReleaseRepository {
160 pub github_api_base_url: String,
161 pub nat_detection_base_url: String,
162 pub node_launchpad_base_url: String,
163 pub autonomi_base_url: String,
164 pub safenode_base_url: String,
165 pub safenode_manager_base_url: String,
166 pub safenode_rpc_client_base_url: String,
167}
168
169impl SafeReleaseRepository {
170 fn get_base_url(&self, release_type: &ReleaseType) -> String {
171 match release_type {
172 ReleaseType::NatDetection => self.nat_detection_base_url.clone(),
173 ReleaseType::NodeLaunchpad => self.node_launchpad_base_url.clone(),
174 ReleaseType::Autonomi => self.autonomi_base_url.clone(),
175 ReleaseType::Safenode => self.safenode_base_url.clone(),
176 ReleaseType::SafenodeManager => self.safenode_manager_base_url.clone(),
177 ReleaseType::SafenodeManagerDaemon => self.safenode_manager_base_url.clone(),
178 ReleaseType::SafenodeRpcClient => self.safenode_rpc_client_base_url.clone(),
179 }
180 }
181
182 async fn download_url(
183 &self,
184 url: &str,
185 dest_path: &Path,
186 callback: &ProgressCallback,
187 ) -> Result<()> {
188 let client = Client::new();
189 let mut response = client.get(url).send().await?;
190 if !response.status().is_success() {
191 return Err(Error::ReleaseBinaryNotFound(url.to_string()));
192 }
193
194 let total_size = response
195 .headers()
196 .get("content-length")
197 .and_then(|ct_len| ct_len.to_str().ok())
198 .and_then(|ct_len| ct_len.parse::<u64>().ok())
199 .unwrap_or(0);
200
201 let mut downloaded: u64 = 0;
202 let mut out_file = File::create(&dest_path).await?;
203
204 while let Some(chunk) = response.chunk().await.unwrap() {
205 downloaded += chunk.len() as u64;
206 out_file.write_all(&chunk).await?;
207 callback(downloaded, total_size);
208 }
209
210 Ok(())
211 }
212}
213
214#[async_trait]
215impl SafeReleaseRepoActions for SafeReleaseRepository {
216 async fn get_latest_version(&self, release_type: &ReleaseType) -> Result<Version> {
233 if matches!(release_type, ReleaseType::NodeLaunchpad) {
236 return Ok(Version::parse("0.1.0")?);
237 }
238
239 let crate_name = *RELEASE_TYPE_CRATE_NAME_MAP.get(release_type).unwrap();
240 let url = format!("https://crates.io/api/v1/crates/{}", crate_name);
241
242 let client = reqwest::Client::new();
243 let response = client
244 .get(url)
245 .header("User-Agent", "reqwest")
246 .send()
247 .await?;
248 if !response.status().is_success() {
249 return Err(Error::CratesIoResponseError(response.status().as_u16()));
250 }
251
252 let body = response.text().await?;
253 let json: Value = serde_json::from_str(&body)?;
254
255 if let Some(version) = json["crate"]["newest_version"].as_str() {
256 return Ok(Version::parse(version)?);
257 }
258
259 Err(Error::LatestReleaseNotFound(release_type.to_string()))
260 }
261
262 async fn download_release_from_s3(
278 &self,
279 release_type: &ReleaseType,
280 version: &Version,
281 platform: &Platform,
282 archive_type: &ArchiveType,
283 dest_path: &Path,
284 callback: &ProgressCallback,
285 ) -> Result<PathBuf> {
286 let archive_ext = archive_type.to_string();
287 let url = format!(
288 "{}/{}-{}-{}.{}",
289 self.get_base_url(release_type),
290 release_type.to_string().to_lowercase(),
291 version,
292 platform,
293 archive_type
294 );
295
296 let archive_name = format!(
297 "{}-{}-{}.{}",
298 release_type.to_string().to_lowercase(),
299 version,
300 platform,
301 archive_ext
302 );
303 let archive_path = dest_path.join(archive_name);
304
305 self.download_url(&url, &archive_path, callback).await?;
306
307 Ok(archive_path)
308 }
309
310 async fn download_release(
311 &self,
312 url: &str,
313 dest_dir_path: &Path,
314 callback: &ProgressCallback,
315 ) -> Result<PathBuf> {
316 if !url.ends_with(".tar.gz") && !url.ends_with(".zip") {
317 return Err(Error::UrlIsNotArchive);
318 }
319
320 let file_name = url
321 .split('/')
322 .last()
323 .ok_or_else(|| Error::CannotParseFilenameFromUrl)?;
324 let dest_path = dest_dir_path.join(file_name);
325
326 self.download_url(url, &dest_path, callback).await?;
327
328 Ok(dest_path)
329 }
330
331 async fn download_winsw(&self, dest_path: &Path, callback: &ProgressCallback) -> Result<()> {
332 self.download_url(WINSW_URL, dest_path, callback).await?;
333 Ok(())
334 }
335
336 fn extract_release_archive(
349 &self,
350 archive_path: &Path,
351 dest_dir_path: &Path,
352 ) -> Result<PathBuf> {
353 if !archive_path.exists() {
354 return Err(Error::Io(std::io::Error::new(
355 std::io::ErrorKind::NotFound,
356 format!("Archive not found at: {:?}", archive_path),
357 )));
358 }
359
360 if archive_path.extension() == Some(std::ffi::OsStr::new("gz")) {
361 let archive_file = std::fs::File::open(archive_path)?;
362 let tarball = flate2::read::GzDecoder::new(archive_file);
363 let mut archive = Archive::new(tarball);
364 if let Some(file) = (archive.entries()?).next() {
365 let mut file = file?;
366 let out_path = dest_dir_path.join(file.path()?);
367 file.unpack(&out_path)?;
368 return Ok(out_path);
369 }
370 } else if archive_path.extension() == Some(std::ffi::OsStr::new("zip")) {
371 let archive_file = std::fs::File::open(archive_path)?;
372 let mut archive = ZipArchive::new(archive_file)?;
373 if let Some(i) = (0..archive.len()).next() {
374 let mut file = archive.by_index(i)?;
375 let out_path = dest_dir_path.join(file.name());
376 if file.name().ends_with('/') {
377 std::fs::create_dir_all(&out_path)?;
378 } else {
379 let mut outfile = std::fs::File::create(&out_path)?;
380 std::io::copy(&mut file, &mut outfile)?;
381 }
382 return Ok(out_path);
383 }
384 } else {
385 return Err(Error::Io(std::io::Error::new(
386 std::io::ErrorKind::InvalidInput,
387 "Unsupported archive format",
388 )));
389 }
390
391 Err(Error::Io(std::io::Error::new(
392 std::io::ErrorKind::Other,
393 "Failed to extract archive",
394 )))
395 }
396}
397
398pub fn get_running_platform() -> Result<Platform> {
399 match OS {
400 "linux" => match ARCH {
401 "x86_64" => Ok(Platform::LinuxMusl),
402 "armv7" => Ok(Platform::LinuxMuslArmV7),
403 "arm" => Ok(Platform::LinuxMuslArm),
404 "aarch64" => Ok(Platform::LinuxMuslAarch64),
405 &_ => Err(Error::PlatformNotSupported(format!(
406 "We currently do not have binaries for the {OS}/{ARCH} combination"
407 ))),
408 },
409 "windows" => {
410 if ARCH != "x86_64" {
411 return Err(Error::PlatformNotSupported(
412 "We currently only have x86_64 binaries available for Windows".to_string(),
413 ));
414 }
415 Ok(Platform::Windows)
416 }
417 "macos" => match ARCH {
418 "x86_64" => Ok(Platform::MacOs),
419 "aarch64" => Ok(Platform::MacOsAarch64),
420 &_ => Err(Error::PlatformNotSupported(format!(
421 "We currently do not have binaries for the {OS}/{ARCH} combination"
422 ))),
423 },
424 &_ => Err(Error::PlatformNotSupported(format!(
425 "{OS} is not currently supported"
426 ))),
427 }
428}