use anyhow::{Context, Result};
use flate2::read::GzDecoder;
use mecha10_cli_core::paths::urls::mecha10_api_base_url;
use serde::Deserialize;
use std::fs;
use std::path::{Path, PathBuf};
use tar::Archive;
#[derive(Debug, Deserialize)]
struct VersionResponse {
version: String,
}
pub struct TemplateDownloadService {
cache_dir: PathBuf,
}
impl TemplateDownloadService {
pub fn new() -> Self {
let cache_dir = dirs::home_dir()
.unwrap_or_else(|| PathBuf::from("."))
.join(".mecha10")
.join("templates");
Self { cache_dir }
}
#[allow(dead_code)]
pub fn with_cache_dir(cache_dir: PathBuf) -> Self {
Self { cache_dir }
}
pub async fn get_latest_version(&self) -> Result<String> {
let url = format!("{}/downloads/templates/version", mecha10_api_base_url());
let client = Self::build_client()?;
let response = client
.get(&url)
.send()
.await
.context("Failed to fetch latest templates version from mecha10-api")?;
if !response.status().is_success() {
anyhow::bail!(
"Failed to get latest templates version: HTTP {}\nURL: {}",
response.status(),
url
);
}
let parsed: VersionResponse = response
.json()
.await
.context("Failed to parse version response from mecha10-api")?;
Ok(parsed.version)
}
fn build_client() -> Result<reqwest::Client> {
reqwest::Client::builder()
.user_agent("mecha10-cli")
.build()
.context("Failed to build HTTP client")
}
pub fn get_cached_path(&self, version: &str) -> PathBuf {
self.cache_dir.join(format!("v{}", version))
}
#[allow(dead_code)]
pub fn is_cached(&self, version: &str) -> bool {
let cache_path = self.get_cached_path(version);
cache_path.exists() && cache_path.join("templates").exists()
}
pub async fn ensure_templates(&self, version: &str) -> Result<PathBuf> {
let cache_path = self.get_cached_path(version);
let templates_path = cache_path.join("templates");
if templates_path.exists() {
tracing::debug!("Using cached templates v{}", version);
return Ok(templates_path);
}
tracing::info!("Downloading templates v{}...", version);
self.download_templates(version, &cache_path).await?;
Ok(templates_path)
}
async fn download_templates(&self, version: &str, cache_path: &Path) -> Result<()> {
let url = format!("{}/downloads/templates?version={}", mecha10_api_base_url(), version);
let client = Self::build_client()?;
let response = client
.get(&url)
.send()
.await
.context("Failed to connect to mecha10-api for templates")?;
if !response.status().is_success() {
anyhow::bail!("Failed to download templates: HTTP {}\nURL: {}", response.status(), url);
}
let bytes = response.bytes().await.context("Failed to read template archive")?;
fs::create_dir_all(cache_path).context("Failed to create template cache directory")?;
let decoder = GzDecoder::new(&bytes[..]);
let mut archive = Archive::new(decoder);
archive
.unpack(cache_path)
.context("Failed to extract template archive")?;
tracing::info!("Templates v{} cached at {:?}", version, cache_path);
Ok(())
}
#[allow(dead_code)]
pub fn read_template(&self, version: &str, relative_path: &str) -> Result<String> {
let templates_path = self.get_cached_path(version).join("templates");
let file_path = templates_path.join(relative_path);
fs::read_to_string(&file_path).with_context(|| format!("Failed to read template: {:?}", file_path))
}
#[allow(dead_code)]
pub fn read_template_bytes(&self, version: &str, relative_path: &str) -> Result<Vec<u8>> {
let templates_path = self.get_cached_path(version).join("templates");
let file_path = templates_path.join(relative_path);
fs::read(&file_path).with_context(|| format!("Failed to read template: {:?}", file_path))
}
#[allow(dead_code)]
pub fn list_template_dir(&self, version: &str, relative_path: &str) -> Result<Vec<PathBuf>> {
let templates_path = self.get_cached_path(version).join("templates");
let dir_path = templates_path.join(relative_path);
if !dir_path.exists() {
return Ok(Vec::new());
}
let mut files = Vec::new();
for entry in fs::read_dir(&dir_path)? {
let entry = entry?;
files.push(entry.path());
}
Ok(files)
}
pub fn copy_template_dir(&self, version: &str, template_relative_path: &str, dest_path: &Path) -> Result<()> {
let templates_path = self.get_cached_path(version).join("templates");
let src_path = templates_path.join(template_relative_path);
if !src_path.exists() {
tracing::debug!("Template directory not found: {:?}", src_path);
return Ok(());
}
copy_dir_recursive(&src_path, dest_path)?;
Ok(())
}
pub fn copy_template_file(&self, version: &str, template_relative_path: &str, dest_path: &Path) -> Result<()> {
let templates_path = self.get_cached_path(version).join("templates");
let src_path = templates_path.join(template_relative_path);
if !src_path.exists() {
tracing::debug!("Template file not found: {:?}", src_path);
return Ok(());
}
if let Some(parent) = dest_path.parent() {
fs::create_dir_all(parent)?;
}
fs::copy(&src_path, dest_path)?;
Ok(())
}
}
impl Default for TemplateDownloadService {
fn default() -> Self {
Self::new()
}
}
fn copy_dir_recursive(src: &Path, dst: &Path) -> Result<()> {
fs::create_dir_all(dst)?;
for entry in fs::read_dir(src)? {
let entry = entry?;
let src_path = entry.path();
let dst_path = dst.join(entry.file_name());
if src_path.is_dir() {
copy_dir_recursive(&src_path, &dst_path)?;
} else {
fs::copy(&src_path, &dst_path)?;
}
}
Ok(())
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_cache_path() {
let service = TemplateDownloadService::with_cache_dir(PathBuf::from("/tmp/test"));
assert_eq!(service.get_cached_path("0.1.14"), PathBuf::from("/tmp/test/v0.1.14"));
}
}