use crate::subvolume_anchor::Anchor;
use crate::subvolume_error::{Interrupt, MigrateError};
use crate::subvolume_platform::Platform;
use crate::subvolume_sync_attrs::preserve_on_descriptor;
use std::collections::{HashMap, HashSet};
use std::ffi::{OsStr, OsString};
use std::fs::Metadata;
use std::os::unix::fs::{DirBuilderExt, FileTypeExt, MetadataExt};
use std::path::{Path, PathBuf};
pub use crate::subvolume_sync_pass_state_and_checks::{SyncOptions, SyncStats};
use crate::subvolume_sync_pass_state_and_checks::*;
impl Pass<'_> {
fn open_destination(&mut self, relative: &Path) -> Result<Option<Anchor>, MigrateError> {
return match Anchor::open_below(self.destination, relative) {
Ok(anchor) => Ok(Some(anchor)),
Err(error) if is_replaced(&error) && self.options.update_only && !relative.as_os_str().is_empty() => {
self.stats.conflicts.push(relative.to_path_buf());
Ok(None)
}
Err(error) => Err(MigrateError::io("open the directory", self.shown(relative).as_path(), &error)),
};
}
fn visit(&mut self, relative: &Path, stack: &mut Vec<Work>) -> Result<(), MigrateError> {
let source_directory: PathBuf = self.source.join(relative);
let listing: std::fs::ReadDir = match vanished_is_counted(std::fs::read_dir(&source_directory), &mut self.stats.vanished) {
Ok(Some(found)) => found,
Ok(None) => return Ok(()),
Err(error) => return Err(MigrateError::UnreadableDirectory { path: source_directory, reason: error.to_string() }),
};
let Some(anchor) = self.open_destination(relative)? else { return Ok(()) };
make_writable(&anchor, self.shown(relative).as_path())?;
let mut names: HashSet<OsString> = HashSet::new();
let mut children: Vec<(OsString, Metadata)> = Vec::new();
for item in listing {
let entry: std::fs::DirEntry = match item {
Ok(found) => found,
Err(error) => return Err(MigrateError::UnreadableDirectory { path: source_directory.clone(), reason: error.to_string() }),
};
names.insert(entry.file_name());
if self.options.excluded.contains(&relative.join(entry.file_name())) {
continue;
}
match vanished_is_counted(entry.metadata(), &mut self.stats.vanished) {
Ok(Some(metadata)) => children.push((entry.file_name(), metadata)),
Ok(None) => {}
Err(error) => return Err(MigrateError::UnreadableDirectory { path: source_directory, reason: error.to_string() }),
}
}
let mut temporary: OsString = OsString::from(PARTIAL_NAME);
while names.contains(&temporary) {
temporary.push("~");
}
let directory: Directory = Directory { anchor, temporary };
remove_if_present(directory.partial().as_path(), self.shown(relative).join(&directory.temporary).as_path())?;
if self.options.delete && !self.options.update_only {
self.delete_extras(&directory, relative, &names)?;
}
for (name, metadata) in children {
self.stats.entries += 1;
self.handle_child(&directory, relative.join(&name), metadata, stack)?;
}
return Ok(());
}
fn delete_extras(&mut self, directory: &Directory, relative: &Path, names: &HashSet<OsString>) -> Result<(), MigrateError> {
let Ok(listing) = std::fs::read_dir(directory.anchor.path()) else { return Ok(()) };
for item in listing.flatten() {
if names.contains(&item.file_name()) {
continue;
}
if self.options.excluded.contains(&relative.join(item.file_name())) {
continue;
}
remove_entry(directory.child(item.file_name().as_os_str()).as_path(), self.shown(relative).join(item.file_name()).as_path())?;
self.stats.deleted += 1;
}
return Ok(());
}
fn handle_child(&mut self, directory: &Directory, relative: PathBuf, metadata: Metadata, stack: &mut Vec<Work>) -> Result<(), MigrateError> {
let name: OsString = relative.file_name().map(OsStr::to_os_string).unwrap_or_default();
let destination: PathBuf = directory.child(name.as_os_str());
let existing: Option<Metadata> = std::fs::symlink_metadata(&destination).ok();
let older_than_since: bool = match self.options.created_since {
Some(since) => metadata.mtime() < since,
None => false,
};
if existing.is_none() && self.options.update_only && older_than_since {
self.stats.left_out += 1;
return Ok(());
}
let file_type: std::fs::FileType = metadata.file_type();
if file_type.is_dir() {
if let Some(found) = &existing && !found.is_dir() {
if !self.replace_allowed(relative.as_path(), found, &metadata) {
return Ok(());
}
remove_entry(destination.as_path(), self.shown(relative.as_path()).as_path())?;
}
let is_directory_now: bool = match existing.as_ref() {
Some(found) => found.is_dir(),
None => false,
};
if !is_directory_now {
MigrateError::from_io_result(std::fs::DirBuilder::new().mode(0o700).create(&destination), "create the directory", self.shown(relative.as_path()).as_path())?;
}
self.stats.directories += 1;
stack.push(Work::Finish(relative.clone(), metadata));
stack.push(Work::Visit(relative));
return Ok(());
}
if file_type.is_symlink() {
return self.copy_symlink(directory, &relative, &metadata, existing);
}
if file_type.is_fifo() {
return self.copy_fifo(directory, &relative, &metadata, existing);
}
if file_type.is_file() {
return self.copy_file(directory, &relative, &metadata, existing);
}
self.stats.special_skipped.push(relative);
return Ok(());
}
}
pub fn sync_tree(
platform: &dyn Platform,
source: &Path,
destination: &Path,
options: SyncOptions<'_>,
interrupt: &Interrupt,
) -> Result<SyncStats, MigrateError> {
let mut pass: Pass<'_> = Pass { platform, source, destination, options, interrupt, stats: SyncStats::default(), links: HashMap::new() };
let root_metadata: Metadata = MigrateError::from_io_result(std::fs::symlink_metadata(source), "examine", source)?;
let mut stack: Vec<Work> = vec![Work::Finish(PathBuf::new(), root_metadata), Work::Visit(PathBuf::new())];
while let Some(work) = stack.pop() {
match work {
Work::Visit(relative) => pass.visit(relative.as_path(), &mut stack)?,
Work::Finish(relative, metadata) => {
let Some(anchor) = pass.open_destination(relative.as_path())? else { continue };
let shown: PathBuf = pass.shown(relative.as_path());
attribute_result(preserve_on_descriptor(source.join(&relative).as_path(), anchor.file(), &metadata, !options.update_only, &mut pass.stats.losses)
, shown.as_path())?;
}
}
}
pass.stats.unreadable.sort();
pass.stats.conflicts.sort();
pass.stats.conflicts.dedup();
return Ok(pass.stats);
}
#[cfg(test)]
#[path = "subvolume_sync_tests.rs"]
mod tests;