diff --git a/src/api2/node/apt.rs b/src/api2/node/apt.rs index 49a46324..f720dee6 100644 --- a/src/api2/node/apt.rs +++ b/src/api2/node/apt.rs @@ -26,17 +26,26 @@ use crate::api2::types::{Authid, APTUpdateInfo, NODE_SCHEMA, UPID_SCHEMA}; type: APTUpdateInfo }, }, + protected: true, access: { permission: &Permission::Privilege(&[], PRIV_SYS_AUDIT, false), }, )] /// List available APT updates fn apt_update_available(_param: Value) -> Result { - let all_upgradeable = apt::list_installed_apt_packages(|data| { - data.candidate_version == data.active_version && - data.installed_version != Some(data.candidate_version) - }, None); - Ok(json!(all_upgradeable)) + + match apt::pkg_cache_expired() { + Ok(false) => { + if let Ok(Some(cache)) = apt::read_pkg_state() { + return Ok(json!(cache.package_status)); + } + }, + _ => (), + } + + let cache = apt::update_cache()?; + + return Ok(json!(cache.package_status)); } fn do_apt_update(worker: &WorkerTask, quiet: bool) -> Result<(), Error> { diff --git a/src/tools/apt.rs b/src/tools/apt.rs index 0ffb8143..4c314f90 100644 --- a/src/tools/apt.rs +++ b/src/tools/apt.rs @@ -1,12 +1,77 @@ use std::collections::HashSet; -use anyhow::{Error, bail}; +use anyhow::{Error, bail, format_err}; use apt_pkg_native::Cache; use proxmox::const_regex; +use proxmox::tools::fs::{file_read_optional_string, replace_file, CreateOptions}; use crate::api2::types::APTUpdateInfo; +const APT_PKG_STATE_FN: &str = "/var/lib/proxmox-backup/pkg-state.json"; + +#[derive(Debug, serde::Serialize, serde::Deserialize)] +/// Some information we cache about the package (update) state +pub struct PkgState { + /// A list of pending updates + pub package_status: Vec, +} + +pub fn write_pkg_cache(state: &PkgState) -> Result<(), Error> { + let serialized_state = serde_json::to_string(state)?; + + replace_file(APT_PKG_STATE_FN, &serialized_state.as_bytes(), CreateOptions::new()) + .map_err(|err| format_err!("Error writing package cache - {}", err))?; + Ok(()) +} + +pub fn read_pkg_state() -> Result, Error> { + let serialized_state = match file_read_optional_string(&APT_PKG_STATE_FN) { + Ok(Some(raw)) => raw, + Ok(None) => return Ok(None), + Err(err) => bail!("could not read cached package state file - {}", err), + }; + + serde_json::from_str(&serialized_state) + .map(|s| Some(s)) + .map_err(|err| format_err!("could not parse cached package status - {}", err)) +} + +pub fn pkg_cache_expired () -> Result { + if let Ok(pbs_cache) = std::fs::metadata(APT_PKG_STATE_FN) { + let apt_pkgcache = std::fs::metadata("/var/cache/apt/pkgcache.bin")?; + let dpkg_status = std::fs::metadata("/var/lib/dpkg/status")?; + + let mtime = pbs_cache.modified()?; + + if apt_pkgcache.modified()? <= mtime && dpkg_status.modified()? <= mtime { + return Ok(false); + } + } + Ok(true) +} + +pub fn update_cache() -> Result { + // update our cache + let all_upgradeable = list_installed_apt_packages(|data| { + data.candidate_version == data.active_version && + data.installed_version != Some(data.candidate_version) + }, None); + + let cache = match read_pkg_state() { + Ok(Some(mut cache)) => { + cache.package_status = all_upgradeable; + cache + }, + _ => PkgState { + package_status: all_upgradeable, + }, + }; + write_pkg_cache(&cache)?; + Ok(cache) +} + + const_regex! { VERSION_EPOCH_REGEX = r"^\d+:"; FILENAME_EXTRACT_REGEX = r"^.*/.*?_(.*)_Packages$";