]> git.proxmox.com Git - proxmox-backup.git/blame - src/tools/apt.rs
clippy: remove unnecessary closures
[proxmox-backup.git] / src / tools / apt.rs
CommitLineData
e6513bd5 1use std::collections::HashSet;
86d60245 2use std::collections::HashMap;
e6513bd5 3
33508b12 4use anyhow::{Error, bail, format_err};
e6513bd5
TL
5use apt_pkg_native::Cache;
6
7use proxmox::const_regex;
33508b12 8use proxmox::tools::fs::{file_read_optional_string, replace_file, CreateOptions};
e6513bd5
TL
9
10use crate::api2::types::APTUpdateInfo;
11
33508b12
TL
12const APT_PKG_STATE_FN: &str = "/var/lib/proxmox-backup/pkg-state.json";
13
14#[derive(Debug, serde::Serialize, serde::Deserialize)]
86d60245
TL
15/// Some information we cache about the package (update) state, like what pending update version
16/// we already notfied an user about
33508b12 17pub struct PkgState {
86d60245
TL
18 /// simple map from package name to most recently notified (emailed) version
19 pub notified: Option<HashMap<String, String>>,
33508b12
TL
20 /// A list of pending updates
21 pub package_status: Vec<APTUpdateInfo>,
22}
23
24pub fn write_pkg_cache(state: &PkgState) -> Result<(), Error> {
25 let serialized_state = serde_json::to_string(state)?;
26
27 replace_file(APT_PKG_STATE_FN, &serialized_state.as_bytes(), CreateOptions::new())
28 .map_err(|err| format_err!("Error writing package cache - {}", err))?;
29 Ok(())
30}
31
32pub fn read_pkg_state() -> Result<Option<PkgState>, Error> {
33 let serialized_state = match file_read_optional_string(&APT_PKG_STATE_FN) {
34 Ok(Some(raw)) => raw,
35 Ok(None) => return Ok(None),
36 Err(err) => bail!("could not read cached package state file - {}", err),
37 };
38
39 serde_json::from_str(&serialized_state)
22a9189e 40 .map(Some)
33508b12
TL
41 .map_err(|err| format_err!("could not parse cached package status - {}", err))
42}
43
44pub 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")?;
48
49 let mtime = pbs_cache.modified()?;
50
51 if apt_pkgcache.modified()? <= mtime && dpkg_status.modified()? <= mtime {
52 return Ok(false);
53 }
54 }
55 Ok(true)
56}
57
58pub fn update_cache() -> Result<PkgState, Error> {
59 // update our cache
60 let all_upgradeable = list_installed_apt_packages(|data| {
61 data.candidate_version == data.active_version &&
62 data.installed_version != Some(data.candidate_version)
63 }, None);
64
65 let cache = match read_pkg_state() {
66 Ok(Some(mut cache)) => {
67 cache.package_status = all_upgradeable;
68 cache
69 },
70 _ => PkgState {
86d60245 71 notified: None,
33508b12
TL
72 package_status: all_upgradeable,
73 },
74 };
75 write_pkg_cache(&cache)?;
76 Ok(cache)
77}
78
79
e6513bd5
TL
80const_regex! {
81 VERSION_EPOCH_REGEX = r"^\d+:";
82 FILENAME_EXTRACT_REGEX = r"^.*/.*?_(.*)_Packages$";
83}
84
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)
88fn get_changelog_url(
89 package: &str,
90 filename: &str,
91 version: &str,
92 origin: &str,
93 component: &str,
94) -> Result<String, Error> {
95 if origin == "" {
96 bail!("no origin available for package {}", package);
97 }
98
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() {
106 Some(output) => {
107 if output.len() < 2 {
108 bail!("invalid output (URI part too short) from 'apt-get changelog --print-uris': {}", output)
109 }
110 output[1..output.len()-1].to_owned()
111 },
112 None => bail!("invalid output from 'apt-get changelog --print-uris': {}", output)
113 };
114 return Ok(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, "");
119
120 let base = match (FILENAME_EXTRACT_REGEX.regex_obj)().captures(filename) {
121 Some(captures) => {
122 let base_capture = captures.get(1);
123 match base_capture {
124 Some(base_underscore) => base_underscore.as_str().replace("_", "/"),
125 None => bail!("incompatible filename, cannot find regex group")
126 }
127 },
128 None => bail!("incompatible filename, doesn't match regex")
129 };
130
137a6ebc
SR
131 if component == "pbs-enterprise" {
132 return Ok(format!("https://enterprise.proxmox.com/{}/{}_{}.changelog",
133 base, package, version));
134 } else {
135 return Ok(format!("http://download.proxmox.com/{}/{}_{}.changelog",
136 base, package, version));
137 }
e6513bd5
TL
138 }
139
140 bail!("unknown origin ({}) or component ({})", origin, component)
141}
142
143pub struct FilterData<'a> {
38260cdd
TL
144 /// package name
145 pub package: &'a str,
146 /// this is version info returned by APT
e6513bd5
TL
147 pub installed_version: Option<&'a str>,
148 pub candidate_version: &'a str,
149
38260cdd 150 /// this is the version info the filter is supposed to check
e6513bd5
TL
151 pub active_version: &'a str,
152}
153
154enum PackagePreSelect {
155 OnlyInstalled,
156 OnlyNew,
157 All,
158}
159
160pub fn list_installed_apt_packages<F: Fn(FilterData) -> bool>(
161 filter: F,
162 only_versions_for: Option<&str>,
163) -> Vec<APTUpdateInfo> {
164
165 let mut ret = Vec::new();
166 let mut depends = HashSet::new();
167
168 // note: this is not an 'apt update', it just re-reads the cache from disk
169 let mut cache = Cache::get_singleton();
170 cache.reload();
171
172 let mut cache_iter = match only_versions_for {
173 Some(name) => cache.find_by_name(name),
174 None => cache.iter()
175 };
176
177 loop {
178
179 match cache_iter.next() {
180 Some(view) => {
181 let di = if only_versions_for.is_some() {
182 query_detailed_info(
183 PackagePreSelect::All,
184 &filter,
185 view,
186 None
187 )
188 } else {
189 query_detailed_info(
190 PackagePreSelect::OnlyInstalled,
191 &filter,
192 view,
193 Some(&mut depends)
194 )
195 };
196 if let Some(info) = di {
197 ret.push(info);
198 }
199
200 if only_versions_for.is_some() {
201 break;
202 }
203 },
204 None => {
205 drop(cache_iter);
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() {
210 Some(view) => view,
211 None => continue // package not found, ignore
212 };
213
214 let di = query_detailed_info(
215 PackagePreSelect::OnlyNew,
216 &filter,
217 view,
218 None
219 );
220 if let Some(info) = di {
221 ret.push(info);
222 }
223 }
224 break;
225 }
226 }
227 }
228
229 return ret;
230}
231
232fn query_detailed_info<'a, F, V>(
233 pre_select: PackagePreSelect,
234 filter: F,
235 view: V,
236 depends: Option<&mut HashSet<String>>,
237) -> Option<APTUpdateInfo>
238where
239 F: Fn(FilterData) -> bool,
240 V: std::ops::Deref<Target = apt_pkg_native::sane::PkgView<'a>>
241{
242 let current_version = view.current_version();
243 let candidate_version = view.candidate_version();
244
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
251 },
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,
257 },
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,
263 },
264 };
265
266 // get additional information via nested APT 'iterators'
267 let mut view_iter = view.versions();
268 while let Some(ver) = view_iter.next() {
269
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();
278
279 let fd = FilterData {
38260cdd 280 package: package.as_str(),
e6513bd5
TL
281 installed_version: current_version.as_deref(),
282 candidate_version: &candidate_version,
283 active_version: &version,
284 };
285
286 if filter(fd) {
287 if let Some(section) = ver.section() {
288 section_res = section;
289 }
290
291 if let Some(prio) = ver.priority_type() {
292 priority_res = prio;
293 }
294
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 {
301
302 if let Some(sd) = origin.short_desc() {
303 short_desc = sd;
304 }
305
306 if let Some(ld) = origin.long_desc() {
307 long_desc = ld;
308 }
309
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
313 // versions anyway
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;
319 }
320
321 let filename = pkg_file.file_name();
322 let component = pkg_file.component();
323
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;
330 }
331 }
332 }
333
334 if let Some(depends) = depends {
335 let mut dep_iter = ver.dep_iter();
336 loop {
337 let dep = match dep_iter.next() {
338 Some(dep) if dep.dep_type() != "Depends" => continue,
339 Some(dep) => dep,
340 None => break
341 };
342
343 let dep_pkg = dep.target_pkg();
344 let name = dep_pkg.name();
345
346 depends.insert(name);
347 }
348 }
349
350 return Some(APTUpdateInfo {
351 package,
352 title: short_desc,
353 arch: view.arch(),
354 description: long_desc,
355 change_log_url,
356 origin: origin_res,
357 version: candidate_version.clone(),
358 old_version: match current_version {
359 Some(vers) => vers,
360 None => "".to_owned()
361 },
362 priority: priority_res,
363 section: section_res,
2decf85d 364 extra_info: None,
e6513bd5
TL
365 });
366 }
367 }
368
369 return None;
370}