Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

chore: remove testnet from release type #11

Merged
merged 3 commits into from
Feb 1, 2024
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.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
8 changes: 5 additions & 3 deletions src/error.rs
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
// Copyright (C) 2023 MaidSafe.net limited.
// Copyright (C) 2024 MaidSafe.net limited.
//
// This SAFE Network Software is licensed to you under The General Public License (GPL), version 3.
// Unless required by applicable law or agreed to in writing, the SAFE Network Software distributed
Expand All @@ -15,16 +15,18 @@ pub type Result<T> = std::result::Result<T, Error>;
pub enum Error {
#[error("Cannot parse file name from the URL")]
CannotParseFilenameFromUrl,
#[error("Unexpected response from crates.io: {0}")]
CratesIoResponseError(u16),
#[error(transparent)]
DateTimeParseError(#[from] chrono::ParseError),
#[error("Could not convert API response header links to string")]
HeaderLinksToStrError,
#[error(transparent)]
Io(#[from] std::io::Error),
#[error(transparent)]
JsonError(#[from] serde_json::Error),
#[error("Latest release not found for {0}")]
LatestReleaseNotFound(String),
#[error("The Github API's latest release response document was not in the expected format")]
MalformedLatestReleaseResponse,
#[error("{0}")]
PlatformNotSupported(String),
#[error(transparent)]
Expand Down
167 changes: 21 additions & 146 deletions src/lib.rs
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
// Copyright (C) 2023 MaidSafe.net limited.
// Copyright (C) 2024 MaidSafe.net limited.
//
// This SAFE Network Software is licensed to you under The General Public License (GPL), version 3.
// Unless required by applicable law or agreed to in writing, the SAFE Network Software distributed
Expand All @@ -11,9 +11,8 @@ pub use crate::error::{Error, Result};
pub mod error;

use async_trait::async_trait;
use chrono::{DateTime, Duration, Utc};
use lazy_static::lazy_static;
use reqwest::{header::HeaderMap, Client, Response};
use reqwest::Client;
use serde_json::Value;
use std::collections::HashMap;
use std::env::consts::{ARCH, OS};
Expand All @@ -25,15 +24,12 @@ use tokio::io::AsyncWriteExt;
use zip::ZipArchive;

const GITHUB_API_URL: &str = "https://api.github.com";
const GITHUB_ORG_NAME: &str = "maidsafe";
const GITHUB_REPO_NAME: &str = "safe_network";
const FAUCET_S3_BASE_URL: &str = "https://sn-faucet.s3.eu-west-2.amazonaws.com";
const SAFE_S3_BASE_URL: &str = "https://sn-cli.s3.eu-west-2.amazonaws.com";
const SAFENODE_S3_BASE_URL: &str = "https://sn-node.s3.eu-west-2.amazonaws.com";
const SAFENODE_MANAGER_S3_BASE_URL: &str = "https://sn-node-manager.s3.eu-west-2.amazonaws.com";
const SAFENODE_RPC_CLIENT_S3_BASE_URL: &str =
"https://sn-node-rpc-client.s3.eu-west-2.amazonaws.com";
const TESTNET_S3_BASE_URL: &str = "https://sn-testnet.s3.eu-west-2.amazonaws.com";

#[derive(Clone, Debug, Eq, Hash, PartialEq)]
pub enum ReleaseType {
Expand All @@ -42,7 +38,6 @@ pub enum ReleaseType {
Safenode,
SafenodeManager,
SafenodeRpcClient,
Testnet,
}

impl fmt::Display for ReleaseType {
Expand All @@ -56,25 +51,11 @@ impl fmt::Display for ReleaseType {
ReleaseType::Safenode => "safenode",
ReleaseType::SafenodeManager => "safenode-manager",
ReleaseType::SafenodeRpcClient => "safenode_rpc_client",
ReleaseType::Testnet => "testnet",
}
)
}
}

impl ReleaseType {
pub fn get_repo_name(&self) -> String {
match &self {
ReleaseType::Faucet
| ReleaseType::Safe
| ReleaseType::Safenode
| ReleaseType::SafenodeRpcClient
| ReleaseType::Testnet => "safe_network".to_string(),
ReleaseType::SafenodeManager => "sn-node-manager".to_string(),
}
}
}

lazy_static! {
static ref RELEASE_TYPE_CRATE_NAME_MAP: HashMap<ReleaseType, &'static str> = {
let mut m = HashMap::new();
Expand All @@ -83,7 +64,6 @@ lazy_static! {
m.insert(ReleaseType::Safenode, "sn_node");
m.insert(ReleaseType::SafenodeManager, "sn-node-manager");
m.insert(ReleaseType::SafenodeRpcClient, "sn_node_rpc_client");
m.insert(ReleaseType::Testnet, "sn_testnet");
m
};
}
Expand Down Expand Up @@ -159,7 +139,6 @@ impl dyn SafeReleaseRepositoryInterface {
safenode_base_url: SAFENODE_S3_BASE_URL.to_string(),
safenode_manager_base_url: SAFENODE_MANAGER_S3_BASE_URL.to_string(),
safenode_rpc_client_base_url: SAFENODE_RPC_CLIENT_S3_BASE_URL.to_string(),
testnet_base_url: TESTNET_S3_BASE_URL.to_string(),
})
}
}
Expand All @@ -171,7 +150,6 @@ pub struct SafeReleaseRepository {
pub safenode_base_url: String,
pub safenode_manager_base_url: String,
pub safenode_rpc_client_base_url: String,
pub testnet_base_url: String,
}

