mecha10_sim/services/
simulation_assets.rs1use crate::paths;
19use anyhow::{Context, Result};
20use indicatif::{ProgressBar, ProgressStyle};
21use serde::Deserialize;
22use std::path::PathBuf;
23
24pub struct SimulationAssetsService {
26 cache_dir: PathBuf,
28}
29
30#[derive(Debug, Deserialize)]
32struct VersionResponse {
33 version: String,
34}
35
36#[allow(dead_code)]
37impl SimulationAssetsService {
38 pub fn new() -> Self {
40 let cache_dir = paths::user::mecha10_dir();
41 Self { cache_dir }
42 }
43
44 pub fn cache_dir(&self) -> &PathBuf {
46 &self.cache_dir
47 }
48
49 pub fn current_assets_path(&self) -> Option<PathBuf> {
53 let current = paths::user::simulation_current();
54 if current.exists() {
55 std::fs::canonicalize(¤t).ok()
57 } else {
58 None
59 }
60 }
61
62 pub fn models_path(&self) -> Option<PathBuf> {
64 self.current_assets_path()
65 .map(|p| p.join("models"))
66 .filter(|p| p.exists())
67 }
68
69 pub fn environments_path(&self) -> Option<PathBuf> {
71 self.current_assets_path()
72 .map(|p| p.join("environments"))
73 .filter(|p| p.exists())
74 }
75
76 pub fn is_installed(&self) -> bool {
78 self.models_path().is_some() || self.environments_path().is_some()
79 }
80
81 pub fn installed_version(&self) -> Option<String> {
83 let version_file = paths::user::simulation_dir().join("version");
84 std::fs::read_to_string(version_file).ok()
85 }
86
87 pub async fn get_latest_version(&self) -> Result<String> {
92 let url = format!("{}/downloads/simulation/version", paths::urls::mecha10_api_base_url());
93 let client = Self::build_client()?;
94 let response = client
95 .get(&url)
96 .send()
97 .await
98 .context("Failed to fetch latest simulation assets version from mecha10-api")?;
99
100 if !response.status().is_success() {
101 return Err(anyhow::anyhow!(
102 "Failed to get latest version: HTTP {}\nURL: {}",
103 response.status(),
104 url
105 ));
106 }
107
108 let parsed: VersionResponse = response
109 .json()
110 .await
111 .context("Failed to parse version response from mecha10-api")?;
112 Ok(parsed.version)
113 }
114
115 pub async fn ensure_assets(&self, version: &str) -> Result<PathBuf> {
119 let version_dir = paths::user::simulation_dir().join(version);
120 if version_dir.exists() {
121 tracing::debug!("Using cached simulation assets {}", version);
122 return Ok(version_dir);
123 }
124
125 println!("📦 Downloading simulation assets {}...", version);
126 self.download_version(version).await
127 }
128
129 fn build_client() -> Result<reqwest::Client> {
131 reqwest::Client::builder()
132 .user_agent("mecha10-cli")
133 .build()
134 .context("Failed to build HTTP client")
135 }
136
137 async fn download_version(&self, version: &str) -> Result<PathBuf> {
139 let url = format!(
140 "{}/downloads/simulation?version={}",
141 paths::urls::mecha10_api_base_url(),
142 version
143 );
144
145 tracing::info!("Downloading sim assets from mecha10-api: {}", url);
146
147 let client = Self::build_client()?;
148 let response = client
149 .get(&url)
150 .send()
151 .await
152 .context("Failed to connect to mecha10-api for simulation assets")?;
153
154 if !response.status().is_success() {
155 return Err(anyhow::anyhow!(
156 "Simulation asset not found: HTTP {}\n\
157 URL: {}\n\n\
158 Alternative: Set MECHA10_FRAMEWORK_PATH to your local mecha10 clone:\n\
159 export MECHA10_FRAMEWORK_PATH=/path/to/mecha10",
160 response.status(),
161 url
162 ));
163 }
164
165 let size = response.content_length().unwrap_or(0);
166
167 println!(" Release: {}", version);
168 if size > 0 {
169 println!(" Size: {:.1} MB", size as f64 / 1024.0 / 1024.0);
170 }
171
172 self.download_and_extract(response, version, size).await
173 }
174
175 pub async fn download_latest(&self) -> Result<PathBuf> {
177 println!(" Checking latest release...");
178 let version = self.get_latest_version().await?;
179 self.download_version(&version).await
180 }
181
182 async fn download_and_extract(&self, response: reqwest::Response, version: &str, size: u64) -> Result<PathBuf> {
184 let pb = ProgressBar::new(size);
186 pb.set_style(
187 ProgressStyle::default_bar()
188 .template("{spinner:.green} [{bar:40.cyan/blue}] {bytes}/{total_bytes} ({eta})")
189 .unwrap()
190 .progress_chars("#>-"),
191 );
192
193 println!(" Downloading...");
194
195 let temp_dir = tempfile::tempdir()?;
197 let temp_file = temp_dir.path().join("simulation.tar.gz");
198
199 let mut file = tokio::fs::File::create(&temp_file).await?;
200 let mut stream = response.bytes_stream();
201
202 use futures_util::StreamExt;
203 use tokio::io::AsyncWriteExt;
204
205 while let Some(chunk) = stream.next().await {
206 let chunk = chunk.context("Error downloading chunk")?;
207 file.write_all(&chunk).await?;
208 pb.inc(chunk.len() as u64);
209 }
210
211 file.flush().await?;
212 pb.finish_with_message("Download complete");
213
214 println!(" Extracting...");
216
217 let simulation_dir = paths::user::simulation_dir();
218 let version_dir = simulation_dir.join(version);
219
220 tokio::fs::create_dir_all(&version_dir).await?;
222
223 let tar_gz = std::fs::File::open(&temp_file)?;
225 let tar = flate2::read::GzDecoder::new(tar_gz);
226 let mut archive = tar::Archive::new(tar);
227 archive.unpack(&version_dir)?;
228
229 let current_link = simulation_dir.join("current");
231 if current_link.exists() {
232 tokio::fs::remove_file(¤t_link).await.ok();
233 }
234
235 #[cfg(unix)]
236 {
237 std::os::unix::fs::symlink(&version_dir, ¤t_link)?;
238 }
239
240 #[cfg(windows)]
241 {
242 std::os::windows::fs::symlink_dir(&version_dir, ¤t_link)?;
243 }
244
245 let version_file = simulation_dir.join("version");
247 tokio::fs::write(&version_file, version).await?;
248
249 println!("✅ Simulation assets installed to {:?}", version_dir);
250
251 Ok(version_dir)
252 }
253
254 pub async fn remove(&self) -> Result<()> {
256 let simulation_dir = paths::user::simulation_dir();
257 if simulation_dir.exists() {
258 tokio::fs::remove_dir_all(&simulation_dir).await?;
259 println!("✅ Simulation assets removed");
260 } else {
261 println!("No simulation assets installed");
262 }
263 Ok(())
264 }
265
266 pub async fn update(&self) -> Result<PathBuf> {
268 self.remove().await.ok();
270 self.download_latest().await
271 }
272}
273
274impl Default for SimulationAssetsService {
275 fn default() -> Self {
276 Self::new()
277 }
278}