obsidian-git-backup/src/filesync.rs

97 lines
3.4 KiB
Rust

use std::fs;
use std::path::{Path, PathBuf};
pub fn sync_directory(
source: &Path,
target: &Path,
protect_prefix: Option<&str>, // Will not get touched in target (file or directory at top level)
) -> Result<(), Box<dyn std::error::Error>> {
// Ensure the target directory exists
fs::create_dir_all(target)?;
// Gather all paths in the source and target directories
let filter = |entry: Result<fs::DirEntry, std::io::Error>| {
entry
.ok()
.and_then(|entry| match (entry.file_name().to_str(), protect_prefix) {
(Some(name), Some(prefix)) if name.starts_with(prefix) => None,
_ => Some(entry.path()),
})
};
let mut source_entries: Vec<PathBuf> = fs::read_dir(source)?.filter_map(filter).collect();
let mut target_entries: Vec<PathBuf> = fs::read_dir(target)?.filter_map(filter).collect();
// Sort entries to ensure consistent order for comparison
source_entries.sort_unstable();
target_entries.sort_unstable();
// Two-pointer technique: iterate through both directories simultaneously
let mut i = 0;
let mut j = 0;
while i < source_entries.len() || j < target_entries.len() {
if i == source_entries.len() {
// If there are remaining entries in the target, remove them
remove_entry(&target_entries[j])?;
j += 1;
} else if j == target_entries.len() {
// If there are remaining entries in the source, copy them to the target
sync_entry(&source_entries[i], target)?;
i += 1;
} else {
let source_path = &source_entries[i];
let target_path = &target_entries[j];
let source_name = source_path.file_name().unwrap();
let target_name = target_path.file_name().unwrap();
if source_name == target_name {
// If both entries are the same, sync them
sync_entry(source_path, target)?;
i += 1;
j += 1;
} else if source_name < target_name {
// If the source entry is less than the target entry, copy it to the target
sync_entry(source_path, target)?;
i += 1;
} else {
// If the target entry is less than the source entry, remove it from the target
remove_entry(target_path)?;
j += 1;
}
}
}
Ok(())
}
fn sync_entry(source: &Path, target: &Path) -> Result<(), Box<dyn std::error::Error>> {
let target_path = target.join(source.file_name().unwrap());
if source.is_file() {
// Copy the file if it doesn't exist or differs
if !target_path.exists()
|| fs::metadata(source)?.modified()? != fs::metadata(&target_path)?.modified()?
{
println!("File copied/replaced: {:?}", source);
fs::create_dir_all(target_path.parent().unwrap())?;
fs::copy(source, &target_path)?;
}
} else if source.is_dir() {
// Recursively sync directories
sync_directory(source, &target_path, None)?;
}
Ok(())
}
fn remove_entry(path: &Path) -> Result<(), Box<dyn std::error::Error>> {
println!("File/dir removed: {:?}", path);
if path.is_file() {
fs::remove_file(path)?;
} else if path.is_dir() {
fs::remove_dir_all(path)?;
}
Ok(())
}