use std::path::{Path, PathBuf};
use crate::error::MarsError;
use crate::types::{ContentHash, ItemKind};
pub mod fs_ops;
pub use fs_ops::*;
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum DestinationState {
Empty,
File { hash: ContentHash },
Directory { hash: ContentHash },
Symlink { target: PathBuf },
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum DesiredState {
CopyFile { source: PathBuf, hash: ContentHash },
CopyDir { source: PathBuf, hash: ContentHash },
Symlink { target: PathBuf },
Absent,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum ReconcileOutcome {
Created,
Updated,
Removed,
Skipped {
reason: &'static str,
},
Conflict {
existing: DestinationState,
desired: DesiredState,
},
}
pub fn scan_destination(path: &Path) -> DestinationState {
scan_destination_checked(path).unwrap_or(DestinationState::Empty)
}
pub fn reconcile_one(
dest: &Path,
desired: DesiredState,
force: bool,
) -> Result<ReconcileOutcome, MarsError> {
let existing = scan_destination_checked(dest)?;
match desired {
DesiredState::Absent => {
if matches!(existing, DestinationState::Empty) {
Ok(ReconcileOutcome::Skipped {
reason: "already absent",
})
} else {
safe_remove(dest)?;
Ok(ReconcileOutcome::Removed)
}
}
DesiredState::CopyFile { source, hash } => match existing {
DestinationState::Empty => {
atomic_copy_file(&source, dest)?;
Ok(ReconcileOutcome::Created)
}
DestinationState::File {
hash: existing_hash,
} if existing_hash == hash => Ok(ReconcileOutcome::Skipped {
reason: "already up-to-date",
}),
DestinationState::Symlink { .. } => {
safe_remove(dest)?;
atomic_copy_file(&source, dest)?;
Ok(ReconcileOutcome::Updated)
}
existing_state => {
if !force {
return Ok(ReconcileOutcome::Conflict {
existing: existing_state,
desired: DesiredState::CopyFile { source, hash },
});
}
safe_remove(dest)?;
atomic_copy_file(&source, dest)?;
Ok(ReconcileOutcome::Updated)
}
},
DesiredState::CopyDir { source, hash } => match existing {
DestinationState::Empty => {
atomic_copy_dir(&source, dest)?;
Ok(ReconcileOutcome::Created)
}
DestinationState::Directory {
hash: existing_hash,
} if existing_hash == hash => Ok(ReconcileOutcome::Skipped {
reason: "already up-to-date",
}),
DestinationState::Symlink { .. } => {
safe_remove(dest)?;
atomic_copy_dir(&source, dest)?;
Ok(ReconcileOutcome::Updated)
}
existing_state => {
if !force {
return Ok(ReconcileOutcome::Conflict {
existing: existing_state,
desired: DesiredState::CopyDir { source, hash },
});
}
safe_remove(dest)?;
atomic_copy_dir(&source, dest)?;
Ok(ReconcileOutcome::Updated)
}
},
DesiredState::Symlink { target } => match existing {
DestinationState::Symlink {
target: existing_target,
} if existing_target == target => Ok(ReconcileOutcome::Skipped {
reason: "already symlinked",
}),
DestinationState::Empty => {
atomic_symlink(dest, &target)?;
Ok(ReconcileOutcome::Created)
}
existing_state => {
if !force {
return Ok(ReconcileOutcome::Conflict {
existing: existing_state,
desired: DesiredState::Symlink { target },
});
}
safe_remove(dest)?;
atomic_symlink(dest, &target)?;
Ok(ReconcileOutcome::Updated)
}
},
}
}
fn scan_destination_checked(path: &Path) -> Result<DestinationState, MarsError> {
let metadata = match std::fs::symlink_metadata(path) {
Ok(metadata) => metadata,
Err(e) if e.kind() == std::io::ErrorKind::NotFound => return Ok(DestinationState::Empty),
Err(e) => return Err(e.into()),
};
if metadata.file_type().is_symlink() {
let target = path.read_link()?;
return Ok(DestinationState::Symlink { target });
}
if metadata.is_file() {
return Ok(DestinationState::File {
hash: content_hash(path, ItemKind::Agent)?,
});
}
if metadata.is_dir() {
return Ok(DestinationState::Directory {
hash: content_hash(path, ItemKind::Skill)?,
});
}
Ok(DestinationState::Empty)
}