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::{
11 collections::BTreeMap,
12 path::{Path, PathBuf},
13};
14use wasmer_backend_api::WasmerClient;
15use wasmer_config::package::{Manifest, NamedPackageIdent, PackageHash};
16use wasmer_package::package::Package;
17
18pub mod macros;
19pub mod wait;
20
21pub(super) fn on_error(e: anyhow::Error) -> anyhow::Error {
22 #[cfg(feature = "telemetry")]
23 sentry::integrations::anyhow::capture_anyhow(&e);
24
25 e
26}
27
28pub(super) fn invalidate_graphql_query_cache(cache_dir: &Path) -> Result<(), anyhow::Error> {
34 let cache_dir = cache_dir.join("queries");
35 std::fs::remove_dir_all(cache_dir)?;
36
37 Ok(())
38}
39
40pub(super) async fn upload(
42 client: &WasmerClient,
43 hash: &PackageHash,
44 timeout: humantime::Duration,
45 package: &Package,
46 pb: ProgressBar,
47 proxy: Option<reqwest::Proxy>,
48) -> anyhow::Result<String> {
49 let hash_str = hash.to_string();
50 let hash_str = hash_str.trim_start_matches("sha256:");
51
52 let session_uri = {
53 let default_timeout_secs = Some(60 * 30);
54 let q = wasmer_backend_api::query::get_signed_url_for_package_upload(
55 client,
56 default_timeout_secs,
57 Some(hash_str),
58 None,
59 None,
60 );
61
62 match q.await? {
63 Some(u) => u.url,
64 None => anyhow::bail!(
65 "The backend did not provide a valid signed URL to upload the package"
66 ),
67 }
68 };
69
70 tracing::info!("signed url is: {session_uri}");
71
72 let client = {
73 let builder = reqwest::Client::builder()
74 .default_headers(reqwest::header::HeaderMap::default())
75 .timeout(timeout.into());
76
77 let builder = if let Some(proxy) = proxy {
78 builder.proxy(proxy)
79 } else {
80 builder
81 };
82
83 builder.build().unwrap()
84 };
85
86 let res = client
87 .post(&session_uri)
88 .header(reqwest::header::CONTENT_LENGTH, "0")
89 .header(reqwest::header::CONTENT_TYPE, "application/octet-stream")
90 .header("x-goog-resumable", "start");
91
92 let result = res.send().await?;
93
94 if result.status() != reqwest::StatusCode::from_u16(201).unwrap() {
95 return Err(anyhow::anyhow!(
96 "Uploading package failed: got HTTP {:?} when uploading",
97 result.status()
98 ));
99 }
100
101 let headers = result
102 .headers()
103 .into_iter()
104 .filter_map(|(k, v)| {
105 let k = k.to_string();
106 let v = v.to_str().ok()?.to_string();
107 Some((k.to_lowercase(), v))
108 })
109 .collect::<BTreeMap<_, _>>();
110
111 let session_uri = headers
112 .get("location")
113 .ok_or_else(|| {
114 anyhow::anyhow!("The upload server did not provide the upload URL correctly")
115 })?
116 .clone();
117
118 tracing::info!("session uri is: {session_uri}");
119 let bytes = package.serialize()?;
127
128 let total_bytes = bytes.len();
129 pb.set_length(total_bytes.try_into().unwrap());
130 pb.set_style(ProgressStyle::with_template("{spinner:.yellow} [{elapsed_precise}] [{bar:.white}] {bytes}/{total_bytes} ({bytes_per_sec}, {eta})")
131 .unwrap()
132 .progress_chars("█▉▊▋▌▍▎▏ ")
133 .tick_strings(&["✶", "✸", "✹", "✺", "✹", "✷", "✶"]));
134 tracing::info!("webc is {total_bytes} bytes long");
135
136 let chunk_size = 8 * 1024;
137
138 let stream = futures::stream::unfold(0, move |offset| {
139 let pb = pb.clone();
140 let bytes = bytes.clone();
141 async move {
142 if offset >= total_bytes {
143 return None;
144 }
145
146 let start = offset;
147
148 let end = if (start + chunk_size) >= total_bytes {
149 total_bytes
150 } else {
151 start + chunk_size
152 };
153
154 let n = end - start;
155 let next_chunk = bytes.slice(start..end);
156 pb.inc(n as u64);
157
158 Some((Ok::<_, std::io::Error>(next_chunk), offset + n))
159 }
160 });
161
162 let res = client
163 .put(&session_uri)
164 .header(reqwest::header::CONTENT_TYPE, "application/octet-stream")
165 .header(reqwest::header::CONTENT_LENGTH, format!("{total_bytes}"))
166 .body(Body::wrap_stream(stream));
167
168 res.send()
169 .await
170 .map(|response| response.error_for_status())
171 .map_err(|e| anyhow::anyhow!("error uploading package to {session_uri}: {e}"))??;
172
173 Ok(session_uri)
174}
175
176pub(super) fn get_manifest(path: &Path) -> anyhow::Result<(PathBuf, Manifest)> {
181 load_package_manifest(path).and_then(|j| {
182 j.ok_or_else(|| anyhow::anyhow!("No valid manifest found in path '{}'", path.display()))
183 })
184}
185
186pub(super) async fn login_user(
187 env: &WasmerEnv,
188 interactive: bool,
189 msg: &str,
190) -> anyhow::Result<WasmerClient> {
191 if let Ok(client) = env.client() {
192 return Ok(client);
193 }
194
195 let theme = dialoguer::theme::ColorfulTheme::default();
196
197 if env.token().is_none() {
198 if interactive {
199 eprintln!(
200 "{}: You need to be logged in to {msg}.",
201 "WARN".yellow().bold()
202 );
203
204 if Confirm::with_theme(&theme)
205 .with_prompt("Do you want to login now?")
206 .interact()?
207 {
208 Login {
209 no_browser: false,
210 wasmer_dir: env.dir().to_path_buf(),
211 cache_dir: env.cache_dir().to_path_buf(),
212 token: None,
213 registry: env.registry.clone(),
214 }
215 .run_async()
216 .await?;
217 } else {
218 anyhow::bail!("Stopping the flow as the user is not logged in.")
219 }
220 } else {
221 let bin_name = self::macros::bin_name!();
222 eprintln!(
223 "You are not logged in. Use the `--token` flag or log in (use `{bin_name} login`) to {msg}."
224 );
225 anyhow::bail!("Stopping execution as the user is not logged in.")
226 }
227 }
228
229 env.client()
230}
231
232pub(super) fn make_package_url(client: &WasmerClient, pkg: &NamedPackageIdent) -> String {
233 let host = client.graphql_endpoint().domain().unwrap_or("wasmer.io");
234
235 let host = match host {
237 _ if host.contains("wasmer.wtf") => "wasmer.wtf",
238 _ if host.contains("wasmer.io") => "wasmer.io",
239 _ => host,
240 };
241
242 format!(
243 "https://{host}/{}@{}",
244 pkg.full_name(),
245 pkg.version_or_default().to_string().replace('=', "")
246 )
247}