use std::path::PathBuf;
use std::time::Duration;
#[cfg(feature = "do-not-track")]
use crate::do_not_track_enabled;
use crate::{
DetailedUpdateInfo, Error, USER_AGENT, UpdateInfo, compare_versions, extract_newest_version,
read_cache, truncate_message, validate_crate_name,
};
#[derive(Debug, Clone)]
pub struct UpdateChecker {
crate_name: String,
current_version: String,
cache_duration: Duration,
timeout: Duration,
cache_dir: Option<PathBuf>,
include_prerelease: bool,
message_url: Option<String>,
}
impl UpdateChecker {
#[must_use]
pub fn new(crate_name: impl Into<String>, current_version: impl Into<String>) -> Self {
Self {
crate_name: crate_name.into(),
current_version: current_version.into(),
cache_duration: Duration::from_secs(24 * 60 * 60),
timeout: Duration::from_secs(5),
cache_dir: crate::cache_dir(),
include_prerelease: false,
message_url: None,
}
}
#[must_use]
pub const fn cache_duration(mut self, duration: Duration) -> Self {
self.cache_duration = duration;
self
}
#[must_use]
pub const fn timeout(mut self, timeout: Duration) -> Self {
self.timeout = timeout;
self
}
#[must_use]
pub fn cache_dir(mut self, dir: Option<PathBuf>) -> Self {
self.cache_dir = dir;
self
}
#[must_use]
pub const fn include_prerelease(mut self, include: bool) -> Self {
self.include_prerelease = include;
self
}
#[must_use]
pub fn message_url(mut self, url: impl Into<String>) -> Self {
self.message_url = Some(url.into());
self
}
pub async fn check(&self) -> Result<Option<UpdateInfo>, Error> {
#[cfg(feature = "do-not-track")]
if do_not_track_enabled() {
return Ok(None);
}
validate_crate_name(&self.crate_name)?;
let client = reqwest::Client::builder()
.timeout(self.timeout)
.user_agent(USER_AGENT)
.build()
.map_err(|e| Error::HttpError(e.to_string()))?;
let (latest, _) = self.get_latest_version(&client).await?;
compare_versions(&self.current_version, latest, self.include_prerelease)
}
pub async fn check_detailed(&self) -> Result<Option<DetailedUpdateInfo>, Error> {
#[cfg(feature = "do-not-track")]
if do_not_track_enabled() {
return Ok(None);
}
validate_crate_name(&self.crate_name)?;
let client = reqwest::Client::builder()
.timeout(self.timeout)
.user_agent(USER_AGENT)
.build()
.map_err(|e| Error::HttpError(e.to_string()))?;
#[cfg(feature = "response-body")]
let (latest, response_body) = self.get_latest_version(&client).await?;
#[cfg(not(feature = "response-body"))]
let (latest, _) = self.get_latest_version(&client).await?;
let update = compare_versions(&self.current_version, latest, self.include_prerelease)?;
match update {
Some(info) => {
let mut detailed = DetailedUpdateInfo::from(info);
if let Some(ref url) = self.message_url {
detailed.message = Self::fetch_message(&client, url).await;
}
#[cfg(feature = "response-body")]
{
detailed.response_body = response_body;
}
Ok(Some(detailed))
}
None => Ok(None),
}
}
async fn get_latest_version(
&self,
client: &reqwest::Client,
) -> Result<(String, Option<String>), Error> {
use std::fs;
let path = self
.cache_dir
.as_ref()
.map(|d| d.join(format!("{}-update-check", self.crate_name)));
if self.cache_duration > Duration::ZERO {
if let Some(ref path) = path {
if let Some(cached) = read_cache(path, self.cache_duration) {
return Ok((cached, None));
}
}
}
let (latest, response_body) = self.fetch_latest_version(client).await?;
if let Some(ref path) = path {
let _ = fs::write(path, &latest);
}
Ok((latest, response_body))
}
async fn fetch_latest_version(
&self,
client: &reqwest::Client,
) -> Result<(String, Option<String>), Error> {
let url = format!("https://crates.io/api/v1/crates/{}", self.crate_name);
let body = client
.get(&url)
.send()
.await
.map_err(|e| Error::HttpError(e.to_string()))?
.text()
.await
.map_err(|e| Error::HttpError(e.to_string()))?;
let version = extract_newest_version(&body)?;
#[cfg(feature = "response-body")]
return Ok((version, Some(body)));
#[cfg(not(feature = "response-body"))]
Ok((version, None))
}
async fn fetch_message(client: &reqwest::Client, url: &str) -> Option<String> {
let body = client.get(url).send().await.ok()?.text().await.ok()?;
truncate_message(&body)
}
}
pub async fn check(
crate_name: impl Into<String>,
current_version: impl Into<String>,
) -> Result<Option<UpdateInfo>, Error> {
UpdateChecker::new(crate_name, current_version)
.check()
.await
}