use crate::subvolume_error::MigrateError;
use crate::subvolume_index_files::{IndexLock, refresh_all_indexes};
use crate::subvolume_lock::{release_all, token_of};
use crate::subvolume_preflight::{Refusal, remove_old_refusals, rollback_refusals};
use crate::subvolume_remove::remove_tree;
use crate::subvolume_run::Run;
use crate::subvolume_sync::SyncStats;
use std::path::{Path, PathBuf};
fn remove_if_present(path: &Path) -> Result<(), MigrateError> {
return match std::fs::remove_file(path) {
Ok(()) => Ok(()),
Err(error) if error.kind() == std::io::ErrorKind::NotFound => Ok(()),
Err(error) => Err(MigrateError::io("remove", path, &error)),
};
}
fn nested_subvolumes(run: &Run<'_>, top: &Path) -> Vec<PathBuf> {
let mut found: Vec<PathBuf> = Vec::new();
let mut stack: Vec<PathBuf> = vec![top.to_path_buf()];
while let Some(directory) = stack.pop() {
let Ok(listing) = std::fs::read_dir(&directory) else { continue };
for item in listing.flatten() {
let Ok(file_type) = item.file_type() else { continue };
if !file_type.is_dir() {
continue;
}
if run.platform.is_subvolume_root(item.path().as_path()).unwrap_or(false) {
found.push(item.path());
}
stack.push(item.path());
}
}
found.sort_by_key(|path: &PathBuf| { return std::cmp::Reverse(path.components().count()); });
return found;
}
fn remove_one_subvolume(run: &mut Run<'_>, path: &Path) -> Result<(), MigrateError> {
if run.platform.removal_allowed(path) && run.platform.destroy_subvolume(path).is_ok() {
return Ok(());
}
remove_tree(path).map_err(|error: std::io::Error| { return MigrateError::io("empty and remove the subvolume", path, &error); })?;
return Ok(());
}
pub fn remove_subvolume_tree(run: &mut Run<'_>, path: &Path) -> Result<(), MigrateError> {
for nested in nested_subvolumes(run, path) {
remove_one_subvolume(run, nested.as_path())?;
}
return remove_one_subvolume(run, path);
}
fn swap_back(run: &mut Run<'_>, discard_unreadable: bool) -> Result<Option<Vec<Refusal>>, MigrateError> {
let root: PathBuf = run.layout.root.clone();
let new_path: PathBuf = run.layout.new_path.clone();
let old_path: PathBuf = run.layout.old_path.clone();
let swapped: bool = run.journal.has_begun("rollback-swap") && !run.platform.is_subvolume_root(root.as_path()).unwrap_or(false);
if !swapped {
if !old_path.exists() {
return Ok(Some(vec![Refusal {
cause: format!("the migration is recorded as exchanged, but the old tree {} is missing, so there is nothing to restore", old_path.display()),
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."),
}]));
}
let carried: SyncStats = run.pass_unrecorded("carry back (files newer in the new root)", root.as_path(), old_path.as_path(), false, true)?;
if !carried.unreadable.is_empty() && !discard_unreadable {
let shown: Vec<String> = carried.unreadable.iter().take(10).map(|path: &PathBuf| { return path.display().to_string(); }).collect();
return Ok(Some(vec![Refusal {
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(", ")),
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."),
}]));
}
run.checkpoint("rollback-before-exchange")?;
run.journal.mark("rollback-swap", "started")?;
run.platform.exchange(root.as_path(), old_path.as_path())?;
run.checkpoint("rollback-after-swap")?;
}
if old_path.exists() && !new_path.exists() {
std::fs::rename(&old_path, &new_path).map_err(|error: std::io::Error| { return MigrateError::io("rename", old_path.as_path(), &error); })?;
}
run.checkpoint("rollback-after-rename")?;
run.journal.mark("exchange", "rolled-back")?;
run.checkpoint("rollback-after-exchange")?;
return Ok(None);
}
pub fn rollback(run: &mut Run<'_>, discard_unreadable: bool) -> Result<Option<Vec<Refusal>>, MigrateError> {
let refusals: Vec<Refusal> = rollback_refusals(run.layout, &run.journal);
if !refusals.is_empty() {
return Ok(Some(refusals));
}
let root: PathBuf = run.layout.root.clone();
let new_path: PathBuf = run.layout.new_path.clone();
let old_path: PathBuf = run.layout.old_path.clone();
if run.journal.has_begun("exchange") && !run.journal.is_done("exchange") && run.platform.is_subvolume_root(root.as_path()).unwrap_or(false) {
if new_path.exists() && !old_path.exists() {
std::fs::rename(&new_path, &old_path).map_err(|error: std::io::Error| { return MigrateError::io("rename", new_path.as_path(), &error); })?;
}
run.journal.mark("exchange", "done")?;
}
if run.journal.is_done("exchange")
&& let Some(blocked) = swap_back(run, discard_unreadable)?
{
return Ok(Some(blocked));
}
let token: String = token_of(&run.journal);
release_all(root.as_path(), token.as_str())?;
if run.journal.state("exchange") == Some("rolled-back") && !run.journal.is_done("rollback-indexes") {
let mut lines: Vec<String> = Vec::new();
refresh_all_indexes(root.as_path(), IndexLock::Acquire, std::slice::from_ref(&new_path), &mut |text: &str| { lines.push(text.to_string()); });
for text in &lines {
run.line(text.as_str());
}
run.journal.mark("rollback-indexes", "done")?;
}
if new_path.exists() {
run.line(format!("deleting the new subvolume {} (nested subvolumes first)", new_path.display()).as_str());
remove_subvolume_tree(run, new_path.as_path())?;
}
run.journal.delete()?;
remove_if_present(run.layout.unreadable_path.as_path())?;
run.line(format!("rolled back: {} is the original directory again, and the migration journal is deleted.", root.display()).as_str());
return Ok(None);
}
fn carry_late_writes(run: &mut Run<'_>) -> Result<(), MigrateError> {
let (root, old_path): (PathBuf, PathBuf) = (run.layout.root.clone(), run.layout.old_path.clone());
let since: i64 = run.journal.seconds_of("catchup").unwrap_or(i64::MAX);
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)?;
if stats.files_copied > 0 || !stats.conflicts.is_empty() {
run.line(format!("{} file(s) written to the old tree after the catch-up were carried into {}", stats.files_copied, root.display()).as_str());
}
return Ok(());
}
pub fn remove_old_tree(run: &mut Run<'_>, discard_unreadable: bool) -> Result<Option<Vec<Refusal>>, MigrateError> {
let mut refusals: Vec<Refusal> = remove_old_refusals(run.layout, &run.journal);
let old_path: PathBuf = run.layout.old_path.clone();
if refusals.is_empty() {
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); })?;
if metadata.file_type().is_symlink() {
return Ok(Some(vec![Refusal {
cause: format!("{} is a symbolic link, and the command deletes only a directory it moved itself", old_path.display()),
remedy: String::from("Inspect it and delete it by hand."),
}]));
}
carry_late_writes(run)?;
}
if refusals.is_empty() && !run.unreadable.is_empty() && !discard_unreadable {
refusals.push(Refusal {
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()),
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."),
});
}
if !refusals.is_empty() {
return Ok(Some(refusals));
}
run.line(format!("deleting {} (every file is unlinked; this can take minutes)", old_path.display()).as_str());
remove_tree(&old_path).map_err(|error: std::io::Error| { return MigrateError::io("delete", old_path.as_path(), &error); })?;
run.journal.delete()?;
remove_if_present(run.layout.unreadable_path.as_path())?;
run.line("the old tree is deleted and the migration journal is removed.");
return Ok(None);
}
#[cfg(test)]
#[path = "subvolume_rollback_tests.rs"]
mod tests;