Initial commit
This commit is contained in:
3
.gitignore
vendored
Normal file
3
.gitignore
vendored
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
/target
|
||||||
|
/config.toml
|
||||||
|
/updates.seen
|
||||||
1730
Cargo.lock
generated
Normal file
1730
Cargo.lock
generated
Normal file
File diff suppressed because it is too large
Load Diff
16
Cargo.toml
Normal file
16
Cargo.toml
Normal file
@@ -0,0 +1,16 @@
|
|||||||
|
[package]
|
||||||
|
name = "updatecheck"
|
||||||
|
version = "0.1.0"
|
||||||
|
edition = "2024"
|
||||||
|
|
||||||
|
[dependencies]
|
||||||
|
#bytes = "1.11.0"
|
||||||
|
chrono = { version = "0.4.42", default-features = false, features = ["serde"] }
|
||||||
|
reqwest = { version = "0.12.24", features = ["json"] }
|
||||||
|
#ruzstd = "0.8.2"
|
||||||
|
serde = { version = "1.0.228", features = ["derive"] }
|
||||||
|
#serde-xml-rs = "0.8.2"
|
||||||
|
tokio = { version = "1.48.0", default-features = false, features = ["rt", "macros"] }
|
||||||
|
toml = "0.9.8"
|
||||||
|
tracing = "0.1.41"
|
||||||
|
tracing-subscriber = { version = "0.3.20", features = ["env-filter"] }
|
||||||
2
rustfmt.toml
Normal file
2
rustfmt.toml
Normal file
@@ -0,0 +1,2 @@
|
|||||||
|
match_block_trailing_comma = true
|
||||||
|
max_width = 79
|
||||||
130
src/config.rs
Normal file
130
src/config.rs
Normal file
@@ -0,0 +1,130 @@
|
|||||||
|
use std::borrow::Cow;
|
||||||
|
use std::path::Path;
|
||||||
|
use std::path::PathBuf;
|
||||||
|
|
||||||
|
use serde::Deserialize;
|
||||||
|
|
||||||
|
static DEFAULT_CONFIG: &str = "config.toml";
|
||||||
|
static CONFIG_ENV_VAR: &str = "UPDATECHECK_CONFIG";
|
||||||
|
|
||||||
|
#[derive(Debug)]
|
||||||
|
pub enum Error {
|
||||||
|
Toml(toml::de::Error),
|
||||||
|
Io(std::io::Error),
|
||||||
|
}
|
||||||
|
|
||||||
|
impl std::fmt::Display for Error {
|
||||||
|
fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
|
||||||
|
match self {
|
||||||
|
Self::Toml(e) => write!(f, "{e}"),
|
||||||
|
Self::Io(e) => write!(f, "{e}"),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl std::error::Error for Error {
|
||||||
|
fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
|
||||||
|
match self {
|
||||||
|
Self::Toml(e) => Some(e),
|
||||||
|
Self::Io(e) => Some(e),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<toml::de::Error> for Error {
|
||||||
|
fn from(e: toml::de::Error) -> Self {
|
||||||
|
Self::Toml(e)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<std::io::Error> for Error {
|
||||||
|
fn from(e: std::io::Error) -> Self {
|
||||||
|
Self::Io(e)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Deserialize)]
|
||||||
|
#[serde(rename_all = "UPPERCASE")]
|
||||||
|
pub enum HttpMethod {
|
||||||
|
Get,
|
||||||
|
Post,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Default for HttpMethod {
|
||||||
|
fn default() -> Self {
|
||||||
|
Self::Post
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Deserialize)]
|
||||||
|
pub struct RequestHeader {
|
||||||
|
pub name: String,
|
||||||
|
pub value_file: Option<String>,
|
||||||
|
pub value: Option<String>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Deserialize)]
|
||||||
|
pub struct UpdateHandler {
|
||||||
|
pub url: String,
|
||||||
|
#[serde(default)]
|
||||||
|
pub method: HttpMethod,
|
||||||
|
#[serde(default = "default_updatehandler_coalesce")]
|
||||||
|
pub coalesce: bool,
|
||||||
|
#[serde(default)]
|
||||||
|
pub headers: Option<Vec<RequestHeader>>,
|
||||||
|
}
|
||||||
|
|
||||||
|
fn default_updatehandler_coalesce() -> bool {
|
||||||
|
true
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Deserialize)]
|
||||||
|
pub struct Watch {
|
||||||
|
pub packages: String,
|
||||||
|
pub on_update: UpdateHandler,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Deserialize)]
|
||||||
|
pub struct StorageConfig {
|
||||||
|
#[serde(default = "default_storage_dir")]
|
||||||
|
pub dir: PathBuf,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Default for StorageConfig {
|
||||||
|
fn default() -> Self {
|
||||||
|
Self {
|
||||||
|
dir: default_storage_dir(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn default_storage_dir() -> PathBuf {
|
||||||
|
PathBuf::from(".")
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Deserialize)]
|
||||||
|
pub struct Configuration {
|
||||||
|
#[serde(rename = "watch")]
|
||||||
|
pub watches: Vec<Watch>,
|
||||||
|
#[serde(default)]
|
||||||
|
pub storage: StorageConfig,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Configuration {
|
||||||
|
pub fn load(path: impl AsRef<Path>) -> Result<Self, Error> {
|
||||||
|
let data = std::fs::read_to_string(path)?;
|
||||||
|
Ok(toml::from_str(&data)?)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn load(path: Option<&Path>) -> Result<Configuration, Error> {
|
||||||
|
let path: Cow<'_, Path> = match path {
|
||||||
|
Some(p) => Cow::Borrowed(p),
|
||||||
|
None => match std::env::var_os(CONFIG_ENV_VAR) {
|
||||||
|
Some(v) => Cow::Owned(PathBuf::from(v)),
|
||||||
|
None => Cow::Borrowed(Path::new(DEFAULT_CONFIG)),
|
||||||
|
},
|
||||||
|
};
|
||||||
|
tracing::debug!("Loading configuration file from {}", path.display());
|
||||||
|
Configuration::load(path)
|
||||||
|
}
|
||||||
63
src/main.rs
Normal file
63
src/main.rs
Normal file
@@ -0,0 +1,63 @@
|
|||||||
|
mod config;
|
||||||
|
mod notify;
|
||||||
|
mod tracker;
|
||||||
|
mod updates;
|
||||||
|
|
||||||
|
use std::path::PathBuf;
|
||||||
|
|
||||||
|
use notify::Notifier;
|
||||||
|
use tracker::UpdateTracker;
|
||||||
|
|
||||||
|
#[tokio::main(flavor = "current_thread")]
|
||||||
|
async fn main() -> Result<(), Box<dyn std::error::Error>> {
|
||||||
|
tracing_subscriber::fmt()
|
||||||
|
.with_env_filter(tracing_subscriber::EnvFilter::from_default_env())
|
||||||
|
.with_writer(std::io::stderr)
|
||||||
|
.init();
|
||||||
|
|
||||||
|
let args: Vec<_> = std::env::args_os().collect();
|
||||||
|
|
||||||
|
let config = match config::load(args.get(1).map(PathBuf::from).as_deref())
|
||||||
|
{
|
||||||
|
Ok(c) => c,
|
||||||
|
Err(e) => {
|
||||||
|
tracing::error!("Failed to load configuration file: {e}");
|
||||||
|
std::process::exit(1);
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
let mut tracker = UpdateTracker::new(&config);
|
||||||
|
let notifier = Notifier::default();
|
||||||
|
for watch in config.watches {
|
||||||
|
let mut new_updates = vec![];
|
||||||
|
for update in updates::get_updates(Some(watch.packages)).await? {
|
||||||
|
match tracker.seen_update(&update).await {
|
||||||
|
Ok(true) => continue,
|
||||||
|
Ok(false) => (),
|
||||||
|
Err(e) => {
|
||||||
|
tracing::error!(
|
||||||
|
"Failed to check update tracker state: {e}"
|
||||||
|
);
|
||||||
|
continue;
|
||||||
|
},
|
||||||
|
};
|
||||||
|
tracing::info!(
|
||||||
|
"Found update {} ({}) for Fedora {} from {}",
|
||||||
|
update.alias,
|
||||||
|
update.title,
|
||||||
|
update.release.version,
|
||||||
|
update.date_stable,
|
||||||
|
);
|
||||||
|
if let Err(e) = tracker.record_update(&update).await {
|
||||||
|
tracing::error!("Failed to record update: {e}");
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
new_updates.push(update);
|
||||||
|
}
|
||||||
|
if let Err(e) = notifier.notify(&new_updates, &watch.on_update).await {
|
||||||
|
tracing::error!("Error sending update notification: {e}");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
215
src/notify.rs
Normal file
215
src/notify.rs
Normal file
@@ -0,0 +1,215 @@
|
|||||||
|
use std::str::FromStr;
|
||||||
|
|
||||||
|
use chrono::DateTime;
|
||||||
|
use chrono::Utc;
|
||||||
|
use reqwest::header::HeaderMap;
|
||||||
|
use reqwest::header::HeaderName;
|
||||||
|
use reqwest::header::HeaderValue;
|
||||||
|
use serde::Serialize;
|
||||||
|
|
||||||
|
use crate::config::{RequestHeader, UpdateHandler};
|
||||||
|
use crate::updates::Update;
|
||||||
|
|
||||||
|
#[derive(Debug)]
|
||||||
|
pub enum Error {
|
||||||
|
Header(HeaderError),
|
||||||
|
Request(reqwest::Error),
|
||||||
|
}
|
||||||
|
|
||||||
|
impl std::fmt::Display for Error {
|
||||||
|
fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
|
||||||
|
match self {
|
||||||
|
Self::Header(e) => write!(f, "{e}"),
|
||||||
|
Self::Request(e) => write!(f, "{e}"),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl std::error::Error for Error {
|
||||||
|
fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
|
||||||
|
match self {
|
||||||
|
Self::Header(e) => Some(e),
|
||||||
|
Self::Request(e) => Some(e),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<HeaderError> for Error {
|
||||||
|
fn from(e: HeaderError) -> Self {
|
||||||
|
Self::Header(e)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<reqwest::Error> for Error {
|
||||||
|
fn from(e: reqwest::Error) -> Self {
|
||||||
|
Self::Request(e)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug)]
|
||||||
|
pub enum HeaderError {
|
||||||
|
InvalidName(reqwest::header::InvalidHeaderName),
|
||||||
|
InvalidValue(reqwest::header::InvalidHeaderValue),
|
||||||
|
Io(std::io::Error),
|
||||||
|
}
|
||||||
|
|
||||||
|
impl std::fmt::Display for HeaderError {
|
||||||
|
fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
|
||||||
|
match self {
|
||||||
|
Self::InvalidName(e) => write!(f, "{e}"),
|
||||||
|
Self::InvalidValue(e) => write!(f, "{e}"),
|
||||||
|
Self::Io(e) => {
|
||||||
|
write!(f, "Error reading header value from file: {e}")
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl std::error::Error for HeaderError {
|
||||||
|
fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
|
||||||
|
match self {
|
||||||
|
Self::InvalidName(e) => Some(e),
|
||||||
|
Self::InvalidValue(e) => Some(e),
|
||||||
|
Self::Io(e) => Some(e),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<reqwest::header::InvalidHeaderName> for HeaderError {
|
||||||
|
fn from(e: reqwest::header::InvalidHeaderName) -> Self {
|
||||||
|
Self::InvalidName(e)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<reqwest::header::InvalidHeaderValue> for HeaderError {
|
||||||
|
fn from(e: reqwest::header::InvalidHeaderValue) -> Self {
|
||||||
|
Self::InvalidValue(e)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<std::io::Error> for HeaderError {
|
||||||
|
fn from(e: std::io::Error) -> Self {
|
||||||
|
Self::Io(e)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Serialize)]
|
||||||
|
struct BuildInfo {
|
||||||
|
nvr: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Serialize)]
|
||||||
|
struct UpdateNotification {
|
||||||
|
id: String,
|
||||||
|
title: String,
|
||||||
|
release: String,
|
||||||
|
date: DateTime<Utc>,
|
||||||
|
builds: Vec<BuildInfo>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<&Update> for UpdateNotification {
|
||||||
|
fn from(update: &Update) -> Self {
|
||||||
|
UpdateNotification {
|
||||||
|
id: update.alias.clone(),
|
||||||
|
title: update.title.clone(),
|
||||||
|
release: update.release.version.clone(),
|
||||||
|
date: update.date_stable,
|
||||||
|
builds: update
|
||||||
|
.builds
|
||||||
|
.iter()
|
||||||
|
.map(|b| BuildInfo {
|
||||||
|
nvr: b.nvr.to_string(),
|
||||||
|
})
|
||||||
|
.collect(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub struct Notifier {
|
||||||
|
client: reqwest::Client,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Default for Notifier {
|
||||||
|
fn default() -> Self {
|
||||||
|
let mut headers = HeaderMap::new();
|
||||||
|
headers.insert(
|
||||||
|
"User-Agent",
|
||||||
|
concat!(env!("CARGO_PKG_NAME"), "/", env!("CARGO_PKG_VERSION"))
|
||||||
|
.parse()
|
||||||
|
.unwrap(),
|
||||||
|
);
|
||||||
|
let client = reqwest::Client::builder()
|
||||||
|
.default_headers(headers)
|
||||||
|
.build()
|
||||||
|
.unwrap();
|
||||||
|
Self::new(client)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Notifier {
|
||||||
|
pub fn new(client: reqwest::Client) -> Self {
|
||||||
|
Self { client }
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn notify(
|
||||||
|
&self,
|
||||||
|
updates: &[Update],
|
||||||
|
handler: &UpdateHandler,
|
||||||
|
) -> Result<(), Error> {
|
||||||
|
use crate::config::HttpMethod;
|
||||||
|
|
||||||
|
let notifications: Vec<_> =
|
||||||
|
updates.iter().map(UpdateNotification::from).collect();
|
||||||
|
|
||||||
|
let mut headermap = HeaderMap::new();
|
||||||
|
if let Some(headers) = &handler.headers {
|
||||||
|
request_headers(headers, &mut headermap)?;
|
||||||
|
}
|
||||||
|
if handler.coalesce {
|
||||||
|
let request = match handler.method {
|
||||||
|
HttpMethod::Get => self.client.get(&handler.url),
|
||||||
|
HttpMethod::Post => {
|
||||||
|
self.client.post(&handler.url).json(¬ifications)
|
||||||
|
},
|
||||||
|
};
|
||||||
|
request
|
||||||
|
.headers(headermap)
|
||||||
|
.send()
|
||||||
|
.await?
|
||||||
|
.error_for_status()?;
|
||||||
|
} else {
|
||||||
|
for notif in notifications {
|
||||||
|
let request = match handler.method {
|
||||||
|
HttpMethod::Get => self.client.get(&handler.url),
|
||||||
|
HttpMethod::Post => {
|
||||||
|
self.client.post(&handler.url).json(¬if)
|
||||||
|
},
|
||||||
|
};
|
||||||
|
request
|
||||||
|
.headers(headermap.clone())
|
||||||
|
.send()
|
||||||
|
.await?
|
||||||
|
.error_for_status()?;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn request_headers(
|
||||||
|
headers: &Vec<RequestHeader>,
|
||||||
|
headermap: &mut HeaderMap,
|
||||||
|
) -> Result<(), HeaderError> {
|
||||||
|
for header in headers {
|
||||||
|
let value = if let Some(filename) = &header.value_file {
|
||||||
|
let value = &std::fs::read_to_string(filename)?;
|
||||||
|
HeaderValue::from_str(value.trim())?
|
||||||
|
} else if let Some(value) = &header.value {
|
||||||
|
HeaderValue::from_str(value)?
|
||||||
|
} else {
|
||||||
|
HeaderValue::from_static("")
|
||||||
|
};
|
||||||
|
headermap.insert(HeaderName::from_str(&header.name)?, value);
|
||||||
|
}
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
117
src/tracker.rs
Normal file
117
src/tracker.rs
Normal file
@@ -0,0 +1,117 @@
|
|||||||
|
use std::collections::HashSet;
|
||||||
|
use std::path::Path;
|
||||||
|
use std::path::PathBuf;
|
||||||
|
|
||||||
|
use tokio::sync::RwLock;
|
||||||
|
|
||||||
|
use crate::config::Configuration;
|
||||||
|
use crate::updates::Update;
|
||||||
|
|
||||||
|
#[derive(Debug)]
|
||||||
|
pub enum Error {
|
||||||
|
Io(std::io::Error),
|
||||||
|
}
|
||||||
|
|
||||||
|
impl std::fmt::Display for Error {
|
||||||
|
fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
|
||||||
|
match self {
|
||||||
|
Self::Io(e) => write!(f, "{e}"),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl std::error::Error for Error {
|
||||||
|
fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
|
||||||
|
match self {
|
||||||
|
Self::Io(e) => Some(e),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<std::io::Error> for Error {
|
||||||
|
fn from(e: std::io::Error) -> Self {
|
||||||
|
Self::Io(e)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub struct UpdateTracker {
|
||||||
|
state_file: PathBuf,
|
||||||
|
seen: RwLock<Option<HashSet<String>>>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl UpdateTracker {
|
||||||
|
pub fn new(config: &Configuration) -> Self {
|
||||||
|
let mut state_file = config.storage.dir.clone();
|
||||||
|
state_file.push("updates.seen");
|
||||||
|
Self {
|
||||||
|
state_file,
|
||||||
|
seen: Default::default(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn seen_update(&self, update: &Update) -> Result<bool, Error> {
|
||||||
|
tracing::debug!("Checking if update {} has been seen", update.alias);
|
||||||
|
let lock = self.seen.read().await;
|
||||||
|
let seen = if let Some(seen) = &*lock {
|
||||||
|
seen.contains(&update.alias)
|
||||||
|
} else {
|
||||||
|
drop(lock);
|
||||||
|
let mut lock = self.seen.write().await;
|
||||||
|
let seen = load_seen(&self.state_file)?;
|
||||||
|
let r = seen.contains(&update.alias);
|
||||||
|
lock.replace(seen);
|
||||||
|
r
|
||||||
|
};
|
||||||
|
if seen {
|
||||||
|
tracing::debug!("Update {} has been seen before", update.alias);
|
||||||
|
} else {
|
||||||
|
tracing::debug!(
|
||||||
|
"Update {} has not been seen before",
|
||||||
|
update.alias
|
||||||
|
);
|
||||||
|
}
|
||||||
|
Ok(seen)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn record_update(
|
||||||
|
&mut self,
|
||||||
|
update: &Update,
|
||||||
|
) -> Result<(), Box<dyn std::error::Error>> {
|
||||||
|
use std::io::Write;
|
||||||
|
tracing::info!("Recording update {} as seen", update.alias);
|
||||||
|
let mut f = std::fs::OpenOptions::new()
|
||||||
|
.append(true)
|
||||||
|
.create(true)
|
||||||
|
.open(&self.state_file)?;
|
||||||
|
f.write_all(
|
||||||
|
format!("{} {}\n", update.alias, update.date_stable).as_bytes(),
|
||||||
|
)?;
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn load_seen(
|
||||||
|
path: impl AsRef<Path>,
|
||||||
|
) -> Result<HashSet<String>, std::io::Error> {
|
||||||
|
use std::io::BufRead;
|
||||||
|
use std::io::ErrorKind::NotFound;
|
||||||
|
tracing::info!("Reading seen updates from {}", path.as_ref().display());
|
||||||
|
let mut seen = HashSet::new();
|
||||||
|
let f = match std::fs::File::open(path) {
|
||||||
|
Ok(f) => f,
|
||||||
|
Err(e) if e.kind() == NotFound => return Ok(seen),
|
||||||
|
Err(e) => return Err(e),
|
||||||
|
};
|
||||||
|
let mut reader = std::io::BufReader::new(f);
|
||||||
|
loop {
|
||||||
|
let mut buf = String::new();
|
||||||
|
let n = reader.read_line(&mut buf)?;
|
||||||
|
if n == 0 {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
if let Some(alias) = buf.split_whitespace().next() {
|
||||||
|
seen.insert(alias.into());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Ok(seen)
|
||||||
|
}
|
||||||
68
src/updates.rs
Normal file
68
src/updates.rs
Normal file
@@ -0,0 +1,68 @@
|
|||||||
|
use std::borrow::Cow;
|
||||||
|
|
||||||
|
use chrono::DateTime;
|
||||||
|
use chrono::NaiveDateTime;
|
||||||
|
use chrono::Utc;
|
||||||
|
use serde::Deserialize;
|
||||||
|
use serde::Deserializer;
|
||||||
|
|
||||||
|
static BODHI_URL: &str = "https://bodhi.fedoraproject.org/";
|
||||||
|
|
||||||
|
#[derive(Debug, Deserialize)]
|
||||||
|
pub struct BuildInfo {
|
||||||
|
pub nvr: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Deserialize)]
|
||||||
|
pub struct Release {
|
||||||
|
pub version: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Deserialize)]
|
||||||
|
pub struct Update {
|
||||||
|
pub title: String,
|
||||||
|
pub release: Release,
|
||||||
|
pub alias: String,
|
||||||
|
#[serde(deserialize_with = "deserialize_date")]
|
||||||
|
pub date_stable: DateTime<Utc>,
|
||||||
|
pub builds: Vec<BuildInfo>,
|
||||||
|
}
|
||||||
|
|
||||||
|
fn deserialize_date<'de, D>(deserializer: D) -> Result<DateTime<Utc>, D::Error>
|
||||||
|
where
|
||||||
|
D: Deserializer<'de>,
|
||||||
|
{
|
||||||
|
let s = String::deserialize(deserializer)?;
|
||||||
|
let dt = NaiveDateTime::parse_from_str(&s, "%Y-%m-%d %H:%M:%S")
|
||||||
|
.map_err(serde::de::Error::custom)?;
|
||||||
|
Ok(DateTime::<Utc>::from_naive_utc_and_offset(dt, Utc))
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Deserialize)]
|
||||||
|
struct UpdatesResponse {
|
||||||
|
updates: Vec<Update>,
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn get_updates(
|
||||||
|
packages: Option<impl AsRef<str>>,
|
||||||
|
) -> Result<Vec<Update>, reqwest::Error> {
|
||||||
|
let url: Cow<'_, str> = std::env::var("BODHI_URL")
|
||||||
|
.map(Cow::Owned)
|
||||||
|
.unwrap_or_else(|_| Cow::Borrowed(BODHI_URL));
|
||||||
|
let mut url = reqwest::Url::parse(&url)
|
||||||
|
.unwrap()
|
||||||
|
.join("/updates/")
|
||||||
|
.unwrap();
|
||||||
|
{
|
||||||
|
let mut query = url.query_pairs_mut();
|
||||||
|
query.append_pair("rows_per_page", "10");
|
||||||
|
query.append_pair("status", "stable");
|
||||||
|
query.append_pair("releases", "__current__");
|
||||||
|
if let Some(packages) = packages {
|
||||||
|
query.append_pair("packages", packages.as_ref());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
tracing::info!("Retrieving updates from {url}");
|
||||||
|
let updates: UpdatesResponse = reqwest::get(url).await?.json().await?;
|
||||||
|
Ok(updates.updates)
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user