-
Notifications
You must be signed in to change notification settings - Fork 824
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
feat(cli): Add "package dowload" command
Adds a new CLI command for downloading packages.
- Loading branch information
Showing
8 changed files
with
224 additions
and
53 deletions.
There are no files selected for viewing
Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.
Oops, something went wrong.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,166 @@ | ||
use std::path::PathBuf; | ||
|
||
use anyhow::Context; | ||
use futures::StreamExt; | ||
use tokio::io::AsyncWriteExt; | ||
use wasmer_registry::wasmer_env::WasmerEnv; | ||
use wasmer_wasix::runtime::resolver::PackageSpecifier; | ||
|
||
/// Download a package from the registry. | ||
#[derive(clap::Parser, Debug)] | ||
pub struct CmdPackageDownload { | ||
#[clap(flatten)] | ||
env: WasmerEnv, | ||
|
||
/// Verify that the downloaded file is a valid package. | ||
#[clap(long)] | ||
validate: bool, | ||
|
||
/// Path where the package file should be written to. | ||
/// If not specified, the data will be written to stdout. | ||
#[clap(short = 'o', long)] | ||
out_path: PathBuf, | ||
|
||
/// The package to download. | ||
/// Can be: | ||
/// * a pakage specifier: `namespace/package[@vesion]` | ||
/// * a URL | ||
package: PackageSpecifier, | ||
} | ||
|
||
impl CmdPackageDownload { | ||
pub(crate) fn execute(&self) -> Result<(), anyhow::Error> { | ||
let rt = tokio::runtime::Runtime::new()?; | ||
rt.block_on(self.run()) | ||
} | ||
|
||
async fn run(&self) -> Result<(), anyhow::Error> { | ||
if let Some(parent) = self.out_path.parent() { | ||
match parent.metadata() { | ||
Ok(m) => { | ||
if !m.is_dir() { | ||
anyhow::bail!( | ||
"parent of output file is not a directory: '{}'", | ||
parent.display() | ||
); | ||
} | ||
} | ||
Err(err) if err.kind() == std::io::ErrorKind::NotFound => { | ||
std::fs::create_dir_all(parent) | ||
.context("could not create parent directory of output file")?; | ||
} | ||
Err(err) => return Err(err.into()), | ||
} | ||
}; | ||
|
||
let (url, token) = match &self.package { | ||
PackageSpecifier::Registry { full_name, version } => { | ||
let mut url = self.env.registry_public_url()?; | ||
let p = format!("/{full_name}@{version}"); | ||
url.set_path(&p); | ||
(url, self.env.token()) | ||
} | ||
PackageSpecifier::Url(url) => { | ||
(url.clone(), self.env.get_token_opt().map(|x| x.to_string())) | ||
} | ||
PackageSpecifier::Path(_) => { | ||
anyhow::bail!("cannot download a package from a local path"); | ||
} | ||
}; | ||
|
||
let client = reqwest::Client::new(); | ||
let mut b = client | ||
.get(url.clone()) | ||
.header(http::header::ACCEPT, "application/webc"); | ||
if let Some(token) = token { | ||
b = b.header(http::header::AUTHORIZATION, format!("Bearer {token}")); | ||
}; | ||
let res = b | ||
.send() | ||
.await | ||
.context("http request failed")? | ||
.error_for_status() | ||
.context("http request failed with non-success status code")?; | ||
|
||
let time = std::time::SystemTime::now() | ||
.duration_since(std::time::UNIX_EPOCH)? | ||
.as_secs(); | ||
|
||
let tmp_name = url | ||
.as_str() | ||
.chars() | ||
.map(|x| match x { | ||
'a'..='z' | 'A'..='Z' | '0'..='9' | '-' => x, | ||
_ => '_', | ||
}) | ||
.collect::<String>(); | ||
let tmp_path = std::env::temp_dir().join(format!("wasmer-download-{}-{}", tmp_name, time)); | ||
let mut file = tokio::fs::File::create(&tmp_path).await.with_context(|| { | ||
format!( | ||
"could not create temporary file at '{}'", | ||
tmp_path.display() | ||
) | ||
})?; | ||
|
||
let ty = res | ||
.headers() | ||
.get(http::header::CONTENT_TYPE) | ||
.and_then(|t| t.to_str().ok()) | ||
.unwrap_or_default(); | ||
|
||
if !(ty == "application/webc" || ty == "application/octet-stream") { | ||
eprintln!( | ||
"Warning: response has invalid content type - expected \ | ||
'application/webc' or 'application/octet-stream', got {ty}" | ||
); | ||
} | ||
|
||
let mut body = res.bytes_stream(); | ||
|
||
while let Some(res) = body.next().await { | ||
let chunk = res.context("could not read response body")?; | ||
// Yes, we are mixing async and sync code here, but since this is | ||
// a top-level command, this can't interfere with other tasks. | ||
file.write_all(&chunk) | ||
.await | ||
.context("could not write to temporary file")?; | ||
} | ||
|
||
file.sync_all() | ||
.await | ||
.context("could not sync temporary file")?; | ||
std::mem::drop(file); | ||
|
||
if self.validate { | ||
webc::compat::Container::from_disk(&tmp_path) | ||
.context("could not parse downloaded file as a package - invalid download?")?; | ||
} | ||
|
||
std::fs::rename(&tmp_path, &self.out_path) | ||
.context("could not move temporary file to output path")?; | ||
|
||
Ok(()) | ||
} | ||
} | ||
|
||
#[cfg(test)] | ||
mod tests { | ||
use wasmer_registry::wasmer_env::WASMER_DIR; | ||
|
||
use super::*; | ||
|
||
/// Download a package from the dev registry. | ||
#[test] | ||
fn test_cmd_package_download() { | ||
let dir = tempfile::tempdir().unwrap(); | ||
|
||
let cmd = CmdPackageDownload { | ||
env: WasmerEnv::new(WASMER_DIR.clone(), Some("wasmer.wtf".into()), None, None), | ||
validate: true, | ||
out_path: dir.path().join("hello.webc"), | ||
package: "wasmer/[email protected]".parse().unwrap(), | ||
}; | ||
|
||
cmd.execute().unwrap(); | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,11 @@ | ||
mod download; | ||
pub use download::CmdPackageDownload; | ||
|
||
/// Package related commands. | ||
#[derive(clap::Subcommand, Debug)] | ||
// Allowing missing_docs because the comment would override the doc comment on | ||
// the command struct. | ||
#[allow(missing_docs)] | ||
pub enum CmdPackage { | ||
Download(CmdPackageDownload), | ||
} |
Empty file.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters