wash_cli/ui/
mod.rs

1mod config;
2pub use config::*;
3
4use std::{io::Cursor, path::PathBuf};
5
6use anyhow::{bail, Context, Result};
7use async_compression::tokio::bufread::GzipDecoder;
8use clap::Parser;
9use tokio_tar::Archive;
10use warp::Filter;
11use wash_lib::{
12    cli::{CommandOutput, OutputKind},
13    config::downloads_dir,
14    start::get_download_client,
15};
16
17const DEFAULT_WASHBOARD_VERSION: &str = "v0.6.0";
18
19#[derive(Parser, Debug, Clone)]
20pub struct UiCommand {
21    /// Which port to run the UI on, defaults to 3030
22    #[clap(short = 'p', long = "port", default_value = DEFAULT_WASH_UI_PORT)]
23    pub port: u16,
24
25    /// Which version of the UI to run
26    #[clap(short = 'v', long = "version", default_value = DEFAULT_WASHBOARD_VERSION)]
27    pub version: String,
28}
29
30pub async fn handle_command(command: UiCommand, output_kind: OutputKind) -> Result<CommandOutput> {
31    handle_ui(command, output_kind)
32        .await
33        .map(|_| (CommandOutput::default()))
34}
35
36pub async fn handle_ui(cmd: UiCommand, _output_kind: OutputKind) -> Result<()> {
37    let washboard_path = downloads_dir()?.join("washboard");
38    let washboard_assets = ensure_washboard(&cmd.version, washboard_path).await?;
39    let static_files = warp::fs::dir(washboard_assets);
40
41    let cors = warp::cors()
42        .allow_any_origin()
43        .allow_methods(vec!["GET", "POST"])
44        .allow_headers(vec!["Content-Type"]);
45
46    eprintln!(
47        "washboard-ui@{} running on http://localhost:{}",
48        cmd.version, cmd.port
49    );
50    eprintln!("Hit CTRL-C to stop");
51
52    warp::serve(static_files.with(cors))
53        .run(([127, 0, 0, 1], cmd.port))
54        .await;
55
56    Ok(())
57}
58
59async fn ensure_washboard(version: &str, base_dir: PathBuf) -> Result<PathBuf> {
60    let install_dir = base_dir.join(version);
61
62    if tokio::fs::metadata(&install_dir).await.is_err() {
63        download_washboard(version, &install_dir).await?;
64    }
65
66    Ok(install_dir)
67}
68
69async fn download_washboard(version: &str, install_dir: &PathBuf) -> Result<()> {
70    let urls = vec![
71        format!(
72            "https://github.com/wasmCloud/typescript/releases/download/washboard-ui%40{version}/washboard.tar.gz"
73        ),
74        format!(
75            "https://github.com/wasmCloud/wasmCloud/releases/download/typescript%2Fapps%2Fwashboard-ui%2F{version}/washboard.tar.gz"
76        ),
77        format!(
78            "https://github.com/wasmCloud/wasmCloud/releases/download/washboard-ui-{version}/washboard.tar.gz"
79        ),
80    ];
81
82    let body = try_download_from_urls(&urls)
83        .await
84        .context("Failed to download washboard-ui assets")?;
85
86    eprintln!("Downloaded washboard-ui@{}", version);
87
88    // Unpack and copy to install dir
89    let cursor = Cursor::new(body);
90    let mut tarball = Archive::new(Box::new(GzipDecoder::new(cursor)));
91    tarball
92        .unpack(install_dir)
93        .await
94        .context("Failed to unpack washboard-ui assets")?;
95
96    Ok(())
97}
98
99async fn try_download_from_urls(urls: &[String]) -> Result<bytes::Bytes> {
100    let mut last_error = None;
101
102    for url in urls {
103        match try_download(url).await {
104            Ok(body) => return Ok(body),
105            Err(e) => last_error = Some(e),
106        }
107    }
108
109    Err(last_error.unwrap_or_else(|| anyhow::anyhow!("Failed to find suitable download URL")))
110}
111
112async fn try_download(url: &String) -> Result<bytes::Bytes> {
113    let resp = get_download_client()?
114        .get(url)
115        .send()
116        .await
117        .context("Failed to download washboard.tar.gz. Are you offline?")?;
118
119    if resp.status() != reqwest::StatusCode::OK {
120        bail!("Failed to download washboard.tar.gz: {}", resp.status());
121    }
122
123    resp.bytes().await.context(
124        "Failed to read bytes from washboard.tar.gz. Try deleting the download and try again.",
125    )
126}