Skip to content

Commit

Permalink
feat: function for downloading custom url
Browse files Browse the repository at this point in the history
In the node manager we want to support downloading a 'custom' binary, which is really a binary of
`safenode` from someone's fork. These get uploaded and used during testnet deployments. This new
function downloads the URL not on the basis of a convention, but instead just allowing any URL to be
specified.

We make the assertion that the URL has to point to a zip or gzipped tar file.
  • Loading branch information
jacderida committed Dec 13, 2023
1 parent 805b0bf commit 7dba8dc
Show file tree
Hide file tree
Showing 3 changed files with 120 additions and 20 deletions.
4 changes: 4 additions & 0 deletions src/error.rs
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,8 @@ pub type Result<T> = std::result::Result<T, Error>;
pub enum Error {
#[error(transparent)]
DateTimeParseError(#[from] chrono::ParseError),
#[error("Cannot parse file name from the URL")]
CannotParseFilenameFromUrl,
#[error("Could not convert API response header links to string")]
HeaderLinksToStrError,
#[error(transparent)]
Expand All @@ -29,6 +31,8 @@ pub enum Error {
ReleaseBinaryNotFound(String),
#[error("Could not parse version from tag name")]
TagNameVersionParsingFailed,
#[error("The URL must point to a zip or gzipped tar archive")]
UrlIsNotArchive,
#[error(transparent)]
ZipError(#[from] zip::result::ZipError),
}
79 changes: 59 additions & 20 deletions src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -119,6 +119,12 @@ pub trait SafeReleaseRepositoryInterface {
dest_path: &Path,
callback: &ProgressCallback,
) -> Result<PathBuf>;
async fn download_release(
&self,
url: &str,
dest_dir_path: &Path,
callback: &ProgressCallback,
) -> Result<PathBuf>;
fn extract_release_archive(&self, archive_path: &Path, dest_dir_path: &Path)
-> Result<PathBuf>;
}
Expand Down Expand Up @@ -184,6 +190,37 @@ impl SafeReleaseRepository {
.to_string();
Ok(version.trim_start_matches('v').to_string())
}

async fn download_url(
&self,
url: &str,
dest_path: &PathBuf,
callback: &ProgressCallback,
) -> Result<()> {
let client = Client::new();
let mut response = client.get(url).send().await?;
if !response.status().is_success() {
return Err(Error::ReleaseBinaryNotFound(url.to_string()));
}

let total_size = response
.headers()
.get("content-length")
.and_then(|ct_len| ct_len.to_str().ok())
.and_then(|ct_len| ct_len.parse::<u64>().ok())
.unwrap_or(0);

let mut downloaded: u64 = 0;
let mut out_file = File::create(&dest_path).await?;

while let Some(chunk) = response.chunk().await.unwrap() {
downloaded += chunk.len() as u64;
out_file.write_all(&chunk).await?;
callback(downloaded, total_size);
}

Ok(())
}
}

#[async_trait]
Expand Down Expand Up @@ -304,20 +341,6 @@ impl SafeReleaseRepositoryInterface for SafeReleaseRepository {
archive_type
);

let client = Client::new();
let mut response = client.get(&url).send().await?;
if !response.status().is_success() {
return Err(Error::ReleaseBinaryNotFound(url));
}

let total_size = response
.headers()
.get("content-length")
.and_then(|ct_len| ct_len.to_str().ok())
.and_then(|ct_len| ct_len.parse::<u64>().ok())
.unwrap_or(0);

let mut downloaded: u64 = 0;
let archive_name = format!(
"{}-{}-{}.{}",
release_type.to_string().to_lowercase(),
Expand All @@ -326,17 +349,33 @@ impl SafeReleaseRepositoryInterface for SafeReleaseRepository {
archive_ext
);
let archive_path = dest_path.join(archive_name);
let mut out_file = File::create(&archive_path).await?;

while let Some(chunk) = response.chunk().await.unwrap() {
downloaded += chunk.len() as u64;
out_file.write_all(&chunk).await?;
callback(downloaded, total_size);
}
self.download_url(&url, &archive_path, callback).await?;

Ok(archive_path)
}

async fn download_release(
&self,
url: &str,
dest_dir_path: &Path,
callback: &ProgressCallback,
) -> Result<PathBuf> {
if !url.ends_with(".tar.gz") && !url.ends_with(".zip") {
return Err(Error::UrlIsNotArchive);
}

let file_name = url
.split('/')
.last()
.ok_or_else(|| Error::CannotParseFilenameFromUrl)?;
let dest_path = dest_dir_path.join(file_name);

self.download_url(url, &dest_path, callback).await?;

Ok(dest_path)
}

/// Extracts a release binary archive.
///
/// The archive will include a single binary file.
Expand Down
57 changes: 57 additions & 0 deletions tests/download_url.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
// Copyright (C) 2023 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
// under the GPL Licence is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
// KIND, either express or implied. Please review the Licences for the specific language governing
// permissions and limitations relating to use of the SAFE Network Software.

use assert_fs::prelude::*;
use sn_releases::error::Error;
use sn_releases::SafeReleaseRepositoryInterface;

#[tokio::test]
async fn should_download_a_custom_binary() {
let dest_dir = assert_fs::TempDir::new().unwrap();
let download_dir = dest_dir.child("download_to");
download_dir.create_dir_all().unwrap();
let downloaded_archive =
download_dir.child("safenode-charlie-x86_64-unknown-linux-musl.tar.gz");

let url = "https://sn-node.s3.eu-west-2.amazonaws.com/jacderida/file-upload-address/safenode-charlie-x86_64-unknown-linux-musl.tar.gz";
let progress_callback = |_downloaded: u64, _total: u64| {};
let release_repo = <dyn SafeReleaseRepositoryInterface>::default_config();
release_repo
.download_release(url, &download_dir, &progress_callback)
.await
.unwrap();

downloaded_archive.assert(predicates::path::is_file());
}

#[tokio::test]
async fn should_fail_to_download_non_archive() {
let dest_dir = assert_fs::TempDir::new().unwrap();
let download_dir = dest_dir.child("download_to");
download_dir.create_dir_all().unwrap();

let url = "https://sn-node.s3.eu-west-2.amazonaws.com/jacderida/file-upload-address/safenode-charlie-x86_64-unknown-linux-musl.txt";
let progress_callback = |_downloaded: u64, _total: u64| {};
let release_repo = <dyn SafeReleaseRepositoryInterface>::default_config();
let result = release_repo
.download_release(url, &download_dir, &progress_callback)
.await;

match result {
Ok(_) => panic!("This test should result in a failure"),
Err(e) => match e {
Error::UrlIsNotArchive => {
assert_eq!(
e.to_string(),
"The URL must point to a zip or gzipped tar archive"
);
}
_ => panic!("The error type should be ReleaseBinaryNotFound"),
},
}
}

0 comments on commit 7dba8dc

Please sign in to comment.