impl SafeReleaseRepository {
Expand All @@ -182,63 +160,9 @@ impl SafeReleaseRepository {
ReleaseType::Safenode => self.safenode_base_url.clone(),
ReleaseType::SafenodeManager => self.safenode_manager_base_url.clone(),
ReleaseType::SafenodeRpcClient => self.safenode_rpc_client_base_url.clone(),
ReleaseType::Testnet => self.testnet_base_url.clone(),
}
}

async fn get_latest_release_tag(&self, release_type: &ReleaseType) -> Result<String> {
let client = Client::new();
let response = client
.get(format!(
"{}/repos/{}/{}/releases/latest",
self.github_api_base_url,
GITHUB_ORG_NAME,
release_type.get_repo_name()
))
.header("User-Agent", "request")
.send()
.await?;

let latest_release = response.json::<Value>().await?;
if let Some(Value::String(tag_name)) = latest_release.get("tag_name") {
return Ok(tag_name.trim_start_matches('v').to_string());
}

Err(Error::MalformedLatestReleaseResponse)
}

async fn get_releases_page(&self, page: u32, per_page: u32) -> Result<Response> {
let client = Client::new();
let response = client
.get(format!(
"{}/repos/{}/{}/releases?page={}&per_page={}",
self.github_api_base_url, GITHUB_ORG_NAME, GITHUB_REPO_NAME, page, per_page
))
.header("User-Agent", "request")
.send()
.await?;
Ok(response)
}

async fn has_next_page(&self, headers: &HeaderMap) -> Result<bool> {
if let Some(links) = headers.get("link") {
let links = links.to_str().map_err(|_| Error::HeaderLinksToStrError)?;
Ok(links.split(',').any(|link| link.contains("rel=\"next\"")))
} else {
Ok(false)
}
}

fn get_version_from_tag_name(&self, tag_name: &str) -> Result<String> {
let mut parts = tag_name.split('-');
parts.next();
let version = parts
.next()
.ok_or_else(|| Error::TagNameVersionParsingFailed)?
.to_string();
Ok(version.trim_start_matches('v').to_string())
}

