git_policy_cli/subvolume/
subvolume_rollback.rs1use crate::subvolume_error::MigrateError;
18use crate::subvolume_index_files::{IndexLock, refresh_all_indexes};
19use crate::subvolume_lock::{release_all, token_of};
20use crate::subvolume_preflight::{Refusal, remove_old_refusals, rollback_refusals};
21use crate::subvolume_remove::remove_tree;
22use crate::subvolume_run::Run;
23use crate::subvolume_sync::SyncStats;
24use std::path::{Path, PathBuf};
25
26fn remove_if_present(path: &Path) -> Result<(), MigrateError> {
28 return match std::fs::remove_file(path) {
29 Ok(()) => Ok(()),
30 Err(error) if error.kind() == std::io::ErrorKind::NotFound => Ok(()),
31 Err(error) => Err(MigrateError::io("remove", path, &error)),
32 };
33}
34
35fn nested_subvolumes(run: &Run<'_>, top: &Path) -> Vec<PathBuf> {
45 let mut found: Vec<PathBuf> = Vec::new();
46 let mut stack: Vec<PathBuf> = vec![top.to_path_buf()];
47 while let Some(directory) = stack.pop() {
48 let Ok(listing) = std::fs::read_dir(&directory) else { continue };
49 for item in listing.flatten() {
50 let Ok(file_type) = item.file_type() else { continue };
51 if !file_type.is_dir() {
52 continue;
53 }
54 if run.platform.is_subvolume_root(item.path().as_path()).unwrap_or(false) {
55 found.push(item.path());
56 }
57 stack.push(item.path());
58 }
59 }
60 found.sort_by_key(|path: &PathBuf| { return std::cmp::Reverse(path.components().count()); });
61 return found;
62}
63
64fn remove_one_subvolume(run: &mut Run<'_>, path: &Path) -> Result<(), MigrateError> {
76 if run.platform.removal_allowed(path) && run.platform.destroy_subvolume(path).is_ok() {
77 return Ok(());
78 }
79 remove_tree(path).map_err(|error: std::io::Error| { return MigrateError::io("empty and remove the subvolume", path, &error); })?;
80 return Ok(());
81}
82
83pub fn remove_subvolume_tree(run: &mut Run<'_>, path: &Path) -> Result<(), MigrateError> {
93 for nested in nested_subvolumes(run, path) {
94 remove_one_subvolume(run, nested.as_path())?;
95 }
96 return remove_one_subvolume(run, path);
97}
98
99fn swap_back(run: &mut Run<'_>, discard_unreadable: bool) -> Result<Option<Vec<Refusal>>, MigrateError> {
114 let root: PathBuf = run.layout.root.clone();
115 let new_path: PathBuf = run.layout.new_path.clone();
116 let old_path: PathBuf = run.layout.old_path.clone();
117 let swapped: bool = run.journal.has_begun("rollback-swap") && !run.platform.is_subvolume_root(root.as_path()).unwrap_or(false);
118 if !swapped {
119 if !old_path.exists() {
120 return Ok(Some(vec![Refusal {
121 cause: format!("the migration is recorded as exchanged, but the old tree {} is missing, so there is nothing to restore", old_path.display()),
122 remedy: String::from("If the old tree was moved, move it back to that path and run the command again; if it was deleted, the checkout cannot be rolled back and the new subvolume is the only copy."),
123 }]));
124 }
125 let carried: SyncStats = run.pass_unrecorded("carry back (files newer in the new root)", root.as_path(), old_path.as_path(), false, true)?;
126 if !carried.unreadable.is_empty() && !discard_unreadable {
127 let shown: Vec<String> = carried.unreadable.iter().take(10).map(|path: &PathBuf| { return path.display().to_string(); }).collect();
128 return Ok(Some(vec![Refusal {
129 cause: format!("{} file(s) that this user may not read exist only in the new subvolume {} and cannot be carried back: {}", carried.unreadable.len(), root.display(), shown.join(", ")),
130 remedy: String::from("Copy them out with the privilege they need (for example `sudo cp --archive`), or pass --discard-unreadable together with --rollback to delete them with the new subvolume."),
131 }]));
132 }
133 run.checkpoint("rollback-before-exchange")?;
134 run.journal.mark("rollback-swap", "started")?;
135 run.platform.exchange(root.as_path(), old_path.as_path())?;
136 run.checkpoint("rollback-after-swap")?;
137 }
138 if old_path.exists() && !new_path.exists() {
139 std::fs::rename(&old_path, &new_path).map_err(|error: std::io::Error| { return MigrateError::io("rename", old_path.as_path(), &error); })?;
140 }
141 run.checkpoint("rollback-after-rename")?;
142 run.journal.mark("exchange", "rolled-back")?;
143 run.checkpoint("rollback-after-exchange")?;
144 return Ok(None);
145}
146
147pub fn rollback(run: &mut Run<'_>, discard_unreadable: bool) -> Result<Option<Vec<Refusal>>, MigrateError> {
159 let refusals: Vec<Refusal> = rollback_refusals(run.layout, &run.journal);
160 if !refusals.is_empty() {
161 return Ok(Some(refusals));
162 }
163 let root: PathBuf = run.layout.root.clone();
164 let new_path: PathBuf = run.layout.new_path.clone();
165 let old_path: PathBuf = run.layout.old_path.clone();
166 if run.journal.has_begun("exchange") && !run.journal.is_done("exchange") && run.platform.is_subvolume_root(root.as_path()).unwrap_or(false) {
167 if new_path.exists() && !old_path.exists() {
168 std::fs::rename(&new_path, &old_path).map_err(|error: std::io::Error| { return MigrateError::io("rename", new_path.as_path(), &error); })?;
169 }
170 run.journal.mark("exchange", "done")?;
171 }
172 if run.journal.is_done("exchange")
173 && let Some(blocked) = swap_back(run, discard_unreadable)?
174 {
175 return Ok(Some(blocked));
176 }
177 let token: String = token_of(&run.journal);
181 release_all(root.as_path(), token.as_str())?;
182 if run.journal.state("exchange") == Some("rolled-back") && !run.journal.is_done("rollback-indexes") {
184 let mut lines: Vec<String> = Vec::new();
185 refresh_all_indexes(root.as_path(), IndexLock::Acquire, std::slice::from_ref(&new_path), &mut |text: &str| { lines.push(text.to_string()); });
186 for text in &lines {
187 run.line(text.as_str());
188 }
189 run.journal.mark("rollback-indexes", "done")?;
190 }
191 if new_path.exists() {
192 run.line(format!("deleting the new subvolume {} (nested subvolumes first)", new_path.display()).as_str());
193 remove_subvolume_tree(run, new_path.as_path())?;
194 }
195 run.journal.delete()?;
196 remove_if_present(run.layout.unreadable_path.as_path())?;
197 run.line(format!("rolled back: {} is the original directory again, and the migration journal is deleted.", root.display()).as_str());
198 return Ok(None);
199}
200
201fn carry_late_writes(run: &mut Run<'_>) -> Result<(), MigrateError> {
214 let (root, old_path): (PathBuf, PathBuf) = (run.layout.root.clone(), run.layout.old_path.clone());
215 let since: i64 = run.journal.seconds_of("catchup").unwrap_or(i64::MAX);
216 let stats: SyncStats = run.pass_late("late writes (files written to the old tree after the catch-up)", old_path.as_path(), root.as_path(), since)?;
217 if stats.files_copied > 0 || !stats.conflicts.is_empty() {
218 run.line(format!("{} file(s) written to the old tree after the catch-up were carried into {}", stats.files_copied, root.display()).as_str());
219 }
220 return Ok(());
221}
222
223pub fn remove_old_tree(run: &mut Run<'_>, discard_unreadable: bool) -> Result<Option<Vec<Refusal>>, MigrateError> {
235 let mut refusals: Vec<Refusal> = remove_old_refusals(run.layout, &run.journal);
236 let old_path: PathBuf = run.layout.old_path.clone();
237 if refusals.is_empty() {
238 let metadata: std::fs::Metadata = std::fs::symlink_metadata(&old_path).map_err(|error: std::io::Error| { return MigrateError::io("examine", old_path.as_path(), &error); })?;
239 if metadata.file_type().is_symlink() {
240 return Ok(Some(vec![Refusal {
241 cause: format!("{} is a symbolic link, and the command deletes only a directory it moved itself", old_path.display()),
242 remedy: String::from("Inspect it and delete it by hand."),
243 }]));
244 }
245 carry_late_writes(run)?;
246 }
247 if refusals.is_empty() && !run.unreadable.is_empty() && !discard_unreadable {
248 refusals.push(Refusal {
249 cause: format!("{} file(s) that this user may not read exist only in {} (listed in {})", run.unreadable.len(), run.layout.old_path.display(), run.layout.unreadable_path.display()),
250 remedy: String::from("Copy them out with the privilege they need (for example `sudo cp --archive`), or pass --discard-unreadable together with --remove-old to delete them with the old tree."),
251 });
252 }
253 if !refusals.is_empty() {
254 return Ok(Some(refusals));
255 }
256 run.line(format!("deleting {} (every file is unlinked; this can take minutes)", old_path.display()).as_str());
257 remove_tree(&old_path).map_err(|error: std::io::Error| { return MigrateError::io("delete", old_path.as_path(), &error); })?;
258 run.journal.delete()?;
259 remove_if_present(run.layout.unreadable_path.as_path())?;
260 run.line("the old tree is deleted and the migration journal is removed.");
261 return Ok(None);
262}
263
264#[cfg(test)]
266#[path = "subvolume_rollback_tests.rs"]
267mod tests;