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, package_failed, package_skipped, package_success,
16 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 enum RollbackPackageStatus {
59 Succeeded,
60 Failed { error: String },
61 Skipped { reason: String },
62}
63
64pub struct RollbackPackageOutcome {
65 pub name: String,
66 pub status: RollbackPackageStatus,
67}
68
69pub struct RollbackRestoreOutcome {
70 pub restored: u32,
71 pub failed: u32,
72 pub packages: Vec<RollbackPackageOutcome>,
73}
74
75pub struct RollbackPruneOutcome {
76 pub pruned: u32,
77 pub missing: u32,
78 pub packages: Vec<RollbackPackageOutcome>,
79}
80
81impl RollbackOperation {
82 pub fn new() -> Result<Self> {
83 let paths = UpstreamPaths::new()?;
84 let package_storage = PackageStorage::new(&paths.config.packages_file)?;
85 let metadata_storage = MetadataStorage::new(&paths.config.metadata_file)?;
86 let rollback_file = RollbackManager::rollback_file_path(&paths);
87 let rollback_storage = RollbackStorage::new(&rollback_file)?;
88
89 Ok(Self {
90 paths,
91 package_storage,
92 metadata_storage,
93 rollback_storage,
94 })
95 }
96
97 fn manager(&mut self) -> RollbackManager<'_> {
98 RollbackManager::new(
99 &self.paths,
100 &mut self.package_storage,
101 &mut self.metadata_storage,
102 &mut self.rollback_storage,
103 )
104 }
105
106 pub fn restore_preview(&mut self, names: &[String]) -> Result<RollbackRestorePreview> {
107 if names.is_empty() {
108 return Err(anyhow!(
109 "At least one package name is required unless --prune is provided"
110 ));
111 }
112
113 let manager = self.manager();
114 let preview = restore_preview(names, &manager);
115 let targets = names
116 .iter()
117 .filter_map(|name| {
118 let record = manager.rollback_record(name)?;
119 Some(RollbackRestoreTarget {
120 name: name.clone(),
121 install_path: record
122 .package_snapshot
123 .install_path
124 .as_ref()
125 .map(|path| path.display().to_string())
126 .unwrap_or_else(|| "<missing>".to_string()),
127 source: record.source.clone(),
128 })
129 })
130 .collect();
131
132 Ok(RollbackRestorePreview { preview, targets })
133 }
134
135 pub fn restorable_names(&mut self, names: &[String]) -> Vec<String> {
136 let manager = self.manager();
137 names
138 .iter()
139 .filter(|name| manager.rollback_record(name).is_some())
140 .cloned()
141 .collect()
142 }
143
144 pub fn restore<H>(
145 &mut self,
146 names: &[String],
147 message_callback: &mut Option<H>,
148 ) -> Result<RollbackRestoreOutcome>
149 where
150 H: FnMut(&str, &str),
151 {
152 let restorable_names = self.restorable_names(names);
153 let transaction = TransactionLog::start(
154 &self.paths,
155 TransactionKind::Rollback,
156 planned_packages(restorable_names.clone()),
157 None,
158 )?;
159
160 let mut restored = 0_u32;
161 let mut failed = 0_u32;
162 let mut packages = Vec::new();
163 let mut transaction_packages = Vec::new();
164 {
165 let mut manager = self.manager();
166 for name in &restorable_names {
167 let package_name = name.clone();
168 let mut msg = Some(|line: &str| {
169 if let Some(callback) = message_callback.as_mut() {
170 callback(&package_name, line);
171 }
172 });
173
174 match manager.restore_package(name, &mut msg) {
175 Ok(_) => {
176 packages.push(RollbackPackageOutcome {
177 name: name.clone(),
178 status: RollbackPackageStatus::Succeeded,
179 });
180 transaction_packages.push(package_success(name.clone()));
181 restored += 1;
182 }
183 Err(err) => {
184 let summary = output::error_summary(&err);
185 packages.push(RollbackPackageOutcome {
186 name: name.clone(),
187 status: RollbackPackageStatus::Failed {
188 error: summary.clone(),
189 },
190 });
191 transaction_packages.push(package_failed(name.clone(), summary));
192 failed += 1;
193 }
194 }
195 }
196 }
197
198 if failed > 0 {
199 transaction.fail(
200 transaction_packages,
201 format!("{failed} rollback restore(s) failed"),
202 )?;
203 } else {
204 transaction.complete(transaction_packages)?;
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 transaction = if target_names.is_empty() {
238 None
239 } else {
240 Some(TransactionLog::start(
241 &self.paths,
242 TransactionKind::Rollback,
243 planned_packages(target_names.to_vec()),
244 None,
245 )?)
246 };
247
248 let mut pruned = 0_u32;
249 let mut missing = 0_u32;
250 let mut packages = Vec::new();
251 let mut transaction_packages = Vec::new();
252 let total = target_names.len();
253 {
254 let mut manager = self.manager();
255 for (idx, name) in target_names.iter().enumerate() {
256 if let Some(callback) = message_callback.as_mut() {
257 callback(name, idx + 1, total);
258 }
259
260 match manager.prune_package(name) {
261 Ok(true) => {
262 pruned += 1;
263 packages.push(RollbackPackageOutcome {
264 name: name.clone(),
265 status: RollbackPackageStatus::Succeeded,
266 });
267 transaction_packages.push(package_success(name.clone()));
268 }
269 Ok(false) => {
270 missing += 1;
271 let reason = "no rollback data found".to_string();
272 packages.push(RollbackPackageOutcome {
273 name: name.clone(),
274 status: RollbackPackageStatus::Skipped {
275 reason: reason.clone(),
276 },
277 });
278 transaction_packages.push(package_skipped(name.clone(), reason));
279 }
280 Err(err) => {
281 let summary = output::error_summary(&err);
282 packages.push(RollbackPackageOutcome {
283 name: name.clone(),
284 status: RollbackPackageStatus::Failed {
285 error: summary.clone(),
286 },
287 });
288 transaction_packages.push(package_failed(name.clone(), summary.clone()));
289 if let Some(transaction) = transaction.take() {
290 transaction.fail(transaction_packages, summary)?;
291 }
292 return Err(err);
293 }
294 }
295 }
296 }
297
298 if let Some(transaction) = transaction {
299 transaction.complete(transaction_packages)?;
300 }
301
302 Ok(RollbackPruneOutcome {
303 pruned,
304 missing,
305 packages,
306 })
307 }
308}
309
310fn restore_preview(names: &[String], manager: &RollbackManager<'_>) -> RollbackPreview {
311 let rows = names
312 .iter()
313 .filter_map(|name| {
314 let record = manager.rollback_record(name)?;
315 let pkg = &record.package_snapshot;
316 Some(RollbackPreviewRow {
317 package: format!("{}/{}", pkg.provider, pkg.name),
318 version: pkg.version.to_string(),
319 net_change: manager
320 .estimate_restore_impact(name)
321 .map(|impact| impact.net)
322 .unwrap_or(SignedByteEstimate::exact(0)),
323 })
324 })
325 .collect::<Vec<_>>();
326 let missing_names = missing_names(names, &rows);
327 let impact = names
328 .iter()
329 .filter_map(|name| manager.estimate_restore_impact(name))
330 .fold(DiskImpact::empty(), |total, impact| total + impact);
331
332 RollbackPreview {
333 rows,
334 impact,
335 missing_names,
336 }
337}
338
339fn prune_preview(names: &[String], manager: &RollbackManager<'_>) -> RollbackPreview {
340 let rows = names
341 .iter()
342 .filter_map(|name| {
343 let record = manager.rollback_record(name)?;
344 let pkg = &record.package_snapshot;
345 Some(RollbackPreviewRow {
346 package: format!("{}/{}", pkg.provider, pkg.name),
347 version: pkg.version.to_string(),
348 net_change: manager
349 .estimate_prune_impact(name)
350 .map(|impact| impact.net)
351 .unwrap_or(SignedByteEstimate::exact(0)),
352 })
353 })
354 .collect::<Vec<_>>();
355 let missing_names = missing_names(names, &rows);
356 let impact = names
357 .iter()
358 .filter_map(|name| manager.estimate_prune_impact(name))
359 .fold(DiskImpact::empty(), |total, impact| total + impact);
360
361 RollbackPreview {
362 rows,
363 impact,
364 missing_names,
365 }
366}
367
368fn missing_names(names: &[String], rows: &[RollbackPreviewRow]) -> Vec<String> {
369 names
370 .iter()
371 .filter(|name| {
372 !rows
373 .iter()
374 .any(|row| row.package.ends_with(&format!("/{name}")))
375 })
376 .cloned()
377 .collect()
378}
379
380impl From<&RollbackPreviewRow> for output::TransactionRow {
381 fn from(row: &RollbackPreviewRow) -> Self {
382 output::TransactionRow::single_version(
383 row.package.clone(),
384 row.version.clone(),
385 row.net_change,
386 ByteEstimate::exact(0),
387 )
388 }
389}