async fn download_url(
&self,
url: &str,
Expand Down Expand Up @@ -273,19 +197,7 @@ impl SafeReleaseRepository {

#[async_trait]
impl SafeReleaseRepositoryInterface for SafeReleaseRepository {
/// Gets the latest version for a specified binary.
///
/// If we are looking for a node manager release, this is not a workspace repo, so we can
/// simply use the latest release API. Otherwise, we will query the `safe_network` repo.
///
/// Each release in the repository is checked, starting from the most recent. The `safe_network`
/// repository is a workspace to which many binaries are released, so it's not possible to use the
/// more straight forward Github API which simply returns the latest release, since that's going to
/// be the version number for one of many binaries.
///
/// During the search, if a release is found that was created more than 14 days ago, the function
/// will stop searching through older releases, which will avoid fetching further pages from the
/// Github API.
/// Uses the crates.io API to obtain the latest version of a crate.
///
/// # Arguments
///
Expand All @@ -299,67 +211,30 @@ impl SafeReleaseRepositoryInterface for SafeReleaseRepository {
/// # Errors
///
/// This function will return an error if:
/// - The HTTP request to GitHub API fails
/// - The received JSON data from the API is not as expected
/// - No releases are found that match the specified `ReleaseType`
/// - The HTTP request to crates.io API fails
/// - The received JSON data does not have a `crate.newest_version` value
async fn get_latest_version(&self, release_type: &ReleaseType) -> Result<String> {
if *release_type == ReleaseType::SafenodeManager {
return self.get_latest_release_tag(release_type).await;
let crate_name = *RELEASE_TYPE_CRATE_NAME_MAP.get(release_type).unwrap();
let url = format!("https://crates.io/api/v1/crates/{}", crate_name);

let client = reqwest::Client::new();
let response = client
.get(url)
.header("User-Agent", "reqwest")
.send()
.await?;
if !response.status().is_success() {
return Err(Error::CratesIoResponseError(response.status().as_u16()));
}

let mut page = 1;
let per_page = 100;
let mut latest_release: Option<(String, DateTime<Utc>)> = None;
let target_tag_name = *RELEASE_TYPE_CRATE_NAME_MAP.get(release_type).unwrap();
let now = Utc::now();

loop {
let response = self.get_releases_page(page, per_page).await?;
let headers = response.headers().clone();
let releases = response.json::<Value>().await?;

let mut continue_search = true;
if let Value::Array(releases) = releases {
for release in releases {
if let Value::Object(release) = release {
if let (Some(Value::String(tag_name)), Some(Value::String(created_at))) =
(release.get("tag_name"), release.get("created_at"))
{
let created_at = created_at.parse::<DateTime<Utc>>()?;
let crate_name = tag_name.split('-').next().unwrap().to_string();
if crate_name == target_tag_name {
match latest_release {
Some((_, date)) if created_at > date => {
latest_release = Some((tag_name.clone(), created_at));
}
None => {
latest_release = Some((tag_name.clone(), created_at));
}
_ => {}
}
}

if now.signed_duration_since(created_at) > Duration::days(14) {
continue_search = false;
break;
}
}
}
}
}
let body = response.text().await?;
let json: Value = serde_json::from_str(&body)?;

if continue_search && self.has_next_page(&headers).await? {
page += 1;
} else {
break;
}
if let Some(version) = json["crate"]["newest_version"].as_str() {
return Ok(version.to_string());
}

let tag_name = latest_release
.ok_or_else(|| Error::LatestReleaseNotFound(release_type.to_string()))?
.0;
let version = self.get_version_from_tag_name(&tag_name)?;
Ok(version)
Err(Error::LatestReleaseNotFound(release_type.to_string()))
}

/// Downloads a release binary archive from S3.
Expand Down
2 changes: 1 addition & 1 deletion tests/download_url.rs
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
// Copyright (C) 2023 MaidSafe.net limited.
// Copyright (C) 2024 MaidSafe.net limited.
//
// This SAFE Network Software is licensed to you under The General Public License (GPL), version 3.
// Unless required by applicable law or agreed to in writing, the SAFE Network Software distributed
Expand Down
73 changes: 1 addition & 72 deletions tests/test_download_from_s3.rs
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
// Copyright (C) 2023 MaidSafe.net limited.
// Copyright (C) 2024 MaidSafe.net limited.
//
// This SAFE Network Software is licensed to you under The General Public License (GPL), version 3.
// Unless required by applicable law or agreed to in writing, the SAFE Network Software distributed
Expand All @@ -16,7 +16,6 @@ const SAFE_VERSION: &str = "0.83.51";
const SAFENODE_VERSION: &str = "0.93.7";
const SAFENODE_MANAGER_VERSION: &str = "0.1.8";
const SAFENODE_RPC_CLIENT_VERSION: &str = "0.1.40";
const TESTNET_VERSION: &str = "0.2.213";

async fn download_and_extract(
release_type: &ReleaseType,
Expand Down Expand Up @@ -55,7 +54,6 @@ async fn download_and_extract(
ReleaseType::Safenode => "safenode",
ReleaseType::SafenodeManager => "safenode-manager",
ReleaseType::SafenodeRpcClient => "safenode_rpc_client",
ReleaseType::Testnet => "testnet",
};
let expected_binary_name = if *platform == Platform::Windows {
format!("{}.exe", binary_name)
Expand Down Expand Up @@ -239,75 +237,6 @@ async fn should_download_and_extract_safenode_for_windows() {
.await;
}

///
/// Testnet Tests
///
#[tokio::test]
async fn should_download_and_extract_testnet_for_linux_musl() {
download_and_extract(
&ReleaseType::Testnet,
TESTNET_VERSION,
&Platform::LinuxMusl,
&ArchiveType::TarGz,
)
.await;
}

#[tokio::test]
async fn should_download_and_extract_testnet_for_linux_musl_aarch64() {
download_and_extract(
&ReleaseType::Testnet,
TESTNET_VERSION,
&Platform::LinuxMuslAarch64,
&ArchiveType::TarGz,
)
.await;
}

#[tokio::test]
async fn should_download_and_extract_testnet_for_linux_musl_arm() {
download_and_extract(
&ReleaseType::Testnet,
TESTNET_VERSION,
&Platform::LinuxMuslArm,
&ArchiveType::TarGz,
)
.await;
}

#[tokio::test]
async fn should_download_and_extract_testnet_for_linux_musl_arm_v7() {
download_and_extract(
&ReleaseType::Testnet,
TESTNET_VERSION,
&Platform::LinuxMuslArmV7,
&ArchiveType::TarGz,
)
.await;
}

#[tokio::test]
async fn should_download_and_extract_testnet_for_macos() {
download_and_extract(
&ReleaseType::Testnet,
TESTNET_VERSION,
&Platform::MacOs,
&ArchiveType::TarGz,
)
.await;
}

#[tokio::test]
async fn should_download_and_extract_testnet_for_windows() {
download_and_extract(
&ReleaseType::Testnet,
TESTNET_VERSION,
&Platform::Windows,
&ArchiveType::Zip,
)
.await;
}

///
/// Safenode RPC client tests
///
Expand Down