use crate::download::git_lfs::{download_hf_model_clean, CleanDownloadConfig};
use anyhow::Result;
use std::fs::{self, OpenOptions};
use std::io::Write;
use std::path::{Path, PathBuf};
use std::thread;
use std::time::{Duration, Instant};
pub fn download_model(model_id: &str, verbose: bool) -> Result<PathBuf> {
let cache_base = dirs::cache_dir()
.ok_or_else(|| anyhow::Error::msg("Cannot determine cache directory"))?
.join("candle-coreml");
std::fs::create_dir_all(&cache_base)?;
let config = CleanDownloadConfig::for_hf_model(model_id, &cache_base)
.with_verbose(verbose)
.with_keep_git(false);
let model_cache_name = model_id.replace('/', "--");
let lock_path = cache_base.join(format!(".lock-{model_cache_name}"));
let start = Instant::now();
let timeout = Duration::from_secs(60);
let backoff = Duration::from_millis(200);
loop {
match OpenOptions::new()
.write(true)
.create_new(true)
.open(&lock_path)
{
Ok(mut f) => {
let _ = writeln!(f, "pid:{}", std::process::id());
break;
}
Err(_) => {
if start.elapsed() > timeout {
let _ = fs::remove_file(&lock_path);
match OpenOptions::new()
.write(true)
.create_new(true)
.open(&lock_path)
{
Ok(mut f) => {
let _ = writeln!(f, "pid:{}", std::process::id());
break;
}
Err(e) => {
return Err(anyhow::anyhow!(
"Timed out waiting for download lock for model '{}': {}",
model_id,
e
));
}
}
}
thread::sleep(backoff);
}
}
}
let result = download_hf_model_clean(&config);
let _ = fs::remove_file(&lock_path);
result
}
pub fn download_model_to(model_id: &str, target_dir: &Path, verbose: bool) -> Result<PathBuf> {
let config = CleanDownloadConfig {
model_id: model_id.to_string(),
target_dir: target_dir.to_path_buf(),
verbose,
keep_git_dir: false,
};
download_hf_model_clean(&config)
}
pub fn get_cached_model_path(model_id: &str) -> Option<PathBuf> {
let cache_base = dirs::cache_dir()?.join("candle-coreml");
let model_cache_name = model_id.replace('/', "--");
let model_path = cache_base.join(format!("clean-{model_cache_name}"));
if model_path.exists() && model_path.is_dir() {
Some(model_path)
} else {
None
}
}
pub fn ensure_model_downloaded(model_id: &str, verbose: bool) -> Result<PathBuf> {
if let Some(cached_path) = get_cached_model_path(model_id) {
if verbose {
println!(
"✅ Model {} already cached at: {}",
model_id,
cached_path.display()
);
}
Ok(cached_path)
} else {
if verbose {
println!("📥 Model {model_id} not cached, downloading...");
}
download_model(model_id, verbose)
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_cache_path_generation() {
let model_id = "test/model";
let expected_suffix = "candle-coreml/clean-test--model";
if let Some(cache_path) = get_cached_model_path(model_id) {
assert!(cache_path.to_string_lossy().ends_with(expected_suffix));
}
}
#[test]
fn test_model_id_normalization() {
let model_id = "microsoft/DialoGPT-medium";
let normalized = model_id.replace('/', "--");
assert_eq!(normalized, "microsoft--DialoGPT-medium");
}
}