wasmer_cli/commands/package/common/
mod.rs1use crate::{
2 commands::{AsyncCliCommand, Login},
3 config::WasmerEnv,
4 utils::load_package_manifest,
5};
6use colored::Colorize;
7use dialoguer::Confirm;
8use indicatif::{ProgressBar, ProgressStyle};
9use reqwest::Body;
10use std::path::{Path, PathBuf};
11use wasmer_backend_api::{WasmerClient, query::UploadMethod};
12use wasmer_config::package::{Manifest, NamedPackageIdent, PackageHash};
13use wasmer_package::package::Package;
14
15pub mod macros;
16pub mod wait;
17
18pub(super) fn on_error(e: anyhow::Error) -> anyhow::Error {
19 #[cfg(feature = "telemetry")]
20 sentry::integrations::anyhow::capture_anyhow(&e);
21
22 e
23}
24
25pub(super) fn invalidate_graphql_query_cache(cache_dir: &Path) -> Result<(), anyhow::Error> {
31 let cache_dir = cache_dir.join("queries");
32 std::fs::remove_dir_all(cache_dir)?;
33
34 Ok(())
35}
36
37pub(super) async fn upload(
39 client: &WasmerClient,
40 hash: &PackageHash,
41 timeout: humantime::Duration,
42 package: &Package,
43 pb: ProgressBar,
44 proxy: Option<reqwest::Proxy>,
45) -> anyhow::Result<String> {
46 let hash_str = hash.to_string();
47 let hash_str = hash_str.trim_start_matches("sha256:");
48
49 let session_uri = {
50 let default_timeout_secs = Some(60 * 30);
51 let q = wasmer_backend_api::query::get_signed_url_for_package_upload(
52 client,
53 default_timeout_secs,
54 Some(hash_str),
55 None,
56 None,
57 Some(UploadMethod::R2),
58 );
59
60 match q.await? {
61 Some(u) => u.url,
62 None => anyhow::bail!(
63 "The backend did not provide a valid signed URL to upload the package"
64 ),
65 }
66 };
67
68 tracing::info!("signed url is: {session_uri}");
69
70 let client = {
71 let builder = reqwest::Client::builder()
72 .default_headers(reqwest::header::HeaderMap::default())
73 .timeout(timeout.into());
74
75 let builder = if let Some(proxy) = proxy {
76 builder.proxy(proxy)
77 } else {
78 builder
79 };
80
81 builder.build().unwrap()
82 };
83
84 let bytes = package.serialize()?;
92
93 let total_bytes = bytes.len();
94 pb.set_length(total_bytes.try_into().unwrap());
95 pb.set_style(ProgressStyle::with_template("{spinner:.yellow} [{elapsed_precise}] [{bar:.white}] {bytes}/{total_bytes} ({bytes_per_sec}, {eta})")
96 .unwrap()
97 .progress_chars("█▉▊▋▌▍▎▏ ")
98 .tick_strings(&["✶", "✸", "✹", "✺", "✹", "✷", "✶"]));
99 tracing::info!("webc is {total_bytes} bytes long");
100
101 let chunk_size = 8 * 1024;
102
103 let stream = futures::stream::unfold(0, move |offset| {
104 let pb = pb.clone();
105 let bytes = bytes.clone();
106 async move {
107 if offset >= total_bytes {
108 return None;
109 }
110
111 let start = offset;
112
113 let end = if (start + chunk_size) >= total_bytes {
114 total_bytes
115 } else {
116 start + chunk_size
117 };
118
119 let n = end - start;
120 let next_chunk = bytes.slice(start..end);
121 pb.inc(n as u64);
122
123 Some((Ok::<_, std::io::Error>(next_chunk), offset + n))
124 }
125 });
126
127 let res = client
128 .put(&session_uri)
129 .header(reqwest::header::CONTENT_TYPE, "application/octet-stream")
130 .header(reqwest::header::CONTENT_LENGTH, format!("{total_bytes}"))
131 .body(Body::wrap_stream(stream));
132
133 res.send()
134 .await
135 .map(|response| response.error_for_status())
136 .map_err(|e| anyhow::anyhow!("error uploading package to {session_uri}: {e}"))??;
137
138 Ok(session_uri)
139}
140
141pub(super) fn get_manifest(path: &Path) -> anyhow::Result<(PathBuf, Manifest)> {
146 load_package_manifest(path).and_then(|j| {
147 j.ok_or_else(|| anyhow::anyhow!("No valid manifest found in path '{}'", path.display()))
148 })
149}
150
151pub(super) async fn login_user(
152 env: &WasmerEnv,
153 interactive: bool,
154 msg: &str,
155) -> anyhow::Result<WasmerClient> {
156 if let Ok(client) = env.client() {
157 return Ok(client);
158 }
159
160 let theme = dialoguer::theme::ColorfulTheme::default();
161
162 if env.token().is_none() {
163 if interactive {
164 eprintln!(
165 "{}: You need to be logged in to {msg}.",
166 "WARN".yellow().bold()
167 );
168
169 if Confirm::with_theme(&theme)
170 .with_prompt("Do you want to login now?")
171 .interact()?
172 {
173 Login {
174 no_browser: false,
175 wasmer_dir: env.dir().to_path_buf(),
176 cache_dir: env.cache_dir().to_path_buf(),
177 token: None,
178 registry: env.registry.clone(),
179 }
180 .run_async()
181 .await?;
182 } else {
183 anyhow::bail!("Stopping the flow as the user is not logged in.")
184 }
185 } else {
186 let bin_name = self::macros::bin_name!();
187 eprintln!(
188 "You are not logged in. Use the `--token` flag or log in (use `{bin_name} login`) to {msg}."
189 );
190 anyhow::bail!("Stopping execution as the user is not logged in.")
191 }
192 }
193
194 env.client()
195}
196
197pub(super) fn make_package_url(client: &WasmerClient, pkg: &NamedPackageIdent) -> String {
198 let host = client.graphql_endpoint().domain().unwrap_or("wasmer.io");
199
200 let host = match host {
202 _ if host.contains("wasmer.wtf") => "wasmer.wtf",
203 _ if host.contains("wasmer.io") => "wasmer.io",
204 _ => host,
205 };
206
207 format!(
208 "https://{host}/{}@{}",
209 pkg.full_name(),
210 pkg.version_or_default().to_string().replace('=', "")
211 )
212}
213
214#[cfg(test)]
215mod tests {
216 use super::*;
217 use anyhow::Context;
218 use humantime::Duration as HumanDuration;
219 use indicatif::ProgressBar;
220 use sha2::{Digest, Sha256};
221 use url::Url;
222
223 #[tokio::test]
224 #[ignore = "Requires WASMER_REGISTRY_URL/WASMER_TOKEN"]
225 async fn test_upload_package_r2() -> anyhow::Result<()> {
226 let registry = std::env::var("WASMER_REGISTRY_URL")
227 .context("set WASMER_REGISTRY_URL to point at the registry GraphQL endpoint")?;
228 let token = std::env::var("WASMER_TOKEN")
229 .context("set WASMER_TOKEN for the registry under test")?;
230 let client = WasmerClient::new(Url::parse(®istry)?, "wasmer-cli-upload-test")?
231 .with_auth_token(token);
232 let pkg_path = PathBuf::from(env!("CARGO_MANIFEST_DIR"))
233 .join("../../tests/old-tar-gz/cowsay-0.3.0.tar.gz");
234 let package = Package::from_tarball_file(&pkg_path)?;
235 let bytes = package.serialize()?;
236 let hash_bytes: [u8; 32] = Sha256::digest(&bytes).into();
237 let hash = PackageHash::from_sha256_bytes(hash_bytes);
238 let pb = ProgressBar::hidden();
239
240 let upload_url = upload(
242 &client,
243 &hash,
244 HumanDuration::from(std::time::Duration::from_secs(300)),
245 &package,
246 pb,
247 None,
248 )
249 .await?;
250 assert!(
251 upload_url.starts_with("http"),
252 "upload returned non-url: {upload_url}"
253 );
254 Ok(())
255 }
256}