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 #[clap(short = 'p', long = "port", default_value = DEFAULT_WASH_UI_PORT)]
23 pub port: u16,
24
25 #[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 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}