upstream_rs/application/operations/
rollback_op.rs1use anyhow::{Result, anyhow};
2
3use crate::{
4 output,
5 services::packaging::{
6 RollbackManager,
7 disk_impact::{ByteEstimate, DiskImpact, SignedByteEstimate},
8 },
9 storage::{
10 database::PackageDatabase,
11 rollback::{RollbackSource, RollbackStorage},
12 },
13 utils::static_paths::UpstreamPaths,
14};
15
16pub struct RollbackOperation {
17 paths: UpstreamPaths,
18 package_database: PackageDatabase,
19 rollback_storage: RollbackStorage,
20}
21
22pub struct RollbackPreviewRow {
23 pub package: String,
24 pub version: String,
25 pub net_change: SignedByteEstimate,
26}
27
28pub struct RollbackRestoreTarget {
29 pub name: String,
30 pub install_path: String,
31 pub source: RollbackSource,
32}
33
34pub struct RollbackPreview {
35 pub rows: Vec<RollbackPreviewRow>,
36 pub impact: DiskImpact,
37 pub missing_names: Vec<String>,
38}
39
40pub struct RollbackRestorePreview {
41 pub preview: RollbackPreview,
42 pub targets: Vec<RollbackRestoreTarget>,
43}
44
45pub struct RollbackPrunePreview {
46 pub target_names: Vec<String>,
47 pub preview: RollbackPreview,
48}
49
50pub struct RollbackListRow {
51 pub name: String,
52 pub version: String,
53 pub source: RollbackSource,
54 pub install_path: String,
55}
56
57pub enum RollbackPackageStatus {
58 Succeeded,
59 Failed { error: String },
60 Skipped { reason: String },
61}
62
63pub struct RollbackPackageOutcome {
64 pub name: String,
65 pub status: RollbackPackageStatus,
66}
67
68pub struct RollbackRestoreOutcome {
69 pub restored: u32,
70 pub failed: u32,
71 pub packages: Vec<RollbackPackageOutcome>,
72}
73
74pub struct RollbackPruneOutcome {
75 pub pruned: u32,
76 pub missing: u32,
77 pub packages: Vec<RollbackPackageOutcome>,
78}
79
80impl RollbackOperation {
81 pub fn new() -> Result<Self> {
82 let paths = UpstreamPaths::new()?;
83 let package_database = PackageDatabase::open(&paths.config.packages_database_file)?;
84 let rollback_file = RollbackManager::rollback_file_path(&paths);
85 let rollback_storage = RollbackStorage::new(&rollback_file)?;
86
87 Ok(Self {
88 paths,
89 package_database,
90 rollback_storage,
91 })
92 }
93
94 fn manager(&mut self) -> RollbackManager<'_> {
95 RollbackManager::new(
96 &self.paths,
97 &mut self.package_database,
98 &mut self.rollback_storage,
99 )
100 }
101
102 pub fn restore_preview(&mut self, names: &[String]) -> Result<RollbackRestorePreview> {
103 if names.is_empty() {
104 return Err(anyhow!(
105 "At least one package name is required unless --prune is provided"
106 ));
107 }
108
109 let manager = self.manager();
110 let preview = restore_preview(names, &manager);
111 let targets = names
112 .iter()
113 .filter_map(|name| {
114 let record = manager.rollback_record(name)?;
115 Some(RollbackRestoreTarget {
116 name: name.clone(),
117 install_path: record
118 .package_snapshot
119 .install_path
120 .as_ref()
121 .map(|path| path.display().to_string())
122 .unwrap_or_else(|| "<missing>".to_string()),
123 source: record.source.clone(),
124 })
125 })
126 .collect();
127
128 Ok(RollbackRestorePreview { preview, targets })
129 }
130
131 pub fn list_rows(&mut self) -> Vec<RollbackListRow> {
132 let manager = self.manager();
133 manager
134 .rollback_packages()
135 .into_iter()
136 .filter_map(|name| {
137 let record = manager.rollback_record(&name)?;
138 let package = &record.package_snapshot;
139 Some(RollbackListRow {
140 name,
141 version: package.version.to_string(),
142 source: record.source.clone(),
143 install_path: package
144 .install_path
145 .as_ref()
146 .map(|path| path.display().to_string())
147 .unwrap_or_else(|| "-".to_string()),
148 })
149 })
150 .collect()
151 }
152
153 pub fn restorable_names(&mut self, names: &[String]) -> Vec<String> {
154 let manager = self.manager();
155 names
156 .iter()
157 .filter(|name| manager.rollback_record(name).is_some())
158 .cloned()
159 .collect()
160 }
161
162 pub fn restore<H>(
163 &mut self,
164 names: &[String],
165 message_callback: &mut Option<H>,
166 ) -> Result<RollbackRestoreOutcome>
167 where
168 H: FnMut(&str, &str),
169 {
170 let restorable_names = self.restorable_names(names);
171
172 let mut restored = 0_u32;
173 let mut failed = 0_u32;
174 let mut packages = Vec::new();
175 {
176 let mut manager = self.manager();
177 for name in &restorable_names {
178 let package_name = name.clone();
179 let mut msg = Some(|line: &str| {
180 if let Some(callback) = message_callback.as_mut() {
181 callback(&package_name, line);
182 }
183 });
184
185 match manager.restore_package(name, &mut msg) {
186 Ok(_) => {
187 packages.push(RollbackPackageOutcome {
188 name: name.clone(),
189 status: RollbackPackageStatus::Succeeded,
190 });
191 restored += 1;
192 }
193 Err(err) => {
194 let summary = output::error_summary(&err);
195 packages.push(RollbackPackageOutcome {
196 name: name.clone(),
197 status: RollbackPackageStatus::Failed {
198 error: summary.clone(),
199 },
200 });
201 failed += 1;
202 }
203 }
204 }
205 }
206
207 Ok(RollbackRestoreOutcome {
208 restored,
209 failed,
210 packages,
211 })
212 }
213
214 pub fn prune_preview(&mut self, names: Vec<String>) -> RollbackPrunePreview {
215 let manager = self.manager();
216 let target_names = if names.is_empty() {
217 manager.rollback_packages()
218 } else {
219 names
220 };
221 let preview = prune_preview(&target_names, &manager);
222
223 RollbackPrunePreview {
224 target_names,
225 preview,
226 }
227 }
228
229 pub fn prune<H>(
230 &mut self,
231 target_names: &[String],
232 message_callback: &mut Option<H>,
233 ) -> Result<RollbackPruneOutcome>
234 where
235 H: FnMut(&str, usize, usize),
236 {
237 let mut pruned = 0_u32;
238 let mut missing = 0_u32;
239 let mut packages = Vec::new();
240 let total = target_names.len();
241 {
242 let mut manager = self.manager();
243 for (idx, name) in target_names.iter().enumerate() {
244 if let Some(callback) = message_callback.as_mut() {
245 callback(name, idx + 1, total);
246 }
247
248 match manager.prune_package(name) {
249 Ok(true) => {
250 pruned += 1;
251 packages.push(RollbackPackageOutcome {
252 name: name.clone(),
253 status: RollbackPackageStatus::Succeeded,
254 });
255 }
256 Ok(false) => {
257 missing += 1;
258 let reason = "no rollback data found".to_string();
259 packages.push(RollbackPackageOutcome {
260 name: name.clone(),
261 status: RollbackPackageStatus::Skipped {
262 reason: reason.clone(),
263 },
264 });
265 }
266 Err(err) => {
267 let summary = output::error_summary(&err);
268 packages.push(RollbackPackageOutcome {
269 name: name.clone(),
270 status: RollbackPackageStatus::Failed {
271 error: summary.clone(),
272 },
273 });
274 return Err(err);
275 }
276 }
277 }
278 }
279
280 Ok(RollbackPruneOutcome {
281 pruned,
282 missing,
283 packages,
284 })
285 }
286}
287
288fn restore_preview(names: &[String], manager: &RollbackManager<'_>) -> RollbackPreview {
289 let rows = names
290 .iter()
291 .filter_map(|name| {
292 let record = manager.rollback_record(name)?;
293 let pkg = &record.package_snapshot;
294 Some(RollbackPreviewRow {
295 package: format!("{}/{}", pkg.provider, pkg.name),
296 version: pkg.version.to_string(),
297 net_change: manager
298 .estimate_restore_impact(name)
299 .map(|impact| impact.net)
300 .unwrap_or(SignedByteEstimate::exact(0)),
301 })
302 })
303 .collect::<Vec<_>>();
304 let missing_names = missing_names(names, &rows);
305 let impact = names
306 .iter()
307 .filter_map(|name| manager.estimate_restore_impact(name))
308 .fold(DiskImpact::empty(), |total, impact| total + impact);
309
310 RollbackPreview {
311 rows,
312 impact,
313 missing_names,
314 }
315}
316
317fn prune_preview(names: &[String], manager: &RollbackManager<'_>) -> RollbackPreview {
318 let rows = names
319 .iter()
320 .filter_map(|name| {
321 let record = manager.rollback_record(name)?;
322 let pkg = &record.package_snapshot;
323 Some(RollbackPreviewRow {
324 package: format!("{}/{}", pkg.provider, pkg.name),
325 version: pkg.version.to_string(),
326 net_change: manager
327 .estimate_prune_impact(name)
328 .map(|impact| impact.net)
329 .unwrap_or(SignedByteEstimate::exact(0)),
330 })
331 })
332 .collect::<Vec<_>>();
333 let missing_names = missing_names(names, &rows);
334 let impact = names
335 .iter()
336 .filter_map(|name| manager.estimate_prune_impact(name))
337 .fold(DiskImpact::empty(), |total, impact| total + impact);
338
339 RollbackPreview {
340 rows,
341 impact,
342 missing_names,
343 }
344}
345
346fn missing_names(names: &[String], rows: &[RollbackPreviewRow]) -> Vec<String> {
347 names
348 .iter()
349 .filter(|name| {
350 !rows
351 .iter()
352 .any(|row| row.package.ends_with(&format!("/{name}")))
353 })
354 .cloned()
355 .collect()
356}
357
358impl From<&RollbackPreviewRow> for output::TransactionRow {
359 fn from(row: &RollbackPreviewRow) -> Self {
360 output::TransactionRow::single_version(
361 row.package.clone(),
362 row.version.clone(),
363 row.net_change,
364 ByteEstimate::exact(0),
365 )
366 }
367}