1#![warn(missing_docs)]
20
21use std::fs;
22use std::path::PathBuf;
23
24pub mod config;
25pub mod models;
26
27pub use config::ModelConfig;
28pub use models::ModelType;
29
30#[derive(Debug)]
32pub enum HubError {
33 Io(std::io::Error),
35 Json(serde_json::Error),
37 ModelNotFound(String),
39 UnsupportedModel(String),
41}
42
43impl std::fmt::Display for HubError {
44 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
45 match self {
46 Self::Io(e) => write!(f, "IO error: {}", e),
47 Self::Json(e) => write!(f, "JSON error: {}", e),
48 Self::ModelNotFound(m) => write!(f, "Model not found: {}", m),
49 Self::UnsupportedModel(m) => write!(f, "Unsupported model: {}", m),
50 }
51 }
52}
53
54impl std::error::Error for HubError {}
55
56impl From<std::io::Error> for HubError {
57 fn from(e: std::io::Error) -> Self {
58 Self::Io(e)
59 }
60}
61
62impl From<serde_json::Error> for HubError {
63 fn from(e: serde_json::Error) -> Self {
64 Self::Json(e)
65 }
66}
67
68pub type HubResult<T> = Result<T, HubError>;
70
71pub struct FerrotensorHub {
87 cache_dir: PathBuf,
88}
89
90impl FerrotensorHub {
91 pub fn new() -> Self {
95 let cache_dir = dirs_cache_dir().join("huggingface").join("hub");
96
97 Self { cache_dir }
98 }
99
100 pub fn with_cache_dir(cache_dir: PathBuf) -> Self {
102 Self { cache_dir }
103 }
104
105 pub fn get_model_path(&self, model_id: &str) -> PathBuf {
109 let cache_name = format!("models--{}", model_id.replace("/", "--"));
111 self.cache_dir.join(cache_name).join("snapshots")
112 }
113
114 pub fn find_latest_snapshot(&self, model_id: &str) -> HubResult<PathBuf> {
116 let snapshots_dir = self.get_model_path(model_id);
117
118 if !snapshots_dir.exists() {
119 return Err(HubError::ModelNotFound(format!(
120 "Model {} not found. Download with: huggingface-cli download {}",
121 model_id, model_id
122 )));
123 }
124
125 let entries = fs::read_dir(&snapshots_dir)?;
127 for entry in entries {
128 let entry = entry?;
129 if entry.file_type()?.is_dir() {
130 return Ok(entry.path());
131 }
132 }
133
134 Err(HubError::ModelNotFound(format!(
135 "No snapshots found for {}",
136 model_id
137 )))
138 }
139
140 pub fn load_config(&self, model_id: &str) -> HubResult<ModelConfig> {
142 let snapshot = self.find_latest_snapshot(model_id)?;
143 ModelConfig::from_file(&snapshot.join("config.json"))
144 }
145
146 pub fn is_cached(&self, model_id: &str) -> bool {
148 self.get_model_path(model_id).exists()
149 }
150
151 pub fn list_cached(&self) -> Vec<String> {
153 if !self.cache_dir.exists() {
154 return vec![];
155 }
156
157 fs::read_dir(&self.cache_dir)
158 .map(|entries| {
159 entries
160 .filter_map(|e| e.ok())
161 .filter_map(|e| e.file_name().into_string().ok())
162 .filter(|name| name.starts_with("models--"))
163 .map(|name| name.trim_start_matches("models--").replace("--", "/"))
164 .collect()
165 })
166 .unwrap_or_default()
167 }
168
169 pub fn get_weight_files(&self, model_id: &str) -> HubResult<Vec<PathBuf>> {
171 let snapshot = self.find_latest_snapshot(model_id)?;
172
173 let mut files = Vec::new();
174 for entry in fs::read_dir(&snapshot)? {
175 let entry = entry?;
176 let path = entry.path();
177 if path.extension().is_some_and(|e| e == "safetensors") {
178 files.push(path);
179 }
180 }
181
182 files.sort();
184 Ok(files)
185 }
186}
187
188impl Default for FerrotensorHub {
189 fn default() -> Self {
190 Self::new()
191 }
192}
193
194fn dirs_cache_dir() -> PathBuf {
196 std::env::var("HOME")
197 .map(|h| PathBuf::from(h).join(".cache"))
198 .unwrap_or_else(|_| PathBuf::from(".cache"))
199}
200
201#[cfg(test)]
202mod tests {
203 use super::*;
204
205 #[test]
206 fn test_hub_creation() {
207 let hub = FerrotensorHub::new();
208 assert!(hub.cache_dir.to_string_lossy().contains("huggingface"));
209 }
210
211 #[test]
212 fn test_model_path() {
213 let hub = FerrotensorHub::new();
214 let path = hub.get_model_path("meta-llama/Llama-3.2-1B");
215 assert!(path
216 .to_string_lossy()
217 .contains("models--meta-llama--Llama-3.2-1B"));
218 }
219}