// SPDX-License-Identifier: LGPL-3.0-or-later // // Copyright 2019 Hristo Venev use super::Source; use crate::{config, fileutil, proto}; use std::ffi::{OsStr, OsString}; use std::path::PathBuf; use std::time::{Duration, Instant}; use std::{fs, io}; pub(super) struct Updater { config: config::UpdaterConfig, } impl Updater { pub fn new(config: config::UpdaterConfig) -> Self { Self { config } } fn cache_path(&self, s: &Source) -> Option { let mut p = self.config.cache_directory.as_ref()?.clone(); p.push(&s.name); Some(p) } fn cache_update(&self, src: &Source) { let path = match self.cache_path(src) { Some(v) => v, None => return, }; let data = serde_json::to_vec(&src.data).unwrap(); match fileutil::update(&path, &data) { Ok(()) => {} Err(e) => { eprintln!("<4>Failed to cache [{}]: {}", &src.name, e); } } } pub fn cache_load(&self, src: &mut Source) -> bool { let path = match self.cache_path(src) { Some(v) => v, None => return false, }; let data = match fileutil::load(&path) { Ok(Some(data)) => data, Ok(None) => { return false; } Err(e) => { eprintln!("<3>Failed to read [{}] from cache: {}", &src.name, e); return false; } }; let mut de = serde_json::Deserializer::from_slice(&data); src.data = match serde::Deserialize::deserialize(&mut de) { Ok(r) => r, Err(e) => { eprintln!("<3>Failed to load [{}] from cache: {}", &src.name, e); return false; } }; true } pub fn update(&self, src: &mut Source) -> (bool, Instant) { let refresh = self.refresh_time(); let r = fetch_source(&src.config.url); let now = Instant::now(); let r = match r { Ok(r) => { eprintln!("<6>Updated [{}]", &src.config.url); src.data = r; src.backoff = None; src.next_update = now + refresh; self.cache_update(src); return (true, now); } Err(r) => r, }; let b = src .backoff .unwrap_or_else(|| Duration::from_secs(10).min(refresh / 10)); src.next_update = now + b; src.backoff = Some((b + b / 3).min(refresh / 3)); eprintln!( "<3>Failed to update [{}], retrying after {:.1?}: {}", &src.config.url, b, &r ); (false, now) } pub fn refresh_time(&self) -> Duration { Duration::from_secs(u64::from(self.config.refresh_sec)) } } fn fetch_source(url: &str) -> io::Result { use std::env; use std::process::{Command, Stdio}; let curl = match env::var_os("CURL") { None => OsString::new(), Some(v) => v, }; let mut proc = Command::new(if curl.is_empty() { OsStr::new("curl") } else { curl.as_os_str() }); proc.stdin(Stdio::null()); proc.stdout(Stdio::piped()); proc.stderr(Stdio::piped()); proc.arg("-gsSfL"); proc.arg("--fail-early"); proc.arg("--max-time"); proc.arg("10"); proc.arg("--max-filesize"); proc.arg("1M"); proc.arg("--"); proc.arg(url); let out = proc.output()?; if !out.status.success() { let msg = String::from_utf8_lossy(&out.stderr); let msg = msg.replace('\n', "; "); return Err(io::Error::new(io::ErrorKind::Other, msg)); } let mut de = serde_json::Deserializer::from_slice(&out.stdout); let r = serde::Deserialize::deserialize(&mut de)?; Ok(r) } pub fn load_source(path: &OsStr) -> io::Result { let mut data = Vec::new(); { use std::io::Read; let mut f = fs::File::open(&path)?; f.read_to_end(&mut data)?; } let mut de = serde_json::Deserializer::from_slice(&data); let r = serde::Deserialize::deserialize(&mut de)?; Ok(r) }