1 use std
::collections
::HashSet
;
2 use std
::collections
::HashMap
;
4 use anyhow
::{Error, bail, format_err}
;
5 use apt_pkg_native
::Cache
;
7 use proxmox
::const_regex
;
8 use proxmox
::tools
::fs
::{file_read_optional_string, replace_file, CreateOptions}
;
10 use crate::api2
::types
::APTUpdateInfo
;
12 const APT_PKG_STATE_FN
: &str = "/var/lib/proxmox-backup/pkg-state.json";
14 #[derive(Debug, serde::Serialize, serde::Deserialize)]
15 /// Some information we cache about the package (update) state, like what pending update version
16 /// we already notfied an user about
18 /// simple map from package name to most recently notified (emailed) version
19 pub notified
: Option
<HashMap
<String
, String
>>,
20 /// A list of pending updates
21 pub package_status
: Vec
<APTUpdateInfo
>,
24 pub fn write_pkg_cache(state
: &PkgState
) -> Result
<(), Error
> {
25 let serialized_state
= serde_json
::to_string(state
)?
;
27 replace_file(APT_PKG_STATE_FN
, &serialized_state
.as_bytes(), CreateOptions
::new())
28 .map_err(|err
| format_err
!("Error writing package cache - {}", err
))?
;
32 pub fn read_pkg_state() -> Result
<Option
<PkgState
>, Error
> {
33 let serialized_state
= match file_read_optional_string(&APT_PKG_STATE_FN
) {
35 Ok(None
) => return Ok(None
),
36 Err(err
) => bail
!("could not read cached package state file - {}", err
),
39 serde_json
::from_str(&serialized_state
)
41 .map_err(|err
| format_err
!("could not parse cached package status - {}", err
))
44 pub fn pkg_cache_expired () -> Result
<bool
, Error
> {
45 if let Ok(pbs_cache
) = std
::fs
::metadata(APT_PKG_STATE_FN
) {
46 let apt_pkgcache
= std
::fs
::metadata("/var/cache/apt/pkgcache.bin")?
;
47 let dpkg_status
= std
::fs
::metadata("/var/lib/dpkg/status")?
;
49 let mtime
= pbs_cache
.modified()?
;
51 if apt_pkgcache
.modified()?
<= mtime
&& dpkg_status
.modified()?
<= mtime
{
58 pub fn update_cache() -> Result
<PkgState
, Error
> {
60 let all_upgradeable
= list_installed_apt_packages(|data
| {
61 data
.candidate_version
== data
.active_version
&&
62 data
.installed_version
!= Some(data
.candidate_version
)
65 let cache
= match read_pkg_state() {
66 Ok(Some(mut cache
)) => {
67 cache
.package_status
= all_upgradeable
;
72 package_status
: all_upgradeable
,
75 write_pkg_cache(&cache
)?
;
81 VERSION_EPOCH_REGEX
= r
"^\d+:";
82 FILENAME_EXTRACT_REGEX
= r
"^.*/.*?_(.*)_Packages$";
85 // FIXME: once the 'changelog' API call switches over to 'apt-get changelog' only,
86 // consider removing this function entirely, as it's value is never used anywhere
87 // then (widget-toolkit doesn't use the value either)
94 ) -> Result
<String
, Error
> {
96 bail
!("no origin available for package {}", package
);
99 if origin
== "Debian" {
100 let mut command
= std
::process
::Command
::new("apt-get");
101 command
.arg("changelog");
102 command
.arg("--print-uris");
103 command
.arg(package
);
104 let output
= crate::tools
::run_command(command
, None
)?
; // format: 'http://foo/bar' package.changelog
105 let output
= match output
.splitn(2, ' '
).next() {
107 if output
.len() < 2 {
108 bail
!("invalid output (URI part too short) from 'apt-get changelog --print-uris': {}", output
)
110 output
[1..output
.len()-1].to_owned()
112 None
=> bail
!("invalid output from 'apt-get changelog --print-uris': {}", output
)
115 } else if origin
== "Proxmox" {
116 // FIXME: Use above call to 'apt changelog <pkg> --print-uris' as well.
117 // Currently not possible as our packages do not have a URI set in their Release file.
118 let version
= (VERSION_EPOCH_REGEX
.regex_obj
)().replace_all(version
, "");
120 let base
= match (FILENAME_EXTRACT_REGEX
.regex_obj
)().captures(filename
) {
122 let base_capture
= captures
.get(1);
124 Some(base_underscore
) => base_underscore
.as_str().replace("_", "/"),
125 None
=> bail
!("incompatible filename, cannot find regex group")
128 None
=> bail
!("incompatible filename, doesn't match regex")
131 if component
== "pbs-enterprise" {
132 return Ok(format
!("https://enterprise.proxmox.com/{}/{}_{}.changelog",
133 base
, package
, version
));
135 return Ok(format
!("http://download.proxmox.com/{}/{}_{}.changelog",
136 base
, package
, version
));
140 bail
!("unknown origin ({}) or component ({})", origin
, component
)
143 pub struct FilterData
<'a
> {
145 pub package
: &'a
str,
146 /// this is version info returned by APT
147 pub installed_version
: Option
<&'a
str>,
148 pub candidate_version
: &'a
str,
150 /// this is the version info the filter is supposed to check
151 pub active_version
: &'a
str,
154 enum PackagePreSelect
{
160 pub fn list_installed_apt_packages
<F
: Fn(FilterData
) -> bool
>(
162 only_versions_for
: Option
<&str>,
163 ) -> Vec
<APTUpdateInfo
> {
165 let mut ret
= Vec
::new();
166 let mut depends
= HashSet
::new();
168 // note: this is not an 'apt update', it just re-reads the cache from disk
169 let mut cache
= Cache
::get_singleton();
172 let mut cache_iter
= match only_versions_for
{
173 Some(name
) => cache
.find_by_name(name
),
179 match cache_iter
.next() {
181 let di
= if only_versions_for
.is_some() {
183 PackagePreSelect
::All
,
190 PackagePreSelect
::OnlyInstalled
,
196 if let Some(info
) = di
{
200 if only_versions_for
.is_some() {
206 // also loop through missing dependencies, as they would be installed
207 for pkg
in depends
.iter() {
208 let mut iter
= cache
.find_by_name(&pkg
);
209 let view
= match iter
.next() {
211 None
=> continue // package not found, ignore
214 let di
= query_detailed_info(
215 PackagePreSelect
::OnlyNew
,
220 if let Some(info
) = di
{
232 fn query_detailed_info
<'a
, F
, V
>(
233 pre_select
: PackagePreSelect
,
236 depends
: Option
<&mut HashSet
<String
>>,
237 ) -> Option
<APTUpdateInfo
>
239 F
: Fn(FilterData
) -> bool
,
240 V
: std
::ops
::Deref
<Target
= apt_pkg_native
::sane
::PkgView
<'a
>>
242 let current_version
= view
.current_version();
243 let candidate_version
= view
.candidate_version();
245 let (current_version
, candidate_version
) = match pre_select
{
246 PackagePreSelect
::OnlyInstalled
=> match (current_version
, candidate_version
) {
247 (Some(cur
), Some(can
)) => (Some(cur
), can
), // package installed and there is an update
248 (Some(cur
), None
) => (Some(cur
.clone()), cur
), // package installed and up-to-date
249 (None
, Some(_
)) => return None
, // package could be installed
250 (None
, None
) => return None
, // broken
252 PackagePreSelect
::OnlyNew
=> match (current_version
, candidate_version
) {
253 (Some(_
), Some(_
)) => return None
,
254 (Some(_
), None
) => return None
,
255 (None
, Some(can
)) => (None
, can
),
256 (None
, None
) => return None
,
258 PackagePreSelect
::All
=> match (current_version
, candidate_version
) {
259 (Some(cur
), Some(can
)) => (Some(cur
), can
),
260 (Some(cur
), None
) => (Some(cur
.clone()), cur
),
261 (None
, Some(can
)) => (None
, can
),
262 (None
, None
) => return None
,
266 // get additional information via nested APT 'iterators'
267 let mut view_iter
= view
.versions();
268 while let Some(ver
) = view_iter
.next() {
270 let package
= view
.name();
271 let version
= ver
.version();
272 let mut origin_res
= "unknown".to_owned();
273 let mut section_res
= "unknown".to_owned();
274 let mut priority_res
= "unknown".to_owned();
275 let mut change_log_url
= "".to_owned();
276 let mut short_desc
= package
.clone();
277 let mut long_desc
= "".to_owned();
279 let fd
= FilterData
{
280 package
: package
.as_str(),
281 installed_version
: current_version
.as_deref(),
282 candidate_version
: &candidate_version
,
283 active_version
: &version
,
287 if let Some(section
) = ver
.section() {
288 section_res
= section
;
291 if let Some(prio
) = ver
.priority_type() {
295 // assume every package has only one origin file (not
296 // origin, but origin *file*, for some reason those seem to
297 // be different concepts in APT)
298 let mut origin_iter
= ver
.origin_iter();
299 let origin
= origin_iter
.next();
300 if let Some(origin
) = origin
{
302 if let Some(sd
) = origin
.short_desc() {
306 if let Some(ld
) = origin
.long_desc() {
310 // the package files appear in priority order, meaning
311 // the one for the candidate version is first - this is fine
312 // however, as the source package should be the same for all
314 let mut pkg_iter
= origin
.file();
315 let pkg_file
= pkg_iter
.next();
316 if let Some(pkg_file
) = pkg_file
{
317 if let Some(origin_name
) = pkg_file
.origin() {
318 origin_res
= origin_name
;
321 let filename
= pkg_file
.file_name();
322 let component
= pkg_file
.component();
324 // build changelog URL from gathered information
325 // ignore errors, use empty changelog instead
326 let url
= get_changelog_url(&package
, &filename
,
327 &version
, &origin_res
, &component
);
328 if let Ok(url
) = url
{
329 change_log_url
= url
;
334 if let Some(depends
) = depends
{
335 let mut dep_iter
= ver
.dep_iter();
337 let dep
= match dep_iter
.next() {
338 Some(dep
) if dep
.dep_type() != "Depends" => continue,
343 let dep_pkg
= dep
.target_pkg();
344 let name
= dep_pkg
.name();
346 depends
.insert(name
);
350 return Some(APTUpdateInfo
{
354 description
: long_desc
,
357 version
: candidate_version
.clone(),
358 old_version
: match current_version
{
360 None
=> "".to_owned()
362 priority
: priority_res
,
363 section
: section_res
,