kernel/discovery/
lm_studio_scanner.rs1use std::path::{Path, PathBuf};
7
8use crate::discovery::gguf_models::{discovered_models, is_mmproj_name};
9use crate::discovery::scanner::{ScanResult, StoreScanner};
10use crate::records::SourceKind;
11
12pub struct LMStudioScanner {
14 roots: Vec<PathBuf>,
15}
16
17impl LMStudioScanner {
18 pub fn new(roots: Vec<PathBuf>) -> Self {
20 Self { roots }
21 }
22
23 pub fn single(root: impl Into<PathBuf>) -> Self {
25 Self::new(vec![root.into()])
26 }
27
28 fn scan_root(&self, root: &Path, result: &mut ScanResult) {
29 if !root.exists() {
30 return;
31 }
32 let mut ggufs = Vec::new();
33 if collect_ggufs(root, &mut ggufs).is_err() {
34 if !result.failed_kinds.contains(&SourceKind::lm_studio()) {
35 result.failed_kinds.push(SourceKind::lm_studio());
36 }
37 return;
38 }
39 let (models, issues) =
40 discovered_models(&ggufs, &SourceKind::lm_studio(), |path| repo_of(path, root));
41 result.discovered.extend(models);
42 result.issues.extend(issues);
43 }
44}
45
46impl StoreScanner for LMStudioScanner {
47 fn kinds(&self) -> Vec<SourceKind> {
48 vec![SourceKind::lm_studio()]
49 }
50
51 fn scan(&self) -> ScanResult {
52 let mut result = ScanResult::default();
53 for root in &self.roots {
54 self.scan_root(root, &mut result);
55 }
56 result
57 }
58}
59
60fn repo_of(path: &Path, root: &Path) -> Option<String> {
63 let relative = path.strip_prefix(root).ok()?;
64 let parts: Vec<String> = relative
65 .components()
66 .map(|component| component.as_os_str().to_string_lossy().into_owned())
67 .collect();
68 if parts.len() >= 3 {
69 Some(parts[..parts.len() - 1].join("/"))
70 } else {
71 None
72 }
73}
74
75fn collect_ggufs(dir: &Path, into: &mut Vec<(PathBuf, i64)>) -> std::io::Result<()> {
79 for entry in std::fs::read_dir(dir)? {
80 let entry = entry?;
81 let path = entry.path();
82 if path
83 .file_name()
84 .and_then(|name| name.to_str())
85 .is_some_and(|name| name.starts_with('.'))
86 {
87 continue;
88 }
89 match entry.file_type() {
90 Ok(kind) if kind.is_dir() => {
91 let _ = collect_ggufs(&path, into);
92 }
93 _ if is_gguf_weight(&path) => {
94 if let Ok(meta) = std::fs::metadata(&path)
95 && meta.is_file()
96 {
97 into.push((path, meta.len() as i64));
98 }
99 }
100 _ => {}
101 }
102 }
103 Ok(())
104}
105
106fn is_gguf_weight(path: &Path) -> bool {
107 let name = path
108 .file_name()
109 .and_then(|name| name.to_str())
110 .unwrap_or_default();
111 if is_mmproj_name(name) {
112 return false;
113 }
114 path.extension()
115 .and_then(|ext| ext.to_str())
116 .is_some_and(|ext| ext.eq_ignore_ascii_case("gguf"))
117}