use semver::Version;
use serde::{Deserialize, Serialize};
use std::io::ErrorKind;
use std::path::PathBuf;
use time::{Duration, OffsetDateTime};
const CHECK_PERIOD: Duration = Duration::days(1);
fn state_file() -> Option<PathBuf> {
let dirs = directories::BaseDirs::new()?;
let path = dirs.state_dir().unwrap_or_else(|| dirs.data_local_dir());
Some(path.join(env!("CARGO_PKG_NAME")).join("update.json"))
}
#[derive(Clone, Debug, Serialize, Deserialize)]
struct StateInformation {
#[serde(with = "time::serde::rfc3339")]
pub last_check: OffsetDateTime,
pub versions: Versions,
}
#[derive(Clone, Debug, Default, PartialEq, Eq, Serialize, Deserialize)]
pub struct Versions {
#[serde(default, skip_serializing_if = "Option::is_none")]
pub release: Option<Version>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub prerelease: Option<Version>,
}
#[derive(Clone, Debug)]
pub enum State {
NotNeeded(Versions),
Needed,
}
pub async fn need_check() -> State {
let Some(file) = state_file() else {
tracing::debug!("Unable to find a user home. Skipping update checks.");
return State::NotNeeded(Default::default());
};
let state = match tokio::fs::read(&file).await {
Err(err) if err.kind() == ErrorKind::NotFound => return State::Needed,
Err(err) => {
tracing::debug!(
"Failed to check update state file ({}), skipping: {err}",
file.display()
);
return State::NotNeeded(Default::default());
}
Ok(state) => state,
};
let Ok(state) = serde_json::from_slice::<StateInformation>(&state) else {
return State::Needed;
};
let diff = OffsetDateTime::now_utc() - state.last_check;
tracing::debug!("Time since last check: {diff}");
if diff > CHECK_PERIOD {
State::Needed
} else {
State::NotNeeded(state.versions)
}
}
pub async fn record_checked(versions: Versions) {
let Some(file) = state_file() else {
tracing::debug!("Unable to find a user home. Skipping update checks.");
return;
};
let state = match serde_json::to_vec(&StateInformation {
last_check: OffsetDateTime::now_utc(),
versions,
}) {
Ok(state) => state,
Err(err) => {
tracing::debug!("Unable to serialize state file: {err}");
return;
}
};
if let Some(parent) = file.parent() {
if let Err(err) = tokio::fs::create_dir_all(parent).await {
tracing::debug!(
"Failed to create parent directory for update state ({}): {err}",
parent.display()
);
return;
}
}
if let Err(err) = tokio::fs::write(&file, state).await {
tracing::debug!(
"Failed to write update state file ({}): {err}",
file.display()
);
}
}