use std::path::{Path, PathBuf};
use anyhow::{Context, Result};
pub fn store_dir() -> Result<PathBuf> {
let base = dirs::data_dir().context("could not determine data directory")?;
let dir = base.join("modelc").join("models");
std::fs::create_dir_all(&dir).context("failed to create model store directory")?;
Ok(dir)
}
pub fn search_models(query: &str) -> Result<Vec<InstalledModel>> {
let q = query.to_lowercase();
let all = list_models()?;
let filtered: Vec<InstalledModel> = all
.into_iter()
.filter(|m| {
m.name.to_lowercase().contains(&q)
|| m.architecture
.as_ref()
.map(|a| a.to_lowercase().contains(&q))
.unwrap_or(false)
})
.collect();
Ok(filtered)
}
pub fn list_models() -> Result<Vec<InstalledModel>> {
let dir = store_dir()?;
let mut models = Vec::new();
for entry in std::fs::read_dir(&dir).context("failed to read store directory")? {
let entry = entry.context("failed to read directory entry")?;
let path = entry.path();
if path.extension().and_then(|s| s.to_str()) == Some("modelc") {
let name = path
.file_stem()
.and_then(|s| s.to_str())
.unwrap_or("unknown")
.to_string();
let size = std::fs::metadata(&path)
.map(|m| m.len())
.unwrap_or(0);
let mut architecture = None;
let mut params = None;
let mut compressed = false;
if let Ok(header) = crate::pack::read_header(&path) {
architecture = Some(header.architecture);
let p: usize = header.tensors.iter().map(|t| {
t.shape.iter().product::<usize>()
}).sum();
params = Some(p);
}
if let Ok(meta) = std::fs::metadata(&path) {
if meta.len() > 14 {
if let Ok(mut file) = std::fs::File::open(&path) {
use std::io::{Read, Seek};
let _ = file.seek(std::io::SeekFrom::Start(10));
let mut flags_bytes = [0u8; 4];
if file.read_exact(&mut flags_bytes).is_ok() {
let flags = u32::from_le_bytes(flags_bytes);
compressed = flags & 1 != 0;
}
}
}
}
models.push(InstalledModel {
name,
path,
size_bytes: size,
architecture,
params,
compressed,
});
}
}
models.sort_by(|a, b| a.name.cmp(&b.name));
Ok(models)
}
pub fn resolve_model_path(input: &str) -> Result<PathBuf> {
let path = Path::new(input);
if path.is_file() {
return Ok(path.to_path_buf());
}
let dir = store_dir()?;
let candidate = dir.join(format!("{}.modelc", input));
if candidate.is_file() {
return Ok(candidate);
}
anyhow::bail!("model not found: {} (searched local path and store)", input)
}
pub fn install(source: &Path, name: &str) -> Result<PathBuf> {
let dir = store_dir()?;
let dest = dir.join(format!("{}.modelc", name));
std::fs::copy(source, &dest)
.with_context(|| format!("failed to copy {:?} to {:?}", source, dest))?;
Ok(dest)
}
pub fn download(url: &str, name: &str) -> Result<PathBuf> {
let dir = store_dir()?;
let dest = dir.join(format!("{}.modelc", name));
let mut body = ureq::get(url)
.call()
.map_err(|e| anyhow::anyhow!("download failed: {}", e))?
.into_body();
let mut reader = body.as_reader();
let mut file = std::fs::File::create(&dest)
.with_context(|| format!("failed to create {:?}", dest))?;
std::io::copy(&mut reader, &mut file)
.with_context(|| format!("failed to write downloaded data to {:?}", dest))?;
Ok(dest)
}
pub fn list_versions(name: &str) -> Result<Vec<(u32, PathBuf)>> {
let dir = store_dir()?;
let mut versions = Vec::new();
let prefix = format!("{}.v", name);
for entry in std::fs::read_dir(&dir).context("failed to read store directory")? {
let entry = entry.context("failed to read directory entry")?;
let path = entry.path();
if let Some(stem) = path.file_stem().and_then(|s| s.to_str()) {
if let Some(tail) = stem.strip_prefix(&prefix) {
if let Some(ver_str) = tail.split('.').next() {
if let Ok(ver) = ver_str.parse::<u32>() {
versions.push((ver, path));
}
}
}
}
}
versions.sort_by_key(|(v, _)| *v);
Ok(versions)
}
pub fn switch_version(name: &str, version: u32) -> Result<PathBuf> {
let dir = store_dir()?;
let source = dir.join(format!("{}.v{}.modelc", name, version));
if !source.is_file() {
anyhow::bail!("version {} of '{}' not found at {:?}", version, name, source);
}
let dest = dir.join(format!("{}.modelc", name));
std::fs::copy(&source, &dest)
.with_context(|| format!("failed to copy {:?} to {:?}", source, dest))?;
Ok(dest)
}
#[derive(Debug)]
pub struct InstalledModel {
pub name: String,
pub path: PathBuf,
pub size_bytes: u64,
pub architecture: Option<String>,
pub params: Option<usize>,
pub compressed: bool,
}