wasmer_cli/commands/package/common/
mod.rs

1use 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
25// HACK: We want to invalidate the cache used for GraphQL queries so
26// the current user sees the results of publishing immediately. There
27// are cleaner ways to achieve this, but for now we're just going to
28// clear out the whole GraphQL query cache.
29// See https://github.com/wasmerio/wasmer/pull/3983 for more
30pub(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
37// Upload a package to a signed url.
38pub(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    /* XXX: If the package is large this line may result in
85     * a surge in memory use.
86     *
87     * In the future, we might want a way to stream bytes
88     * from the webc instead of a complete in-memory
89     * representation.
90     */
91    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
141/// Read and return a manifest given a path.
142///
143// The difference with the `load_package_manifest` is that
144// this function returns an error if no manifest is found.
145pub(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    // Our special cases..
201    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(&registry)?, "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        // Upload should succeed
241        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}