use std::path::{Path, PathBuf};
const DEFAULT_REGISTRY: &str = "registry.ollama.ai";
const DEFAULT_NAMESPACE: &str = "library";
const MAX_MANIFEST_BYTES: u64 = 1_000_000;
fn store_candidates() -> Vec<PathBuf> {
if let Some(dir) = std::env::var_os("OLLAMA_MODELS").filter(|dir| !dir.is_empty()) {
return vec![PathBuf::from(dir)];
}
let mut roots = Vec::new();
if let Some(dirs) = directories::BaseDirs::new() {
roots.push(dirs.home_dir().join(".ollama").join("models"));
}
if cfg!(target_os = "linux") {
roots.push(PathBuf::from("/usr/share/ollama/.ollama/models"));
}
roots
}
pub fn installed_models() -> Option<Vec<String>> {
installed_models_in(&store_candidates())
}
fn installed_models_in(roots: &[PathBuf]) -> Option<Vec<String>> {
roots
.iter()
.map(|root| scan(root))
.find(|models| !models.is_empty())
}
fn scan(root: &Path) -> Vec<String> {
let mut names = Vec::new();
for (host, host_dir) in subdirs(&root.join("manifests")) {
for (namespace, ns_dir) in subdirs(&host_dir) {
for (model, model_dir) in subdirs(&ns_dir) {
for (tag, manifest) in files_in(&model_dir) {
if is_manifest(&manifest) {
names.push(display_name(&host, &namespace, &model, &tag));
}
}
}
}
}
names.sort();
names
}
fn subdirs(dir: &Path) -> Vec<(String, PathBuf)> {
let Ok(entries) = std::fs::read_dir(dir) else {
return Vec::new();
};
entries
.flatten()
.filter(|entry| entry.path().is_dir())
.filter_map(|entry| Some((entry.file_name().into_string().ok()?, entry.path())))
.collect()
}
fn files_in(dir: &Path) -> Vec<(String, PathBuf)> {
let Ok(entries) = std::fs::read_dir(dir) else {
return Vec::new();
};
entries
.flatten()
.filter(|entry| entry.path().is_file())
.filter_map(|entry| Some((entry.file_name().into_string().ok()?, entry.path())))
.collect()
}
fn is_manifest(path: &Path) -> bool {
let Ok(meta) = std::fs::metadata(path) else {
return false;
};
if meta.len() > MAX_MANIFEST_BYTES {
return false;
}
let Ok(text) = std::fs::read_to_string(path) else {
return false;
};
serde_json::from_str::<serde_json::Value>(&text)
.ok()
.and_then(|value| {
value
.get("schemaVersion")
.and_then(serde_json::Value::as_u64)
})
== Some(2)
}
fn display_name(host: &str, namespace: &str, model: &str, tag: &str) -> String {
if host != DEFAULT_REGISTRY {
return format!("{host}/{namespace}/{model}:{tag}");
}
if namespace != DEFAULT_NAMESPACE {
return format!("{namespace}/{model}:{tag}");
}
format!("{model}:{tag}")
}
#[cfg(test)]
mod tests {
use super::*;
const MANIFEST: &str = r#"{"schemaVersion":2,"mediaType":"application/vnd.docker.distribution.manifest.v2+json","config":{"digest":"sha256:aa","size":545},"layers":[{"mediaType":"application/vnd.ollama.image.model","digest":"sha256:bb","size":5154939136}]}"#;
struct FixtureStore(PathBuf);
impl FixtureStore {
fn new(tag: &str) -> Self {
let root = std::env::temp_dir()
.join(format!("mermaid-ollama-store-{tag}-{}", std::process::id()));
let _ = std::fs::remove_dir_all(&root);
Self(root)
}
fn write(&self, host: &str, namespace: &str, model: &str, tag: &str, body: &str) {
let dir = self
.0
.join("manifests")
.join(host)
.join(namespace)
.join(model);
std::fs::create_dir_all(&dir).expect("create manifest dir");
std::fs::write(dir.join(tag), body).expect("write manifest");
}
}
impl Drop for FixtureStore {
fn drop(&mut self) {
let _ = std::fs::remove_dir_all(&self.0);
}
}
#[test]
fn display_names_match_api_tags_shortening() {
let store = FixtureStore::new("elision");
store.write(
"registry.ollama.ai",
"library",
"gemma4",
"e4b-it-qat",
MANIFEST,
);
store.write(
"registry.ollama.ai",
"library",
"nemotron-3-ultra",
"cloud",
MANIFEST,
);
store.write(
"registry.ollama.ai",
"jmorganca",
"mymodel",
"latest",
MANIFEST,
);
store.write("hf.co", "someone", "some-gguf", "Q4_K_M", MANIFEST);
assert_eq!(
installed_models_in(std::slice::from_ref(&store.0)).expect("models found"),
vec![
"gemma4:e4b-it-qat",
"hf.co/someone/some-gguf:Q4_K_M",
"jmorganca/mymodel:latest",
"nemotron-3-ultra:cloud",
]
);
}
#[test]
fn junk_entries_are_ignored() {
let store = FixtureStore::new("junk");
store.write("registry.ollama.ai", "library", "real", "latest", MANIFEST);
store.write(
"registry.ollama.ai",
"library",
"real",
".DS_Store",
"\0\0junk",
);
store.write(
"registry.ollama.ai",
"library",
"real",
"v1",
r#"{"schemaVersion":1}"#,
);
std::fs::write(
store
.0
.join("manifests")
.join("registry.ollama.ai")
.join("stray.json"),
MANIFEST,
)
.expect("write stray");
std::fs::create_dir_all(
store
.0
.join("manifests")
.join("registry.ollama.ai")
.join("library")
.join("real")
.join("not-a-file"),
)
.expect("create stray dir");
assert_eq!(
installed_models_in(std::slice::from_ref(&store.0)),
Some(vec!["real:latest".to_string()])
);
}
#[test]
fn empty_stores_yield_none_and_first_populated_root_wins() {
let empty = FixtureStore::new("empty");
std::fs::create_dir_all(empty.0.join("manifests")).expect("create manifests dir");
let missing = FixtureStore::new("missing");
let populated = FixtureStore::new("populated");
populated.write("registry.ollama.ai", "library", "qwen3", "8b", MANIFEST);
assert_eq!(installed_models_in(std::slice::from_ref(&empty.0)), None);
assert_eq!(installed_models_in(std::slice::from_ref(&missing.0)), None);
assert_eq!(
installed_models_in(&[missing.0.clone(), empty.0.clone(), populated.0.clone()]),
Some(vec!["qwen3:8b".to_string()])
);
}
#[test]
fn ollama_models_env_overrides_candidates() {
temp_env::with_var("OLLAMA_MODELS", Some("/relocated/models"), || {
assert_eq!(store_candidates(), vec![PathBuf::from("/relocated/models")]);
});
temp_env::with_var("OLLAMA_MODELS", Some(""), || {
assert!(
store_candidates()
.iter()
.all(|root| root != &PathBuf::from("")),
"empty OLLAMA_MODELS must be ignored"
);
});
}
}