use crate::paths;
use anyhow::{Context, Result};
use indicatif::{ProgressBar, ProgressStyle};
use serde::Deserialize;
use std::path::PathBuf;
pub struct SimulationAssetsService {
cache_dir: PathBuf,
}
#[derive(Debug, Deserialize)]
struct VersionResponse {
version: String,
}
#[allow(dead_code)]
impl SimulationAssetsService {
pub fn new() -> Self {
let cache_dir = paths::user::mecha10_dir();
Self { cache_dir }
}
pub fn cache_dir(&self) -> &PathBuf {
&self.cache_dir
}
pub fn current_assets_path(&self) -> Option<PathBuf> {
let current = paths::user::simulation_current();
if current.exists() {
std::fs::canonicalize(¤t).ok()
} else {
None
}
}
pub fn models_path(&self) -> Option<PathBuf> {
self.current_assets_path()
.map(|p| p.join("models"))
.filter(|p| p.exists())
}
pub fn environments_path(&self) -> Option<PathBuf> {
self.current_assets_path()
.map(|p| p.join("environments"))
.filter(|p| p.exists())
}
pub fn is_installed(&self) -> bool {
self.models_path().is_some() || self.environments_path().is_some()
}
pub fn installed_version(&self) -> Option<String> {
let version_file = paths::user::simulation_dir().join("version");
std::fs::read_to_string(version_file).ok()
}
pub async fn get_latest_version(&self) -> Result<String> {
let url = format!("{}/downloads/simulation/version", paths::urls::mecha10_api_base_url());
let client = Self::build_client()?;
let response = client
.get(&url)
.send()
.await
.context("Failed to fetch latest simulation assets version from mecha10-api")?;
if !response.status().is_success() {
return Err(anyhow::anyhow!(
"Failed to get latest version: HTTP {}\nURL: {}",
response.status(),
url
));
}
let parsed: VersionResponse = response
.json()
.await
.context("Failed to parse version response from mecha10-api")?;
Ok(parsed.version)
}
pub async fn ensure_assets(&self, version: &str) -> Result<PathBuf> {
let version_dir = paths::user::simulation_dir().join(version);
if version_dir.exists() {
tracing::debug!("Using cached simulation assets {}", version);
return Ok(version_dir);
}
println!("📦 Downloading simulation assets {}...", version);
self.download_version(version).await
}
fn build_client() -> Result<reqwest::Client> {
reqwest::Client::builder()
.user_agent("mecha10-cli")
.build()
.context("Failed to build HTTP client")
}
async fn download_version(&self, version: &str) -> Result<PathBuf> {
let url = format!(
"{}/downloads/simulation?version={}",
paths::urls::mecha10_api_base_url(),
version
);
tracing::info!("Downloading sim assets from mecha10-api: {}", url);
let client = Self::build_client()?;
let response = client
.get(&url)
.send()
.await
.context("Failed to connect to mecha10-api for simulation assets")?;
if !response.status().is_success() {
return Err(anyhow::anyhow!(
"Simulation asset not found: HTTP {}\n\
URL: {}\n\n\
Alternative: Set MECHA10_FRAMEWORK_PATH to your local mecha10 clone:\n\
export MECHA10_FRAMEWORK_PATH=/path/to/mecha10",
response.status(),
url
));
}
let size = response.content_length().unwrap_or(0);
println!(" Release: {}", version);
if size > 0 {
println!(" Size: {:.1} MB", size as f64 / 1024.0 / 1024.0);
}
self.download_and_extract(response, version, size).await
}
pub async fn download_latest(&self) -> Result<PathBuf> {
println!(" Checking latest release...");
let version = self.get_latest_version().await?;
self.download_version(&version).await
}
async fn download_and_extract(&self, response: reqwest::Response, version: &str, size: u64) -> Result<PathBuf> {
let pb = ProgressBar::new(size);
pb.set_style(
ProgressStyle::default_bar()
.template("{spinner:.green} [{bar:40.cyan/blue}] {bytes}/{total_bytes} ({eta})")
.unwrap()
.progress_chars("#>-"),
);
println!(" Downloading...");
let temp_dir = tempfile::tempdir()?;
let temp_file = temp_dir.path().join("simulation.tar.gz");
let mut file = tokio::fs::File::create(&temp_file).await?;
let mut stream = response.bytes_stream();
use futures_util::StreamExt;
use tokio::io::AsyncWriteExt;
while let Some(chunk) = stream.next().await {
let chunk = chunk.context("Error downloading chunk")?;
file.write_all(&chunk).await?;
pb.inc(chunk.len() as u64);
}
file.flush().await?;
pb.finish_with_message("Download complete");
println!(" Extracting...");
let simulation_dir = paths::user::simulation_dir();
let version_dir = simulation_dir.join(version);
tokio::fs::create_dir_all(&version_dir).await?;
let tar_gz = std::fs::File::open(&temp_file)?;
let tar = flate2::read::GzDecoder::new(tar_gz);
let mut archive = tar::Archive::new(tar);
archive.unpack(&version_dir)?;
let current_link = simulation_dir.join("current");
if current_link.exists() {
tokio::fs::remove_file(¤t_link).await.ok();
}
#[cfg(unix)]
{
std::os::unix::fs::symlink(&version_dir, ¤t_link)?;
}
#[cfg(windows)]
{
std::os::windows::fs::symlink_dir(&version_dir, ¤t_link)?;
}
let version_file = simulation_dir.join("version");
tokio::fs::write(&version_file, version).await?;
println!("✅ Simulation assets installed to {:?}", version_dir);
Ok(version_dir)
}
pub async fn remove(&self) -> Result<()> {
let simulation_dir = paths::user::simulation_dir();
if simulation_dir.exists() {
tokio::fs::remove_dir_all(&simulation_dir).await?;
println!("✅ Simulation assets removed");
} else {
println!("No simulation assets installed");
}
Ok(())
}
pub async fn update(&self) -> Result<PathBuf> {
self.remove().await.ok();
self.download_latest().await
}
}
impl Default for SimulationAssetsService {
fn default() -> Self {
Self::new()
}
}