use alloc::string::{String, ToString};
use alloc::vec::Vec;
use super::types::{FileType, TimeError};
use super::walker::{HistoricalEntry, HistoricalTreeProvider, HistoricalTreeWalker, WalkOptions};
pub trait RestoreTarget {
fn create_file(&mut self, path: &str, entry: &HistoricalEntry) -> Result<(), TimeError>;
fn create_directory(&mut self, path: &str, entry: &HistoricalEntry) -> Result<(), TimeError>;
fn create_symlink(
&mut self,
path: &str,
target: &str,
entry: &HistoricalEntry,
) -> Result<(), TimeError>;
fn copy_data(
&mut self,
dest_path: &str,
source_entry: &HistoricalEntry,
) -> Result<u64, TimeError>;
fn set_metadata(&mut self, path: &str, entry: &HistoricalEntry) -> Result<(), TimeError>;
fn exists(&self, path: &str) -> bool;
fn remove(&mut self, path: &str) -> Result<(), TimeError>;
}
#[derive(Debug, Clone)]
pub struct RestoreOptions {
pub overwrite: bool,
pub preserve_metadata: bool,
pub recursive: bool,
pub max_depth: i32,
pub dry_run: bool,
pub verify: bool,
}
impl Default for RestoreOptions {
fn default() -> Self {
Self {
overwrite: false,
preserve_metadata: true,
recursive: true,
max_depth: -1,
dry_run: false,
verify: true,
}
}
}
#[derive(Debug, Clone)]
pub struct RestoreResult {
pub path: String,
pub dest_path: String,
pub txg: u64,
pub files_restored: usize,
pub dirs_restored: usize,
pub bytes_restored: u64,
pub skipped: Vec<String>,
pub errors: Vec<(String, String)>,
pub dry_run: bool,
}
impl RestoreResult {
pub fn new(path: &str, dest_path: &str, txg: u64, dry_run: bool) -> Self {
Self {
path: path.into(),
dest_path: dest_path.into(),
txg,
files_restored: 0,
dirs_restored: 0,
bytes_restored: 0,
skipped: Vec::new(),
errors: Vec::new(),
dry_run,
}
}
pub fn is_success(&self) -> bool {
self.errors.is_empty()
}
pub fn total_restored(&self) -> usize {
self.files_restored + self.dirs_restored
}
}
pub struct RestoreEngine<'a, P: HistoricalTreeProvider, T: RestoreTarget> {
provider: &'a P,
target: &'a mut T,
}
impl<'a, P: HistoricalTreeProvider, T: RestoreTarget> RestoreEngine<'a, P, T> {
pub fn new(provider: &'a P, target: &'a mut T) -> Self {
Self { provider, target }
}
pub fn restore(
&mut self,
path: &str,
dest_path: Option<&str>,
txg: u64,
options: &RestoreOptions,
) -> Result<RestoreResult, TimeError> {
let walker = HistoricalTreeWalker::new(self.provider, txg);
if !walker.exists(path) {
return Err(TimeError::PathNotFound(path.into()));
}
let source = walker.lookup(path)?;
let dest = dest_path.unwrap_or(path);
let mut result = RestoreResult::new(path, dest, txg, options.dry_run);
if source.is_dir() {
self.restore_directory(&walker, &source, dest, options, &mut result)?;
} else {
self.restore_file(&walker, &source, dest, options, &mut result)?;
}
Ok(result)
}
fn restore_file(
&mut self,
_walker: &HistoricalTreeWalker<P>,
entry: &HistoricalEntry,
dest_path: &str,
options: &RestoreOptions,
result: &mut RestoreResult,
) -> Result<(), TimeError> {
if self.target.exists(dest_path) {
if !options.overwrite {
result.skipped.push(dest_path.into());
return Ok(());
}
if !options.dry_run {
self.target.remove(dest_path)?;
}
}
if options.dry_run {
if entry.is_dir() {
result.dirs_restored += 1;
} else {
result.files_restored += 1;
result.bytes_restored += entry.size;
}
return Ok(());
}
match entry.file_type {
FileType::Regular => {
self.target.create_file(dest_path, entry)?;
let bytes = self.target.copy_data(dest_path, entry)?;
result.bytes_restored += bytes;
}
FileType::Symlink => {
let target = self.provider.readlink_at_txg(&entry.path, entry.txg)?;
self.target.create_symlink(dest_path, &target, entry)?;
}
FileType::Directory => {
self.target.create_directory(dest_path, entry)?;
result.dirs_restored += 1;
return Ok(()); }
_ => {
result.skipped.push(dest_path.into());
return Ok(());
}
}
if options.preserve_metadata {
if let Err(e) = self.target.set_metadata(dest_path, entry) {
result
.errors
.push((dest_path.into(), alloc::format!("{}", e)));
}
}
result.files_restored += 1;
Ok(())
}
fn restore_directory(
&mut self,
walker: &HistoricalTreeWalker<P>,
entry: &HistoricalEntry,
dest_path: &str,
options: &RestoreOptions,
result: &mut RestoreResult,
) -> Result<(), TimeError> {
if !options.dry_run && !self.target.exists(dest_path) {
self.target.create_directory(dest_path, entry)?;
}
result.dirs_restored += 1;
if !options.recursive {
return Ok(());
}
let walk_options = WalkOptions {
max_depth: options.max_depth,
..Default::default()
};
let entries = walker.walk(&entry.path, &walk_options)?;
for child in entries {
let relative = if child.path.starts_with(&entry.path) {
&child.path[entry.path.len()..]
} else {
&child.path
};
let child_dest = if dest_path.ends_with('/') {
alloc::format!("{}{}", dest_path, relative.trim_start_matches('/'))
} else {
alloc::format!("{}{}", dest_path, relative)
};
match self.restore_file(walker, &child, &child_dest, options, result) {
Ok(()) => {}
Err(e) => {
result.errors.push((child_dest, alloc::format!("{}", e)));
}
}
}
if options.preserve_metadata && !options.dry_run {
if let Err(e) = self.target.set_metadata(dest_path, entry) {
result
.errors
.push((dest_path.into(), alloc::format!("{}", e)));
}
}
Ok(())
}
pub fn preview(
&mut self,
path: &str,
dest_path: Option<&str>,
txg: u64,
) -> Result<RestoreResult, TimeError> {
let options = RestoreOptions {
dry_run: true,
..Default::default()
};
self.restore(path, dest_path, txg, &options)
}
}
#[derive(Debug, Default)]
pub struct InMemoryRestoreTarget {
pub files: Vec<(String, HistoricalEntry)>,
pub directories: Vec<String>,
pub symlinks: Vec<(String, String)>,
pub removed: Vec<String>,
}
impl InMemoryRestoreTarget {
pub fn new() -> Self {
Self::default()
}
pub fn file_count(&self) -> usize {
self.files.len()
}
}
impl RestoreTarget for InMemoryRestoreTarget {
fn create_file(&mut self, path: &str, entry: &HistoricalEntry) -> Result<(), TimeError> {
self.files.push((path.into(), entry.clone()));
Ok(())
}
fn create_directory(&mut self, path: &str, _entry: &HistoricalEntry) -> Result<(), TimeError> {
if !self.directories.contains(&path.to_string()) {
self.directories.push(path.into());
}
Ok(())
}
fn create_symlink(
&mut self,
path: &str,
target: &str,
_entry: &HistoricalEntry,
) -> Result<(), TimeError> {
self.symlinks.push((path.into(), target.into()));
Ok(())
}
fn copy_data(
&mut self,
_dest_path: &str,
source_entry: &HistoricalEntry,
) -> Result<u64, TimeError> {
Ok(source_entry.size)
}
fn set_metadata(&mut self, _path: &str, _entry: &HistoricalEntry) -> Result<(), TimeError> {
Ok(())
}
fn exists(&self, path: &str) -> bool {
self.files.iter().any(|(p, _)| p == path) || self.directories.contains(&path.to_string())
}
fn remove(&mut self, path: &str) -> Result<(), TimeError> {
self.removed.push(path.into());
self.files.retain(|(p, _)| p != path);
self.directories.retain(|p| p != path);
Ok(())
}
}
#[cfg(test)]
mod tests {
use super::super::walker::InMemoryTreeProvider;
use super::*;
fn create_entry(
path: &str,
name: &str,
file_type: FileType,
size: u64,
txg: u64,
) -> HistoricalEntry {
HistoricalEntry {
name: name.into(),
path: path.into(),
object_id: path.len() as u64,
parent_id: 1,
file_type,
size,
mode: 0o644,
uid: 1000,
gid: 1000,
atime: txg * 1000,
mtime: txg * 1000,
ctime: txg * 1000,
txg,
checksum: [txg; 4],
nlinks: 1,
blocks: size.div_ceil(512),
generation: txg,
}
}
fn create_test_provider() -> InMemoryTreeProvider {
let mut provider = InMemoryTreeProvider::new();
provider.add_entry(100, create_entry("/", "", FileType::Directory, 0, 100));
provider.add_entry(
100,
create_entry("/data", "data", FileType::Directory, 0, 100),
);
provider.add_entry(
100,
create_entry("/data/file1.txt", "file1.txt", FileType::Regular, 100, 100),
);
provider.add_entry(
100,
create_entry("/data/file2.txt", "file2.txt", FileType::Regular, 200, 100),
);
provider.add_entry(
100,
create_entry("/data/subdir", "subdir", FileType::Directory, 0, 100),
);
provider.add_entry(
100,
create_entry(
"/data/subdir/nested.txt",
"nested.txt",
FileType::Regular,
50,
100,
),
);
provider
}
#[test]
fn test_restore_single_file() {
let provider = create_test_provider();
let mut target = InMemoryRestoreTarget::new();
{
let mut engine = RestoreEngine::new(&provider, &mut target);
let result = engine
.restore("/data/file1.txt", None, 100, &RestoreOptions::default())
.unwrap();
assert!(result.is_success());
assert_eq!(result.files_restored, 1);
assert_eq!(result.bytes_restored, 100);
}
assert_eq!(target.file_count(), 1);
}
#[test]
fn test_restore_to_different_path() {
let provider = create_test_provider();
let mut target = InMemoryRestoreTarget::new();
{
let mut engine = RestoreEngine::new(&provider, &mut target);
let result = engine
.restore(
"/data/file1.txt",
Some("/backup/file1.txt"),
100,
&RestoreOptions::default(),
)
.unwrap();
assert_eq!(result.dest_path, "/backup/file1.txt");
}
assert!(target.files.iter().any(|(p, _)| p == "/backup/file1.txt"));
}
#[test]
fn test_restore_directory() {
let provider = create_test_provider();
let mut target = InMemoryRestoreTarget::new();
{
let mut engine = RestoreEngine::new(&provider, &mut target);
let result = engine
.restore("/data", None, 100, &RestoreOptions::default())
.unwrap();
assert!(result.is_success());
assert_eq!(result.files_restored, 3); assert!(result.dirs_restored >= 1);
}
}
#[test]
fn test_restore_dry_run() {
let provider = create_test_provider();
let mut target = InMemoryRestoreTarget::new();
{
let mut engine = RestoreEngine::new(&provider, &mut target);
let result = engine.preview("/data", None, 100).unwrap();
assert!(result.dry_run);
assert_eq!(result.files_restored, 3);
}
assert_eq!(target.file_count(), 0);
}
#[test]
fn test_restore_skip_existing() {
let provider = create_test_provider();
let mut target = InMemoryRestoreTarget::new();
target.files.push((
"/data/file1.txt".into(),
create_entry("/data/file1.txt", "file1.txt", FileType::Regular, 50, 50),
));
{
let mut engine = RestoreEngine::new(&provider, &mut target);
let options = RestoreOptions {
overwrite: false,
..Default::default()
};
let result = engine
.restore("/data/file1.txt", None, 100, &options)
.unwrap();
assert_eq!(result.skipped.len(), 1);
assert_eq!(result.files_restored, 0);
}
}
#[test]
fn test_restore_overwrite() {
let provider = create_test_provider();
let mut target = InMemoryRestoreTarget::new();
target.files.push((
"/data/file1.txt".into(),
create_entry("/data/file1.txt", "file1.txt", FileType::Regular, 50, 50),
));
{
let mut engine = RestoreEngine::new(&provider, &mut target);
let options = RestoreOptions {
overwrite: true,
..Default::default()
};
let result = engine
.restore("/data/file1.txt", None, 100, &options)
.unwrap();
assert!(result.skipped.is_empty());
assert_eq!(result.files_restored, 1);
}
assert!(target.removed.contains(&"/data/file1.txt".to_string()));
}
#[test]
fn test_restore_not_found() {
let provider = create_test_provider();
let mut target = InMemoryRestoreTarget::new();
let mut engine = RestoreEngine::new(&provider, &mut target);
let result = engine.restore("/nonexistent", None, 100, &RestoreOptions::default());
assert!(matches!(result, Err(TimeError::PathNotFound(_))));
}
#[test]
fn test_restore_non_recursive() {
let provider = create_test_provider();
let mut target = InMemoryRestoreTarget::new();
{
let mut engine = RestoreEngine::new(&provider, &mut target);
let options = RestoreOptions {
recursive: false,
..Default::default()
};
let result = engine.restore("/data", None, 100, &options).unwrap();
assert_eq!(result.dirs_restored, 1);
assert_eq!(result.files_restored, 0);
}
}
#[test]
fn test_restore_result_totals() {
let mut result = RestoreResult::new("/data", "/data", 100, false);
result.files_restored = 5;
result.dirs_restored = 2;
assert_eq!(result.total_restored(), 7);
assert!(result.is_success());
result.errors.push(("path".into(), "error".into()));
assert!(!result.is_success());
}
}