use crate::io::{http, symlink, uri_to_path, ApiResult};
use crate::prelude::{copy, read, Path, PathBuf};
use crate::schema::agent::ModelDetails;
use crate::util::Label;
use crate::{Location, Repository, Scheme};
use color_eyre::eyre::eyre;
use core::fmt;
use strum::EnumIs;
use tracing::error;
#[derive(Clone, Debug, PartialEq, Eq, EnumIs)]
pub enum Source {
Local {
name: Option<String>,
path: PathBuf,
action: Option<SourceAction>,
},
Remote {
name: Option<String>,
identifier: String,
},
Unsupported(String),
}
#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
pub enum SourceAction {
#[default]
Reference,
Copy,
Symlink,
}
impl SourceAction {
pub fn from_options(copy: bool, symlink: bool) -> Option<Self> {
match (copy, symlink) {
| (true, false) => Some(Self::Copy),
| (false, true) => Some(Self::Symlink),
| _ => None,
}
}
pub fn materialize(self, path: &Path, target: &Path, name: &str) -> ApiResult<String> {
let result = match self {
| SourceAction::Copy if target.exists() => Ok(format!("Skipping copy - '{name}' already exists at {}", target.display())),
| SourceAction::Copy => match copy(path, target) {
| Ok(_) => Ok(format!("Copied local model '{name}' -> {}", target.display())),
| Err(why) => {
error!("=> {} Copy local model '{}' to {} - {why}", Label::fail(), name, target.display());
Err(why.into())
}
},
| SourceAction::Symlink if target.exists() || target.symlink_metadata().map(|metadata| metadata.is_symlink()).unwrap_or(false) => {
Ok(format!("Skipping symlink - '{name}' already exists at {}", target.display()))
}
| SourceAction::Symlink => match symlink(path, target) {
| Ok(_) => Ok(format!("Symlinked local model '{name}' -> {}", target.display())),
| Err(why) => {
error!("=> {} Symlink local model '{}' to {} - {why}", Label::fail(), name, target.display());
Err(why)
}
},
| SourceAction::Reference => Ok(format!("Local model '{name}' referenced in place at {}", path.display())),
};
result
}
}
impl Source {
pub async fn read(source: &str, offline: bool) -> ApiResult<String> {
Self::read_bytes(source, offline)
.await
.and_then(|bytes| String::from_utf8(bytes).map_err(|why| eyre!("Failed to decode source as UTF-8 — {why}")))
}
pub async fn read_bytes(source: &str, offline: bool) -> ApiResult<Vec<u8>> {
Source::read_parsed_bytes(Self::parse(source), offline).await
}
pub fn parse(source: &str) -> Self {
let location: Location = source.parse().expect("Location::from_str is infallible");
match location {
| Location::Detailed { scheme: Scheme::File, .. } => {
let path = uri_to_path(source);
Self::Local {
name: None,
path,
action: None,
}
}
| Location::Detailed {
scheme: Scheme::HTTPS | Scheme::HTTP,
..
} => Self::Remote {
name: None,
identifier: source.to_string(),
},
| Location::Detailed {
scheme: Scheme::Unsupported, ..
} => Self::Unsupported(source.to_string()),
| Location::Simple(_) => Self::Local {
name: None,
path: PathBuf::from(source),
action: None,
},
}
}
pub fn name(&self) -> String {
match self {
| Source::Local { name: Some(name), .. } | Source::Remote { name: Some(name), .. } => name.clone(),
| Source::Local { path, .. } => path.file_stem().and_then(|s| s.to_str()).unwrap_or("model").to_string(),
| Source::Remote { identifier, .. } | Source::Unsupported(identifier) => identifier.clone(),
}
}
pub fn identifier(&self) -> String {
match self {
| Source::Local { path, .. } => path.display().to_string(),
| Source::Remote { identifier, .. } | Source::Unsupported(identifier) => identifier.clone(),
}
}
pub fn with_action(self, action: Option<SourceAction>) -> Self {
match self {
| Source::Local { name, path, .. } => Source::Local { name, path, action },
| other => other,
}
}
pub fn with_name(self, value: impl Into<String>) -> Self {
let binding = value.into();
let trimmed = binding.trim();
let name = (!trimmed.is_empty()).then(|| trimmed.to_string());
match self {
| Source::Local { path, action, .. } => Source::Local { name, path, action },
| Source::Remote { identifier, .. } => Source::Remote { name, identifier },
| Source::Unsupported(value) => Source::Unsupported(value),
}
}
async fn read_parsed_bytes(source: Source, offline: bool) -> ApiResult<Vec<u8>> {
match source {
| Source::Local { path, .. } => read(path).map_err(|why| eyre!("Failed to read source — {why}")),
| Source::Remote { identifier, .. } => Source::read_remote_bytes(&identifier, offline).await,
| Source::Unsupported(scheme) => Err(eyre!("Unsupported source URI scheme '{scheme}'")),
}
}
async fn read_remote_bytes(url: &str, offline: bool) -> ApiResult<Vec<u8>> {
match offline {
| true => Err(eyre!("Cannot read remote source while offline")),
| false => http::response_body_bytes(http::get(url).send().await, "Failed to download source").await,
}
}
}
impl fmt::Display for Source {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
let name = self.name();
let identifier = self.identifier();
if name == identifier {
write!(f, "{name}")
} else {
write!(f, "{name} ({identifier})")
}
}
}
impl From<&str> for Source {
fn from(selector: &str) -> Self {
let trimmed = selector.trim();
let location = Location::from(trimmed);
if location.is_local() {
let path = uri_to_path(trimmed);
Self::Local {
name: None,
path,
action: None,
}
} else {
Self::Remote {
name: Some(trimmed.to_string()),
identifier: trimmed.to_string(),
}
}
}
}
impl From<Location> for Source {
fn from(location: Location) -> Self {
let scheme = location.scheme();
let uri = location.uri().unwrap_or_default();
match scheme {
| Scheme::File => Self::Local {
name: None,
path: uri_to_path(&uri),
action: None,
},
| Scheme::HTTPS | Scheme::HTTP => Self::Remote { name: None, identifier: uri },
| Scheme::Unsupported => Self::Unsupported(uri),
}
}
}
impl From<&Repository> for Source {
fn from(repository: &Repository) -> Self {
match repository {
| Repository::HuggingFace { location } => Self::Remote {
name: None,
identifier: repository.id().unwrap_or_else(|| location.uri().unwrap_or_default()),
},
| _ => Self::from(repository.location()),
}
}
}
impl From<ModelDetails> for Option<Source> {
fn from(details: ModelDetails) -> Self {
details.weights.and_then(|weights| weights.to_source(details.name.or(details.id)))
}
}