wasmer_cli/commands/package/common/
mod.rs1use crate::{
2 commands::{AsyncCliCommand, Login},
3 config::WasmerEnv,
4 utils::load_package_manifest,
5};
6use bytes::Bytes;
7use colored::Colorize;
8use dialoguer::Confirm;
9use indicatif::{ProgressBar, ProgressStyle};
10use reqwest::Body;
11use std::path::{Path, PathBuf};
12use wasmer_backend_api::{WasmerClient, query::UploadMethod};
13use wasmer_config::package::{Manifest, NamedPackageIdent, PackageHash};
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 bytes: Bytes,
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 total_bytes = bytes.len();
85 pb.set_length(total_bytes.try_into().unwrap());
86 pb.set_style(ProgressStyle::with_template("{spinner:.yellow} [{elapsed_precise}] [{bar:.white}] {bytes}/{total_bytes} ({bytes_per_sec}, {eta})")
87 .unwrap()
88 .progress_chars("█▉▊▋▌▍▎▏ ")
89 .tick_strings(&["✶", "✸", "✹", "✺", "✹", "✷", "✶"]));
90 tracing::info!("webc is {total_bytes} bytes long");
91
92 let chunk_size = 8 * 1024;
93
94 let stream = futures::stream::unfold(0, move |offset| {
95 let pb = pb.clone();
96 let bytes = bytes.clone();
97 async move {
98 if offset >= total_bytes {
99 return None;
100 }
101
102 let start = offset;
103
104 let end = if (start + chunk_size) >= total_bytes {
105 total_bytes
106 } else {
107 start + chunk_size
108 };
109
110 let n = end - start;
111 let next_chunk = bytes.slice(start..end);
112 pb.inc(n as u64);
113
114 Some((Ok::<_, std::io::Error>(next_chunk), offset + n))
115 }
116 });
117
118 let res = client
119 .put(&session_uri)
120 .header(reqwest::header::CONTENT_TYPE, "application/octet-stream")
121 .header(reqwest::header::CONTENT_LENGTH, format!("{total_bytes}"))
122 .body(Body::wrap_stream(stream));
123
124 res.send()
125 .await
126 .map(|response| response.error_for_status())
127 .map_err(|e| anyhow::anyhow!("error uploading package to {session_uri}: {e}"))??;
128
129 Ok(session_uri)
130}
131
132pub(super) fn get_manifest(path: &Path) -> anyhow::Result<(PathBuf, Manifest)> {
137 if path.is_file() && path.extension().and_then(|s| s.to_str()) == Some("webc") {
139 return Ok((path.to_path_buf(), get_manifest_from_webc(path)?));
140 }
141
142 load_package_manifest(path).and_then(|j| {
143 j.ok_or_else(|| anyhow::anyhow!("No valid manifest found in path '{}'", path.display()))
144 })
145}
146
147fn get_manifest_from_webc(path: &Path) -> anyhow::Result<Manifest> {
149 use wasmer_package::utils::from_disk;
150
151 let container = from_disk(path)
152 .map_err(|e| anyhow::anyhow!("Failed to load webc file '{}': {}", path.display(), e))?;
153
154 let webc_manifest = container.manifest();
155
156 let mut manifest = Manifest::new_empty();
158
159 let wapm_annotation = webc_manifest
161 .wapm()
162 .map_err(|e| anyhow::anyhow!("Failed to read package annotation from webc: {e}"))?;
163
164 if let Some(wapm) = wapm_annotation {
165 let mut package = wasmer_config::package::Package::new_empty();
166 package.name = wapm.name;
167 package.version = if let Some(v) = wapm.version {
168 Some(v.parse()?)
169 } else {
170 None
171 };
172 package.description = wapm.description;
173 package.license = wapm.license;
174 package.homepage = wapm.homepage;
175 package.repository = wapm.repository;
176 package.private = wapm.private;
177 package.entrypoint = webc_manifest.entrypoint.clone();
178
179 manifest.package = Some(package);
183 } else {
184 manifest.package = Some(wasmer_config::package::Package::new_empty());
186 }
187
188 Ok(manifest)
194}
195
196pub(super) async fn login_user(
197 env: &WasmerEnv,
198 interactive: bool,
199 msg: &str,
200) -> anyhow::Result<WasmerClient> {
201 if let Ok(client) = env.client() {
202 return Ok(client);
203 }
204
205 let theme = dialoguer::theme::ColorfulTheme::default();
206
207 if env.token().is_none() {
208 if interactive {
209 eprintln!(
210 "{}: You need to be logged in to {msg}.",
211 "WARN".yellow().bold()
212 );
213
214 if Confirm::with_theme(&theme)
215 .with_prompt("Do you want to login now?")
216 .interact()?
217 {
218 Login {
219 no_browser: false,
220 wasmer_dir: env.dir().to_path_buf(),
221 cache_dir: env.cache_dir().to_path_buf(),
222 token: None,
223 registry: env.registry.clone(),
224 }
225 .run_async()
226 .await?;
227 } else {
228 anyhow::bail!("Stopping the flow as the user is not logged in.")
229 }
230 } else {
231 let bin_name = self::macros::bin_name!();
232 eprintln!(
233 "You are not logged in. Use the `--token` flag or log in (use `{bin_name} login`) to {msg}."
234 );
235 anyhow::bail!("Stopping execution as the user is not logged in.")
236 }
237 }
238
239 env.client()
240}
241
242pub(super) fn make_package_url(client: &WasmerClient, pkg: &NamedPackageIdent) -> String {
243 let host = client.graphql_endpoint().domain().unwrap_or("wasmer.io");
244
245 let host = match host {
247 _ if host.contains("wasmer.wtf") => "wasmer.wtf",
248 _ if host.contains("wasmer.io") => "wasmer.io",
249 _ => host,
250 };
251
252 format!(
253 "https://{host}/{}@{}",
254 pkg.full_name(),
255 pkg.version_or_default().to_string().replace('=', "")
256 )
257}
258
259#[cfg(test)]
260mod tests {
261 use super::*;
262 use anyhow::Context;
263 use humantime::Duration as HumanDuration;
264 use indicatif::ProgressBar;
265 use sha2::{Digest, Sha256};
266 use url::Url;
267 use wasmer_package::package::Package;
268
269 #[tokio::test]
270 #[ignore = "Requires WASMER_REGISTRY_URL/WASMER_TOKEN"]
271 async fn test_upload_package_r2() -> anyhow::Result<()> {
272 let registry = std::env::var("WASMER_REGISTRY_URL")
273 .context("set WASMER_REGISTRY_URL to point at the registry GraphQL endpoint")?;
274 let token = std::env::var("WASMER_TOKEN")
275 .context("set WASMER_TOKEN for the registry under test")?;
276 let client = WasmerClient::new(Url::parse(®istry)?, "wasmer-cli-upload-test")?
277 .with_auth_token(token);
278 let pkg_path = PathBuf::from(env!("CARGO_MANIFEST_DIR"))
279 .join("../../tests/old-tar-gz/cowsay-0.3.0.tar.gz");
280 let package = Package::from_tarball_file(&pkg_path)?;
281 let bytes = package.serialize()?;
282 let hash_bytes: [u8; 32] = Sha256::digest(&bytes).into();
283 let hash = PackageHash::from_sha256_bytes(hash_bytes);
284 let pb = ProgressBar::hidden();
285
286 let upload_url = upload(
288 &client,
289 &hash,
290 HumanDuration::from(std::time::Duration::from_secs(300)),
291 package.serialize().unwrap(),
292 pb,
293 None,
294 )
295 .await?;
296 assert!(
297 upload_url.starts_with("http"),
298 "upload returned non-url: {upload_url}"
299 );
300 Ok(())
301 }
302
303 #[test]
304 fn test_get_manifest_from_webc() -> anyhow::Result<()> {
305 use tempfile::TempDir;
306 use wasmer_package::package::Package;
307
308 let temp_dir = TempDir::new()?;
310 let pkg_dir = temp_dir.path();
311
312 std::fs::write(
314 pkg_dir.join("wasmer.toml"),
315 r#"
316[package]
317name = "test/mypackage"
318version = "0.1.0"
319description = "Test package for webc manifest extraction"
320
321[fs]
322data = "data"
323"#,
324 )?;
325
326 std::fs::create_dir(pkg_dir.join("data"))?;
328 std::fs::write(pkg_dir.join("data/test.txt"), "Hello World")?;
329
330 let pkg = Package::from_manifest(pkg_dir.join("wasmer.toml"))?;
332 let webc_bytes = pkg.serialize()?;
333
334 let webc_path = pkg_dir.join("test.webc");
336 std::fs::write(&webc_path, &webc_bytes)?;
337
338 let (path, manifest) = get_manifest(&webc_path)?;
340
341 assert_eq!(path, webc_path);
342 assert!(
343 manifest.package.is_some(),
344 "manifest.package should be present"
345 );
346
347 let package = manifest.package.unwrap();
348
349 assert_eq!(
351 package.name, None,
352 "Package name should be None in webc (stripped by Package::from_manifest)"
353 );
354 assert_eq!(
355 package.version, None,
356 "Package version should be None in webc (stripped by Package::from_manifest)"
357 );
358 assert_eq!(
359 package.description, None,
360 "Package description should be None in webc (stripped by Package::from_manifest)"
361 );
362
363 Ok(())
364 }
365}