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::{
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
28// HACK: We want to invalidate the cache used for GraphQL queries so
29// the current user sees the results of publishing immediately. There
30// are cleaner ways to achieve this, but for now we're just going to
31// clear out the whole GraphQL query cache.
32// See https://github.com/wasmerio/wasmer/pull/3983 for more
33pub(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
40// Upload a package to a signed url.
41pub(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    /* XXX: If the package is large this line may result in
120     * a surge in memory use.
121     *
122     * In the future, we might want a way to stream bytes
123     * from the webc instead of a complete in-memory
124     * representation.
125     */
126    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
176/// Read and return a manifest given a path.
177///
178// The difference with the `load_package_manifest` is that
179// this function returns an error if no manifest is found.
180pub(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    // Our special cases..
236    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}