mod custom;
mod gpt4all;
mod hf_cache;
mod jan;
mod lmstudio;
mod ollama;
use std::collections::HashMap;
use std::path::{Path, PathBuf};
use crate::registry::{Ecosystem, Source};
#[derive(Debug, Clone, Default)]
pub struct ScanEnv {
pub home_dir: Option<PathBuf>,
pub roots: HashMap<Ecosystem, PathBuf>,
}
impl ScanEnv {
pub fn home(&self) -> Option<PathBuf> {
self.home_dir.clone().or_else(dirs::home_dir)
}
pub fn root_override(&self, eco: Ecosystem) -> Option<&Path> {
self.roots.get(&eco).map(PathBuf::as_path)
}
}
#[derive(Debug, Clone)]
pub struct FoundFile {
pub path: PathBuf,
pub ecosystem: Ecosystem,
pub size_bytes: u64,
pub display_name: String,
pub known_sha256: Option<String>,
pub source: Option<Source>,
}
pub trait Scanner {
fn ecosystem(&self) -> Ecosystem;
fn detect_root(&self, env: &ScanEnv) -> Option<PathBuf>;
fn scan(&self, root: &Path) -> Vec<FoundFile>;
}
pub fn all_scanners() -> Vec<Box<dyn Scanner>> {
vec![
Box::new(ollama::OllamaScanner),
Box::new(lmstudio::LmStudioScanner),
Box::new(hf_cache::HfCacheScanner),
Box::new(jan::JanScanner),
Box::new(gpt4all::Gpt4allScanner),
]
}
pub fn scan_all(
env: &ScanEnv,
only: Option<&[Ecosystem]>,
extra_roots: &[PathBuf],
) -> Vec<FoundFile> {
let mut found = Vec::new();
for scanner in all_scanners() {
if let Some(only) = only {
if !only.contains(&scanner.ecosystem()) {
continue;
}
}
if let Some(root) = scanner.detect_root(env) {
tracing::debug!(ecosystem = ?scanner.ecosystem(), root = %root.display(), "scanning");
found.extend(scanner.scan(&root));
}
}
if only.is_none() || only.is_some_and(|o| o.contains(&Ecosystem::Custom)) {
for root in extra_roots {
found.extend(custom::scan_custom_root(root));
}
}
found
}
pub(crate) fn walk_gguf(root: &Path, ecosystem: Ecosystem) -> Vec<FoundFile> {
let mut out = Vec::new();
for entry in walkdir::WalkDir::new(root)
.follow_links(false)
.into_iter()
.filter_map(|e| e.ok())
{
if !entry.file_type().is_file() {
continue;
}
let path = entry.path();
if path
.extension()
.is_none_or(|e| !e.eq_ignore_ascii_case("gguf"))
{
continue;
}
let Ok(meta) = entry.metadata() else { continue };
out.push(FoundFile {
path: path.to_path_buf(),
ecosystem,
size_bytes: meta.len(),
display_name: path
.file_stem()
.map(|s| s.to_string_lossy().into_owned())
.unwrap_or_else(|| path.to_string_lossy().into_owned()),
known_sha256: None,
source: None,
});
}
out
}
pub(crate) fn is_sha256_hex(s: &str) -> bool {
s.len() == 64 && s.bytes().all(|b| b.is_ascii_hexdigit())
}
#[cfg(any(test, feature = "test-fixtures"))]
pub mod fixtures {
pub use super::hf_cache::fixtures as hf_cache;
pub use super::ollama::fixtures as ollama;
}