#![allow(dead_code)]
use crate::update::config::Channel;
use serde::Deserialize;
pub const GITHUB_OWNER: &str = "bearbinary";
pub const GITHUB_REPO: &str = "jarvy";
#[derive(Debug, Clone, Deserialize)]
pub struct GitHubRelease {
pub tag_name: String,
pub name: Option<String>,
pub body: Option<String>,
pub prerelease: bool,
pub draft: bool,
pub assets: Vec<ReleaseAsset>,
pub published_at: Option<String>,
pub html_url: String,
}
impl GitHubRelease {
pub fn version(&self) -> &str {
self.tag_name.strip_prefix('v').unwrap_or(&self.tag_name)
}
pub fn semver(&self) -> Option<semver::Version> {
semver::Version::parse(self.version()).ok()
}
pub fn matches_channel(&self, channel: Channel) -> bool {
if self.draft {
return false;
}
match channel {
Channel::Stable => !self.prerelease && channel.matches_version(self.version()),
Channel::Beta => channel.matches_version(self.version()),
Channel::Nightly => true,
}
}
pub fn changelog(&self) -> Option<&str> {
self.body.as_deref()
}
pub fn asset_for_platform(&self) -> Option<&ReleaseAsset> {
let target = get_current_target();
self.assets.iter().find(|a| {
let name = a.name.to_lowercase();
name.contains(&target) && (name.ends_with(".tar.gz") || name.ends_with(".zip"))
})
}
pub fn checksum_asset(&self) -> Option<&ReleaseAsset> {
self.assets.iter().find(|a| {
let name = a.name.to_lowercase();
name.contains("sha256") || name.ends_with(".sha256")
})
}
}
#[derive(Debug, Clone, Deserialize)]
pub struct ReleaseAsset {
pub name: String,
pub browser_download_url: String,
pub size: u64,
pub content_type: String,
}
pub fn get_current_target() -> String {
let os = if cfg!(target_os = "macos") {
"darwin"
} else if cfg!(target_os = "linux") {
"linux"
} else if cfg!(target_os = "windows") {
"windows"
} else {
"unknown"
};
let arch = if cfg!(target_arch = "x86_64") {
"x86_64"
} else if cfg!(target_arch = "aarch64") {
"aarch64"
} else if cfg!(target_arch = "arm") {
"arm"
} else {
"unknown"
};
format!("{}-{}", os, arch)
}
pub struct ReleaseClient {
owner: String,
repo: String,
}
impl ReleaseClient {
pub fn new() -> Self {
Self {
owner: GITHUB_OWNER.to_string(),
repo: GITHUB_REPO.to_string(),
}
}
pub fn custom(owner: &str, repo: &str) -> Self {
Self {
owner: owner.to_string(),
repo: repo.to_string(),
}
}
pub fn fetch_releases(&self) -> Result<Vec<GitHubRelease>, ReleaseError> {
let url = format!(
"https://api.github.com/repos/{}/{}/releases",
self.owner, self.repo
);
let agent = ureq::Agent::new_with_defaults();
let response = agent
.get(&url)
.header(
"User-Agent",
&format!("jarvy/{}", env!("CARGO_PKG_VERSION")),
)
.header("Accept", "application/vnd.github.v3+json")
.call()
.map_err(|e| ReleaseError::NetworkError(e.to_string()))?;
let releases: Vec<GitHubRelease> = response
.into_body()
.read_json()
.map_err(|e| ReleaseError::ParseError(e.to_string()))?;
Ok(releases)
}
pub fn fetch_latest(&self, channel: Channel) -> Result<Option<GitHubRelease>, ReleaseError> {
let releases = self.fetch_releases()?;
Ok(releases
.into_iter()
.filter(|r| r.matches_channel(channel))
.max_by(|a, b| {
let a_ver = a.semver();
let b_ver = b.semver();
match (a_ver, b_ver) {
(Some(a), Some(b)) => a.cmp(&b),
(Some(_), None) => std::cmp::Ordering::Greater,
(None, Some(_)) => std::cmp::Ordering::Less,
(None, None) => std::cmp::Ordering::Equal,
}
}))
}
pub fn fetch_by_tag(&self, tag: &str) -> Result<GitHubRelease, ReleaseError> {
let url = format!(
"https://api.github.com/repos/{}/{}/releases/tags/{}",
self.owner, self.repo, tag
);
let agent = ureq::Agent::new_with_defaults();
let response = agent
.get(&url)
.header(
"User-Agent",
&format!("jarvy/{}", env!("CARGO_PKG_VERSION")),
)
.header("Accept", "application/vnd.github.v3+json")
.call()
.map_err(|e| ReleaseError::NetworkError(e.to_string()))?;
let release: GitHubRelease = response
.into_body()
.read_json()
.map_err(|e| ReleaseError::ParseError(e.to_string()))?;
Ok(release)
}
}
impl Default for ReleaseClient {
fn default() -> Self {
Self::new()
}
}
#[derive(Debug, thiserror::Error)]
pub enum ReleaseError {
#[error("Network error: {0}")]
NetworkError(String),
#[error("Failed to parse release data: {0}")]
ParseError(String),
#[error("Release not found: {0}")]
NotFound(String),
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_release_version() {
let release = GitHubRelease {
tag_name: "v1.2.3".to_string(),
name: None,
body: None,
prerelease: false,
draft: false,
assets: vec![],
published_at: None,
html_url: "https://github.com/test".to_string(),
};
assert_eq!(release.version(), "1.2.3");
assert_eq!(release.semver(), Some(semver::Version::new(1, 2, 3)));
}
#[test]
fn test_matches_channel() {
let stable_release = GitHubRelease {
tag_name: "v1.0.0".to_string(),
name: None,
body: None,
prerelease: false,
draft: false,
assets: vec![],
published_at: None,
html_url: "".to_string(),
};
let beta_release = GitHubRelease {
tag_name: "v1.1.0-beta.1".to_string(),
prerelease: true,
..stable_release.clone()
};
let draft_release = GitHubRelease {
tag_name: "v2.0.0".to_string(),
draft: true,
..stable_release.clone()
};
assert!(stable_release.matches_channel(Channel::Stable));
assert!(stable_release.matches_channel(Channel::Beta));
assert!(stable_release.matches_channel(Channel::Nightly));
assert!(!beta_release.matches_channel(Channel::Stable));
assert!(beta_release.matches_channel(Channel::Beta));
assert!(beta_release.matches_channel(Channel::Nightly));
assert!(!draft_release.matches_channel(Channel::Stable));
assert!(!draft_release.matches_channel(Channel::Nightly));
}
#[test]
fn test_get_current_target() {
let target = get_current_target();
assert!(target.contains('-'));
#[cfg(target_os = "macos")]
assert!(target.starts_with("darwin"));
#[cfg(target_os = "linux")]
assert!(target.starts_with("linux"));
#[cfg(target_os = "windows")]
assert!(target.starts_with("windows"));
}
}