use std::path::{Path, PathBuf};
use crate::discovery::gguf_models::{is_gguf_name, is_mmproj_name};
use crate::discovery::gguf_shards::parse;
const MAX_DEPTH: usize = 3;
#[derive(Debug, Default)]
pub(crate) struct GgufTree {
pub weights: Vec<(PathBuf, u64)>,
pub has_projector: bool,
}
pub(crate) fn gguf_tree(dir: &Path) -> GgufTree {
let mut tree = GgufTree::default();
collect(dir, MAX_DEPTH, &mut tree);
tree.weights.sort();
tree
}
fn collect(dir: &Path, depth: usize, tree: &mut GgufTree) {
if depth == 0 {
return;
}
for entry in std::fs::read_dir(dir).into_iter().flatten().flatten() {
let path = entry.path();
let Some(name) = path.file_name().and_then(|name| name.to_str()) else {
continue;
};
if name.starts_with('.') {
continue;
}
let Ok(kind) = entry.file_type() else {
continue;
};
if kind.is_dir() {
collect(&path, depth - 1, tree);
continue;
}
if !is_gguf_name(name) {
continue;
}
let projector = is_mmproj_name(name);
let Ok(metadata) = std::fs::metadata(&path) else {
continue;
};
if !metadata.is_file() {
continue;
}
match projector {
true => tree.has_projector = true,
false => tree.weights.push((path, metadata.len())),
}
}
}
pub(crate) fn primary_of(files: &[(PathBuf, u64)]) -> Option<PathBuf> {
let mut candidates: Vec<&(PathBuf, u64)> = files.iter().collect();
candidates.sort_by(|(left_path, left), (right_path, right)| {
right.cmp(left).then_with(|| left_path.cmp(right_path))
});
candidates
.into_iter()
.find_map(|(path, _)| loadable(path, files))
}
fn loadable(path: &Path, files: &[(PathBuf, u64)]) -> Option<PathBuf> {
let Some(shard) = path
.file_name()
.and_then(|name| name.to_str())
.and_then(parse)
else {
return Some(path.to_path_buf());
};
files
.iter()
.find(|(candidate, _)| {
candidate.parent() == path.parent()
&& candidate
.file_name()
.and_then(|name| name.to_str())
.and_then(parse)
.is_some_and(|member| {
member.index == 1
&& member.total == shard.total
&& member.base == shard.base
})
})
.map(|(first, _)| first.clone())
}