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