1use crate::io::{http, symlink, uri_to_path, ApiResult};
3use crate::prelude::{copy, read, Path, PathBuf};
4use crate::schema::agent::ModelDetails;
5use crate::util::Label;
6use crate::{Location, Repository, Scheme};
7use color_eyre::eyre::eyre;
8use core::fmt;
9use strum::EnumIs;
10use tracing::error;
11
12#[derive(Clone, Debug, PartialEq, Eq, EnumIs)]
21pub enum Source {
22 Local {
24 name: Option<String>,
26 path: PathBuf,
28 action: Option<SourceAction>,
30 },
31 Remote {
33 name: Option<String>,
35 identifier: String,
37 },
38 Unsupported(String),
40}
41#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
43pub enum SourceAction {
44 #[default]
46 Reference,
47 Copy,
49 Symlink,
51}
52impl SourceAction {
53 pub fn from_options(copy: bool, symlink: bool) -> Option<Self> {
55 match (copy, symlink) {
56 | (true, false) => Some(Self::Copy),
57 | (false, true) => Some(Self::Symlink),
58 | _ => None,
59 }
60 }
61 pub fn materialize(self, path: &Path, target: &Path, name: &str) -> ApiResult<String> {
63 let result = match self {
64 | SourceAction::Copy if target.exists() => Ok(format!("Skipping copy - '{name}' already exists at {}", target.display())),
65 | SourceAction::Copy => match copy(path, target) {
66 | Ok(_) => Ok(format!("Copied local model '{name}' -> {}", target.display())),
67 | Err(why) => {
68 error!("=> {} Copy local model '{}' to {} - {why}", Label::fail(), name, target.display());
69 Err(why.into())
70 }
71 },
72 | SourceAction::Symlink if target.exists() || target.symlink_metadata().map(|metadata| metadata.is_symlink()).unwrap_or(false) => {
73 Ok(format!("Skipping symlink - '{name}' already exists at {}", target.display()))
74 }
75 | SourceAction::Symlink => match symlink(path, target) {
76 | Ok(_) => Ok(format!("Symlinked local model '{name}' -> {}", target.display())),
77 | Err(why) => {
78 error!("=> {} Symlink local model '{}' to {} - {why}", Label::fail(), name, target.display());
79 Err(why)
80 }
81 },
82 | SourceAction::Reference => Ok(format!("Local model '{name}' referenced in place at {}", path.display())),
83 };
84 result
85 }
86}
87impl Source {
88 pub async fn read(source: &str, offline: bool) -> ApiResult<String> {
95 Self::read_bytes(source, offline)
96 .await
97 .and_then(|bytes| String::from_utf8(bytes).map_err(|why| eyre!("Failed to decode source as UTF-8 — {why}")))
98 }
99 pub async fn read_bytes(source: &str, offline: bool) -> ApiResult<Vec<u8>> {
103 Source::read_parsed_bytes(Self::parse(source), offline).await
104 }
105 pub fn parse(source: &str) -> Self {
109 let location: Location = source.parse().expect("Location::from_str is infallible");
111 match location {
112 | Location::Detailed { scheme: Scheme::File, .. } => {
113 let path = uri_to_path(source);
114 Self::Local {
115 name: None,
116 path,
117 action: None,
118 }
119 }
120 | Location::Detailed {
121 scheme: Scheme::HTTPS | Scheme::HTTP,
122 ..
123 } => Self::Remote {
124 name: None,
125 identifier: source.to_string(),
126 },
127 | Location::Detailed {
128 scheme: Scheme::Unsupported, ..
129 } => Self::Unsupported(source.to_string()),
130 | Location::Simple(_) => Self::Local {
131 name: None,
132 path: PathBuf::from(source),
133 action: None,
134 },
135 }
136 }
137 pub fn name(&self) -> String {
139 match self {
140 | Source::Local { name: Some(name), .. } | Source::Remote { name: Some(name), .. } => name.clone(),
141 | Source::Local { path, .. } => path.file_stem().and_then(|s| s.to_str()).unwrap_or("model").to_string(),
142 | Source::Remote { identifier, .. } | Source::Unsupported(identifier) => identifier.clone(),
143 }
144 }
145 pub fn identifier(&self) -> String {
147 match self {
148 | Source::Local { path, .. } => path.display().to_string(),
149 | Source::Remote { identifier, .. } | Source::Unsupported(identifier) => identifier.clone(),
150 }
151 }
152 pub fn with_action(self, action: Option<SourceAction>) -> Self {
154 match self {
155 | Source::Local { name, path, .. } => Source::Local { name, path, action },
156 | other => other,
157 }
158 }
159 pub fn with_name(self, value: impl Into<String>) -> Self {
161 let binding = value.into();
162 let trimmed = binding.trim();
163 let name = (!trimmed.is_empty()).then(|| trimmed.to_string());
164 match self {
165 | Source::Local { path, action, .. } => Source::Local { name, path, action },
166 | Source::Remote { identifier, .. } => Source::Remote { name, identifier },
167 | Source::Unsupported(value) => Source::Unsupported(value),
168 }
169 }
170 async fn read_parsed_bytes(source: Source, offline: bool) -> ApiResult<Vec<u8>> {
171 match source {
172 | Source::Local { path, .. } => read(path).map_err(|why| eyre!("Failed to read source — {why}")),
173 | Source::Remote { identifier, .. } => Source::read_remote_bytes(&identifier, offline).await,
174 | Source::Unsupported(scheme) => Err(eyre!("Unsupported source URI scheme '{scheme}'")),
175 }
176 }
177 async fn read_remote_bytes(url: &str, offline: bool) -> ApiResult<Vec<u8>> {
178 match offline {
179 | true => Err(eyre!("Cannot read remote source while offline")),
180 | false => http::response_body_bytes(http::get(url).send().await, "Failed to download source").await,
181 }
182 }
183}
184impl fmt::Display for Source {
185 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
186 let name = self.name();
187 let identifier = self.identifier();
188 if name == identifier {
189 write!(f, "{name}")
190 } else {
191 write!(f, "{name} ({identifier})")
192 }
193 }
194}
195impl From<&str> for Source {
196 fn from(selector: &str) -> Self {
197 let trimmed = selector.trim();
198 let location = Location::from(trimmed);
199 if location.is_local() {
200 let path = uri_to_path(trimmed);
201 Self::Local {
202 name: None,
203 path,
204 action: None,
205 }
206 } else {
207 Self::Remote {
208 name: Some(trimmed.to_string()),
209 identifier: trimmed.to_string(),
210 }
211 }
212 }
213}
214impl From<Location> for Source {
215 fn from(location: Location) -> Self {
216 let scheme = location.scheme();
217 let uri = location.uri().unwrap_or_default();
218 match scheme {
219 | Scheme::File => Self::Local {
220 name: None,
221 path: uri_to_path(&uri),
222 action: None,
223 },
224 | Scheme::HTTPS | Scheme::HTTP => Self::Remote { name: None, identifier: uri },
225 | Scheme::Unsupported => Self::Unsupported(uri),
226 }
227 }
228}
229impl From<&Repository> for Source {
230 fn from(repository: &Repository) -> Self {
231 match repository {
232 | Repository::HuggingFace { location } => Self::Remote {
233 name: None,
234 identifier: repository.id().unwrap_or_else(|| location.uri().unwrap_or_default()),
235 },
236 | _ => Self::from(repository.location()),
237 }
238 }
239}
240impl From<ModelDetails> for Option<Source> {
241 fn from(details: ModelDetails) -> Self {
242 details.weights.and_then(|weights| weights.to_source(details.name.or(details.id)))
243 }
244}