diff --git a/src/filetree.rs b/src/filetree.rs index 1fff67ae..c74d6409 100644 --- a/src/filetree.rs +++ b/src/filetree.rs @@ -280,12 +280,63 @@ pub(crate) fn syncfs(d: &openat::Dir) -> Result<()> { rustix::fs::syncfs(d).map_err(Into::into) } +fn copy_dir(src: &Path, dst: &Path) -> Result<()> { + let r = std::process::Command::new("cp") + .args(["-a"]) + .arg(src) + .arg(dst) + .status()?; + if !r.success() { + anyhow::bail!("Failed to copy"); + } + Ok(()) +} + +/// Convert path parent to temp, `fedora/foo` -> `fedora.tmp/foo` #[cfg(any(target_arch = "x86_64", target_arch = "aarch64"))] -fn tmpname_for_path>(path: P) -> std::path::PathBuf { +fn tmp_for_path>(path: P) -> std::path::PathBuf { let path = path.as_ref(); - let mut buf = path.file_name().expect("filename").to_os_string(); - buf.push(TMP_PREFIX); - path.with_file_name(buf) + let mut parent = String::from(""); + if let Some(p) = path.parent() { + parent = format!("{}.tmp", p.display()); + } + let buf = path.file_name().expect("filename").to_os_string(); + Path::new(parent.as_str()).join(buf) +} + +/// Get diff based on sub dir +#[cfg(any(target_arch = "x86_64", target_arch = "aarch64"))] +pub(crate) fn diff_with_prefix(diff: &FileTreeDiff, prefix: &str) -> Result { + let mut new_diff = FileTreeDiff { + additions: HashSet::new(), + removals: HashSet::new(), + changes: HashSet::new(), + }; + for pathstr in diff.additions.iter() { + let path = Path::new(pathstr); + if let Some(parent) = path.parent() { + if parent.to_string_lossy().eq(prefix) { + new_diff.additions.insert(pathstr.to_string()); + } + } + } + for pathstr in diff.changes.iter() { + let path = Path::new(pathstr); + if let Some(parent) = path.parent() { + if parent.to_string_lossy().eq(prefix) { + new_diff.changes.insert(pathstr.to_string()); + } + } + } + for pathstr in diff.removals.iter() { + let path = Path::new(pathstr); + if let Some(parent) = path.parent() { + if parent.to_string_lossy().eq(prefix) { + new_diff.removals.insert(pathstr.to_string()); + } + } + } + Ok(new_diff) } /// Given two directories, apply a diff generated from srcdir to destdir @@ -302,40 +353,82 @@ pub(crate) fn apply_diff( let opts = opts.unwrap_or(&default_opts); cleanup_tmp(destdir).context("cleaning up temporary files")?; - // Write new and changed files - for pathstr in diff.additions.iter().chain(diff.changes.iter()) { + // Get updates based on dir + let mut updates = std::collections::HashSet::new(); + for pathstr in diff + .additions + .iter() + .chain(diff.changes.iter()) + .chain(diff.removals.iter()) + { let path = Path::new(pathstr); if let Some(parent) = path.parent() { - destdir.ensure_dir_all(parent, DEFAULT_FILE_MODE)?; + updates.insert(parent.to_string_lossy().into_owned()); } - let destp = tmpname_for_path(path); - srcdir - .copy_file_at(path, destdir, destp.as_path()) - .with_context(|| format!("writing {}", &pathstr))?; } - // Ensure all of the new files are written persistently to disk - if !opts.skip_sync { - syncfs(destdir)?; - } - // Now move them all into place (TODO track interruption) - for path in diff.additions.iter().chain(diff.changes.iter()) { - let pathtmp = tmpname_for_path(path); + let efipath = destdir.recover_path()?; + for path in &updates { + let new_diff = diff_with_prefix(diff, path)?; + if !opts.skip_removals { + for path in new_diff.removals.iter() { + destdir + .remove_file_optional(path) + .with_context(|| format!("removing {path}"))?; + } + } + // Ensure all of the new files are written persistently to disk + if !opts.skip_sync { + syncfs(destdir)?; + } + // skip if there are no changes or additions + if new_diff.additions.len() == 0 && new_diff.changes.len() == 0 { + continue; + } + let src_path = efipath.join(&path); + let tmp_path = efipath.join(format!("{}.tmp", path)); + if tmp_path.exists() { + std::fs::remove_dir_all(&tmp_path)?; + } + if let Some(parent) = tmp_path.parent() { + destdir.ensure_dir_all(parent, DEFAULT_FILE_MODE)?; + } + // in case there is additional with subdir + destdir.ensure_dir_all(&src_path, DEFAULT_FILE_MODE)?; + // copy original dir to tmp + copy_dir(&src_path, &tmp_path) + .with_context(|| format!("copying {:?} to {:?}", src_path, tmp_path))?; + assert!(destdir.exists(&tmp_path)?); + + // Write new and changed files + for pathstr in new_diff.additions.iter().chain(new_diff.changes.iter()) { + let path = Path::new(pathstr); + let pathtmp = tmp_for_path(path); + if let Some(parent) = pathtmp.parent() { + destdir.ensure_dir_all(parent, DEFAULT_FILE_MODE)?; + } + destdir.remove_file_optional(&pathtmp)?; + srcdir + .copy_file_at(path, destdir, &pathtmp) + .with_context(|| format!("writing {:?} to {:?}", path, pathtmp))?; + } + // do local exchange + log::trace!( + "doing local exchange for {} and {}", + tmp_path.display(), + src_path.display() + ); destdir - .local_rename(&pathtmp, path) - .with_context(|| format!("renaming {path}"))?; - } - if !opts.skip_removals { - for path in diff.removals.iter() { - destdir - .remove_file_optional(path) - .with_context(|| format!("removing {path}"))?; + .local_exchange(&tmp_path, &src_path) + .with_context(|| format!("exchange for {:?} and {:?}", tmp_path, src_path))?; + // finally remove the temp dir + log::trace!("cleanup: {}", tmp_path.display()); + std::fs::remove_dir_all(&tmp_path).context("clean up temp")?; + // A second full filesystem sync to narrow any races rather than + // waiting for writeback to kick in. + if !opts.skip_sync { + syncfs(destdir)?; } } - // A second full filesystem sync to narrow any races rather than - // waiting for writeback to kick in. - if !opts.skip_sync { - syncfs(destdir)?; - } Ok(()) }