use anyhow::{Context, Result};
use serde::de::DeserializeOwned;
use serde::ser::Serialize;
use std::collections::BTreeMap;
use std::fs::{self, File};
use std::io::{self, ErrorKind, Read};
use std::path::{Path, PathBuf};
#[cfg(unix)]
use std::process::Command;
use crate::config::UnixUser;
pub fn load_file<T>(filename: &Path) -> Result<Option<T>>
where
T: DeserializeOwned,
{
let mut buf = String::new();
let mut f = match File::open(filename) {
Ok(f) => Ok(f),
Err(e) if e.kind() == ErrorKind::NotFound => return Ok(None),
e => e,
}
.context("open file")?;
f.read_to_string(&mut buf).context("read file")?;
let data = toml::from_str::<T>(&buf).context("deserialize file contents")?;
Ok(Some(data))
}
pub fn save_file<T>(filename: &Path, data: T) -> Result<()>
where
T: Serialize,
{
let data = toml::to_string(&data).context("serialize data")?;
fs::write(filename, data).context("write to file")
}
#[cfg_attr(test, mockall::automock)]
pub trait Filesystem {
fn compare_symlink(&mut self, source: &Path, link: &Path) -> Result<SymlinkComparison>;
fn compare_template(&mut self, target: &Path, cache: &Path) -> Result<TemplateComparison>;
fn remove_file(&mut self, path: &Path) -> Result<()>;
fn read_to_string(&mut self, path: &Path) -> Result<String>;
fn write(&mut self, path: &Path, content: String) -> Result<()>;
fn delete_parents(&mut self, path: &Path, no_ask: bool) -> Result<()>;
fn make_symlink(&mut self, link: &Path, target: &Path, owner: &Option<UnixUser>) -> Result<()>;
fn create_dir_all(&mut self, path: &Path, owner: &Option<UnixUser>) -> Result<()>;
fn copy_file(&mut self, source: &Path, target: &Path, owner: &Option<UnixUser>) -> Result<()>;
fn set_owner(&mut self, file: &Path, owner: &Option<UnixUser>) -> Result<()>;
fn copy_permissions(
&mut self,
source: &Path,
target: &Path,
owner: &Option<UnixUser>,
) -> Result<()>;
}
#[cfg(windows)]
pub struct RealFilesystem {
noconfirm: bool,
}
#[cfg(windows)]
impl RealFilesystem {
pub fn new(noconfirm: bool) -> RealFilesystem {
RealFilesystem { noconfirm }
}
}
#[cfg(windows)]
impl Filesystem for RealFilesystem {
fn compare_symlink(&mut self, source: &Path, link: &Path) -> Result<SymlinkComparison> {
let source_state = get_file_state(source).context("get source state")?;
trace!("Source state: {:#?}", source_state);
let link_state = get_file_state(link).context("get link state")?;
trace!("Link state: {:#?}", link_state);
compare_symlink(source, source_state, link_state)
}
fn compare_template(&mut self, target: &Path, cache: &Path) -> Result<TemplateComparison> {
let target_state = get_file_state(target).context("get state of target")?;
trace!("Target state: {:#?}", target_state);
let cache_state = get_file_state(cache).context("get state of cache")?;
trace!("Cache state: {:#?}", cache_state);
Ok(compare_template(target_state, cache_state))
}
fn remove_file(&mut self, path: &Path) -> Result<()> {
let metadata = path.symlink_metadata().context("get metadata")?;
if metadata.is_dir() {
std::fs::remove_dir_all(path).context("remove directory")
} else {
std::fs::remove_file(path).context("remove file")
}
}
fn read_to_string(&mut self, path: &Path) -> Result<String> {
fs::read_to_string(path).context("read from file")
}
fn write(&mut self, path: &Path, content: String) -> Result<()> {
fs::write(path, content).context("write to file")
}
fn delete_parents(&mut self, path: &Path, no_ask: bool) -> Result<()> {
let mut path = path.parent().context("get parent")?;
while path.is_dir()
&& path
.read_dir()
.context("read the contents of parent directory")?
.next()
.is_none()
{
if (self.noconfirm || no_ask)
|| ask_boolean(&format!(
"Directory at {:?} is now empty. Delete [y/N]? ",
path
))
{
std::fs::remove_dir(path).context(format!("remove directory {:?}", path))?;
}
path = path.parent().context(format!("get parent of {:?}", path))?;
}
Ok(())
}
fn make_symlink(&mut self, link: &Path, target: &Path, owner: &Option<UnixUser>) -> Result<()> {
use std::os::windows::fs;
if let Some(owner) = owner {
warn!(
"Ignoring `owner`={:?} when creating symlink {:?} -> {:?}",
owner, link, target
);
}
let real_source_path = real_path(target).context("get real path of source file")?;
if real_source_path.is_dir() {
fs::symlink_dir(real_source_path, link)
} else {
fs::symlink_file(real_source_path, link)
}
.context("create symlink")
}
fn create_dir_all(&mut self, path: &Path, owner: &Option<UnixUser>) -> Result<()> {
if let Some(owner) = owner {
warn!(
"Ignoring `owner`={:?} when creating directory {:?}",
owner, path
);
}
std::fs::create_dir_all(path).context("create directories")
}
fn copy_file(&mut self, source: &Path, target: &Path, owner: &Option<UnixUser>) -> Result<()> {
if let Some(owner) = owner {
warn!(
"Ignoring `owner`={:?} when copying {:?} -> {:?}",
owner, source, target
);
}
std::fs::copy(source, target).context("copy file")?;
Ok(())
}
fn set_owner(&mut self, file: &Path, owner: &Option<UnixUser>) -> Result<()> {
if owner.is_some() {
warn!("ignoring `owner` field on file {:?}", file);
}
Ok(())
}
fn copy_permissions(
&mut self,
source: &Path,
target: &Path,
owner: &Option<UnixUser>,
) -> Result<()> {
if let Some(owner) = owner {
warn!(
"Ignoring `owner`={:?} when copying permissions {:?} -> {:?}",
owner, source, target
);
}
std::fs::set_permissions(
target,
source
.metadata()
.context("get source metadata")?
.permissions(),
)
.context("set target permissions")
}
}
#[cfg(unix)]
pub struct RealFilesystem {
noconfirm: bool,
sudo_occurred: bool,
}
#[cfg(unix)]
impl RealFilesystem {
pub fn new(noconfirm: bool) -> RealFilesystem {
RealFilesystem {
sudo_occurred: false,
noconfirm,
}
}
fn sudo(&mut self, goal: impl AsRef<str>) -> Command {
if !self.sudo_occurred {
warn!("Elevating permissions ({})", goal.as_ref());
if !log_enabled!(log::Level::Debug) {
warn!("To see more than the first time elevated permissions are used, use verbosity 2 or more (-vv)");
}
self.sudo_occurred = true;
} else {
debug!("Elevating permissions ({})", goal.as_ref());
}
Command::new("sudo")
}
fn is_owned_by_user(path: &Path) -> Result<bool> {
use std::os::unix::fs::MetadataExt;
let file_uid = path.metadata().context("get file metadata")?.uid();
let process_uid = unsafe { libc::geteuid() };
Ok(file_uid == process_uid)
}
}
#[cfg(unix)]
impl Filesystem for RealFilesystem {
fn compare_symlink(&mut self, source: &Path, link: &Path) -> Result<SymlinkComparison> {
let source_state = get_file_state(source).context("get source state")?;
let link_state = get_file_state(link).context("get link state")?;
compare_symlink(source, source_state, link_state)
}
fn compare_template(&mut self, target: &Path, cache: &Path) -> Result<TemplateComparison> {
let target_state = get_file_state(target).context("get state of target")?;
let cache_state = get_file_state(cache).context("get state of cache")?;
Ok(compare_template(target_state, cache_state))
}
fn remove_file(&mut self, path: &Path) -> Result<()> {
let metadata = path.symlink_metadata().context("get metadata")?;
let result = if metadata.is_dir() {
std::fs::remove_dir_all(path)
} else {
std::fs::remove_file(path)
};
match result {
Ok(()) => Ok(()),
Err(e) if e.kind() == std::io::ErrorKind::PermissionDenied => {
let success = self
.sudo(format!("removing file {path:?} as root"))
.arg("rm")
.arg("-r")
.arg(path)
.spawn()
.context("spawn sudo rm command")?
.wait()
.context("wait for sudo rm command")?
.success();
anyhow::ensure!(success, "sudo rm command failed");
Ok(())
}
Err(e) => Err(e).context("remove file"),
}
}
fn read_to_string(&mut self, path: &Path) -> Result<String> {
fs::read_to_string(path).context("read from file")
}
fn write(&mut self, path: &Path, content: String) -> Result<()> {
fs::write(path, content).context("write to file")
}
fn delete_parents(&mut self, path: &Path, no_ask: bool) -> Result<()> {
let mut path = path.parent().context("get parent")?;
while path.is_dir()
&& path
.read_dir()
.context("read the contents of parent directory")?
.next()
.is_none()
{
if (self.noconfirm || no_ask)
|| ask_boolean(&format!(
"Directory at {path:?} is now empty. Delete [y/N]? "
))
{
match std::fs::remove_dir(path) {
Ok(()) => {}
Err(e) if e.kind() == std::io::ErrorKind::PermissionDenied => {
let success = self
.sudo(format!("removing directory {path:?}"))
.arg("rmdir")
.arg(path)
.spawn()
.context("spawn sudo rmdir")?
.wait()
.context("wait for sudo rmdir")?
.success();
anyhow::ensure!(success, "sudo rmdir failed");
}
Err(e) => {
Err(e).context("remove dir")?;
}
}
}
path = path.parent().context(format!("get parent of {path:?}"))?;
}
Ok(())
}
fn make_symlink(&mut self, link: &Path, target: &Path, owner: &Option<UnixUser>) -> Result<()> {
use std::os::unix::fs;
if let Some(owner) = owner {
let success = self
.sudo(format!(
"creating symlink {link:?} -> {target:?} from user {owner:?}"
))
.arg("-u")
.arg(owner.as_sudo_arg())
.arg("ln")
.arg("-s")
.arg(real_path(target).context("get real path of source file")?)
.arg(link)
.spawn()
.context("spawn sudo ln")?
.wait()
.context("wait for sudo ln")?
.success();
anyhow::ensure!(success, "sudo ln failed");
} else {
debug!(
"Creating symlink {:?} -> {:?} as current user...",
link, target
);
fs::symlink(
real_path(target).context("get real path of source file")?,
link,
)
.context("create symlink")?;
}
Ok(())
}
fn create_dir_all(&mut self, path: &Path, owner: &Option<UnixUser>) -> Result<()> {
if let Some(owner) = owner {
let success = self
.sudo(format!(
"Creating directory {path:?} from user {owner:?}..."
))
.arg("-u")
.arg(owner.as_sudo_arg())
.arg("mkdir")
.arg("-p")
.arg(path)
.spawn()
.context("spawn sudo mkdir")?
.wait()
.context("wait for sudo mkdir")?
.success();
anyhow::ensure!(success, "sudo mkdir failed");
} else {
debug!("Creating directory {:?} as current user...", path);
std::fs::create_dir_all(path).context("create directories")?;
}
Ok(())
}
fn copy_file(&mut self, source: &Path, target: &Path, owner: &Option<UnixUser>) -> Result<()> {
use std::io::Write;
if let Some(owner) = owner {
let contents = std::fs::read_to_string(source)
.context("read source file contents as current user")?;
let mut child = self
.sudo(format!(
"Copying {source:?} -> {target:?} as user {owner:?}"
))
.arg("-u")
.arg(owner.as_sudo_arg())
.arg("tee")
.arg(target)
.stdin(std::process::Stdio::piped())
.stdout(std::process::Stdio::null())
.spawn()
.context("spawn sudo tee")?;
child
.stdin
.as_ref()
.expect("has stdin")
.write_all(contents.as_bytes())
.context("give input to tee")?;
let success = child.wait().context("wait for sudo tee")?.success();
anyhow::ensure!(success, "sudo tee failed");
} else {
debug!("Copying {:?} -> {:?} as current user", source, target);
std::fs::copy(source, target).context("copy file")?;
}
Ok(())
}
fn set_owner(&mut self, file: &Path, owner: &Option<UnixUser>) -> Result<()> {
if Self::is_owned_by_user(file).context("detect if file is owned by the current user")?
&& owner.is_none()
{
return Ok(());
}
let owner = owner.clone().unwrap_or(UnixUser::Name(
std::env::var("USER").context("get USER env var")?,
));
let success = self
.sudo(format!("setting owner of {file:?} to user \"{owner}\""))
.arg("chown")
.arg(owner.as_chown_arg())
.arg("-h") .arg(file)
.spawn()
.context("spawn sudo chown command")?
.wait()
.context("wait for sudo chown command")?
.success();
anyhow::ensure!(success, "sudo chown command failed");
Ok(())
}
fn copy_permissions(
&mut self,
source: &Path,
target: &Path,
owner: &Option<UnixUser>,
) -> Result<()> {
if let Some(owner) = owner {
let success = self
.sudo(format!(
"Copying permissions {source:?} -> {target:?} as user {owner:?}"
))
.arg("chmod")
.arg("--reference")
.arg(source)
.arg(target)
.spawn()
.context("spawn sudo chmod command")?
.wait()
.context("wait for sudo chmod command")?
.success();
anyhow::ensure!(success, "sudo chmod failed");
} else {
debug!(
"Copying permissions {:?} -> {:?} as current user",
source, target
);
std::fs::set_permissions(
target,
source
.metadata()
.context("get source metadata")?
.permissions(),
)
.context("set target permissions")?;
}
Ok(())
}
}
pub struct DryRunFilesystem {
file_states: BTreeMap<PathBuf, FileState>,
}
#[derive(Debug, Clone, PartialEq)]
enum FileState {
File(Option<String>),
SymbolicLink(PathBuf),
Directory,
Missing,
}
impl DryRunFilesystem {
pub fn new() -> DryRunFilesystem {
DryRunFilesystem {
file_states: BTreeMap::new(),
}
}
fn get_state(&mut self, path: &Path) -> Result<FileState> {
match self.file_states.get(path) {
Some(state) => Ok(state.clone()),
None => get_file_state(path),
}
}
}
impl Filesystem for DryRunFilesystem {
fn compare_symlink(&mut self, source: &Path, link: &Path) -> Result<SymlinkComparison> {
let source_state = if let Some(state) = self.file_states.get(source) {
debug!("Cached (probably not actual) source state: {:?}", state);
state.clone()
} else {
let state = get_file_state(source).context("get source state")?;
debug!("Source state: {:?}", state);
state
};
let link_state = if let Some(state) = self.file_states.get(link) {
debug!("Cached (probably not actual) link state: {:?}", state);
state.clone()
} else {
let state = get_file_state(link).context("get link state")?;
debug!("Link state: {:?}", state);
state
};
compare_symlink(source, source_state, link_state)
}
fn compare_template(&mut self, target: &Path, cache: &Path) -> Result<TemplateComparison> {
let target_state = if let Some(state) = self.file_states.get(target) {
debug!("Cached (probably not actual) target state: {:?}", state);
state.clone()
} else {
let state = get_file_state(target).context("get state of target")?;
debug!("Target state: {:?}", state);
state
};
let cache_state = if let Some(state) = self.file_states.get(cache) {
debug!("Cached (probably not actual) cache state: {:?}", state);
state.clone()
} else {
let state = get_file_state(cache).context("get state of cache")?;
debug!("Cache state: {:?}", state);
state
};
Ok(compare_template(target_state, cache_state))
}
fn remove_file(&mut self, path: &Path) -> Result<()> {
debug!("Removing file {:?}", path);
self.file_states.insert(path.into(), FileState::Missing);
Ok(())
}
fn read_to_string(&mut self, path: &Path) -> Result<String> {
debug!("Reading contents of file {:?}", path);
match self.get_state(path).context("get file state")? {
FileState::File(s) => Ok(s.context("invalid utf-8 in template source")?),
_ => anyhow::bail!("writing to non-file"),
}
}
fn write(&mut self, path: &Path, content: String) -> Result<()> {
debug!("Writing contents {:?} to file {:?}", content, path);
self.file_states
.insert(path.into(), FileState::File(Some(content)));
Ok(())
}
fn delete_parents(&mut self, path: &Path, _no_ask: bool) -> Result<()> {
debug!(
"Recursively deleting parents of {:?} if they're empty",
path
);
Ok(())
}
fn make_symlink(&mut self, link: &Path, target: &Path, owner: &Option<UnixUser>) -> Result<()> {
debug!(
"Making symlink {:?} -> {:?} (owned by {:?})",
link, target, owner
);
self.file_states
.insert(link.into(), FileState::SymbolicLink(target.into()));
Ok(())
}
fn create_dir_all(&mut self, mut path: &Path, owner: &Option<UnixUser>) -> Result<()> {
debug!("Creating directory {:?} (owned by {:?})", path, owner);
self.file_states.insert(path.into(), FileState::Directory);
while path.parent().is_some() {
path = path.parent().unwrap();
self.file_states.insert(path.into(), FileState::Directory);
}
Ok(())
}
fn copy_file(&mut self, source: &Path, target: &Path, owner: &Option<UnixUser>) -> Result<()> {
debug!(
"Copying file {:?} -> {:?} (target owned by {:?})",
source, target, owner
);
match self.get_state(source).context("get state of source file")? {
FileState::File(content) => {
if self
.get_state(target.parent().context("get parent of target")?)
.context("get state of target's parent")?
== FileState::Directory
{
self.file_states
.insert(target.into(), FileState::File(content));
} else {
anyhow::bail!("target's parent is not a directory");
}
Ok(())
}
s @ (FileState::SymbolicLink(_) | FileState::Directory | FileState::Missing) => {
anyhow::bail!("file is not regular file but is a {:?}", s);
}
}
}
fn set_owner(&mut self, file: &Path, owner: &Option<UnixUser>) -> Result<()> {
debug!("Setting owner of file {:?} to {:?}", file, owner);
Ok(())
}
fn copy_permissions(
&mut self,
source: &Path,
target: &Path,
owner: &Option<UnixUser>,
) -> Result<()> {
debug!(
"Copying permissions on files {:?} -> {:?} (target owned by {:?})",
source, target, owner
);
Ok(())
}
}
fn get_file_state(path: &Path) -> Result<FileState> {
if let Ok(target) = fs::read_link(path) {
return Ok(FileState::SymbolicLink(target));
}
if path.is_dir() {
return Ok(FileState::Directory);
}
match fs::read_to_string(path) {
Ok(f) => Ok(FileState::File(Some(f))),
Err(e) if e.kind() == ErrorKind::InvalidData => Ok(FileState::File(None)),
Err(e) if e.kind() == ErrorKind::NotFound => Ok(FileState::Missing),
Err(e) => Err(e).context("read contents of file that isn't symbolic or directory")?,
}
}
#[derive(Debug, PartialEq, Eq)]
pub enum SymlinkComparison {
Identical,
OnlySourceExists,
OnlyTargetExists,
TargetNotSymlink,
Changed,
BothMissing,
}
impl std::fmt::Display for SymlinkComparison {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> Result<(), std::fmt::Error> {
use self::SymlinkComparison::*;
match self {
Identical => "target points at source",
OnlySourceExists => "target missing",
OnlyTargetExists => "source is missing",
TargetNotSymlink => "target already exists and isn't a symlink",
Changed => "target already exists and doesn't point at source",
BothMissing => "source and target are missing",
}
.fmt(f)
}
}
fn compare_symlink(
source_path: &Path,
source_state: FileState,
link_state: FileState,
) -> Result<SymlinkComparison> {
Ok(match (source_state, link_state) {
(FileState::Missing, FileState::SymbolicLink(_)) => SymlinkComparison::OnlyTargetExists,
(_, FileState::SymbolicLink(t)) => {
if t == real_path(source_path).context("get real path of source")? {
SymlinkComparison::Identical
} else {
SymlinkComparison::Changed
}
}
(FileState::Missing, FileState::Missing) => SymlinkComparison::BothMissing,
(_, FileState::Missing) => SymlinkComparison::OnlySourceExists,
_ => SymlinkComparison::TargetNotSymlink,
})
}
#[derive(Debug, PartialEq, Eq)]
pub enum TemplateComparison {
Identical,
OnlyCacheExists,
OnlyTargetExists,
Changed,
TargetNotRegularFile,
BothMissing,
}
impl std::fmt::Display for TemplateComparison {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> Result<(), std::fmt::Error> {
use self::TemplateComparison::*;
match self {
Identical => "target and cache's contents are equal",
OnlyCacheExists => "target doesn't exist",
OnlyTargetExists => "cache doesn't exist",
Changed => "target contents were changed",
TargetNotRegularFile => "target already exists and isn't a regular file",
BothMissing => "cache and target are missing",
}
.fmt(f)
}
}
fn compare_template(target_state: FileState, cache_state: FileState) -> TemplateComparison {
match (target_state, cache_state) {
(FileState::File(t), FileState::File(c)) => {
if t == c {
TemplateComparison::Identical
} else {
TemplateComparison::Changed
}
}
(FileState::File(_), FileState::Missing) => TemplateComparison::OnlyTargetExists,
(FileState::Missing, FileState::File(_)) => TemplateComparison::OnlyCacheExists,
(FileState::Missing, FileState::Missing) => TemplateComparison::BothMissing,
_ => TemplateComparison::TargetNotRegularFile,
}
}
pub fn real_path(path: &Path) -> Result<PathBuf, io::Error> {
let path = std::fs::canonicalize(path)?;
Ok(platform_dunce(&path))
}
pub fn ask_boolean(prompt: &str) -> bool {
let mut buf = String::from("a"); while !(buf.to_lowercase().starts_with('y')
|| buf.to_lowercase().starts_with('n')
|| buf.is_empty())
{
eprintln!("{prompt}");
buf.clear();
io::stdin()
.read_line(&mut buf)
.expect("Failed to read line from stdin");
}
buf.to_lowercase().starts_with('y')
}
pub fn is_template(source: &Path) -> Result<bool> {
if fs::metadata(source)?.is_dir() {
return Ok(false);
}
let mut file = File::open(source).context("open file")?;
let mut buf = String::new();
if file.read_to_string(&mut buf).is_err() {
warn!("File {:?} is not valid UTF-8 - detecting as symlink. Explicitly specify it to silence this message.", source);
Ok(false)
} else {
Ok(buf.contains("{{"))
}
}
#[cfg(windows)]
pub fn symlinks_enabled(test_file_path: &Path) -> Result<bool> {
use std::os::windows::fs;
debug!(
"Testing whether symlinks are enabled on path {:?}",
test_file_path
);
let _ = std::fs::remove_file(test_file_path);
match fs::symlink_file("test.txt", test_file_path) {
Ok(()) => {
std::fs::remove_file(test_file_path)
.context(format!("remove test file {:?}", test_file_path))?;
Ok(true)
}
Err(e) => {
if e.raw_os_error() == Some(1314) {
Ok(false)
} else {
Err(e).context(format!("create test symlink at {:?}", test_file_path))
}
}
}
}
#[cfg(unix)]
pub fn symlinks_enabled(_test_file_path: &Path) -> Result<bool> {
Ok(true)
}
#[cfg(windows)]
pub fn platform_dunce(path: &Path) -> PathBuf {
dunce::simplified(path).into()
}
#[cfg(unix)]
pub fn platform_dunce(path: &Path) -> PathBuf {
path.into()
}
#[cfg(test)]
mod test {
use super::*;
#[test]
fn simple_remove() {
let mut fs = DryRunFilesystem::new();
fs.remove_file(&PathBuf::from("test")).unwrap();
assert_eq!(
fs.file_states.get(&PathBuf::from("test")),
Some(&FileState::Missing)
);
}
#[test]
fn simple_write_read() {
let mut fs = DryRunFilesystem::new();
fs.write(&PathBuf::from("test"), "hello world!".into())
.unwrap();
assert_eq!(
fs.read_to_string(&PathBuf::from("test")).unwrap(),
"hello world!"
);
}
#[test]
fn simple_create_dir_all() {
let mut fs = DryRunFilesystem::new();
fs.create_dir_all(&PathBuf::from("/home/user/.config"), &None)
.unwrap();
assert_eq!(
fs.get_state(&PathBuf::from("/home")).unwrap(),
FileState::Directory
);
assert_eq!(
fs.get_state(&PathBuf::from("/home/user")).unwrap(),
FileState::Directory
);
assert_eq!(
fs.get_state(&PathBuf::from("/home/user/.config")).unwrap(),
FileState::Directory
);
}
#[test]
fn full_dry_run() {
let mut fs = DryRunFilesystem::new();
fs.write(&PathBuf::from("source"), "{{name}}".into())
.unwrap();
fs.remove_file(&PathBuf::from("target_dir/target")).unwrap();
assert_eq!(
fs.compare_template(
&PathBuf::from("target_dir/target"),
&PathBuf::from("cache_dir/cache")
)
.unwrap(),
TemplateComparison::BothMissing
);
fs.create_dir_all(&PathBuf::from("target_dir"), &None)
.unwrap();
assert_eq!(
fs.read_to_string(&PathBuf::from("source")).unwrap(),
"{{name}}"
);
let rendered = String::from("John");
fs.create_dir_all(&PathBuf::from("cache_dir"), &None)
.unwrap();
fs.write(&PathBuf::from("cache_dir/cache"), rendered)
.unwrap();
fs.copy_file(
&PathBuf::from("cache_dir/cache"),
&PathBuf::from("target_dir/target"),
&None,
)
.unwrap();
fs.copy_permissions(
&PathBuf::from("source"),
&PathBuf::from("target_dir/target"),
&None,
)
.unwrap();
assert_eq!(
fs.file_states.get(&PathBuf::from("source")),
Some(&FileState::File(Some("{{name}}".into())))
);
assert_eq!(
fs.file_states.get(&PathBuf::from("cache_dir")),
Some(&FileState::Directory)
);
assert_eq!(
fs.file_states.get(&PathBuf::from("cache_dir/cache")),
Some(&FileState::File(Some("John".into())))
);
assert_eq!(
fs.file_states.get(&PathBuf::from("target_dir")),
Some(&FileState::Directory)
);
assert_eq!(
fs.file_states.get(&PathBuf::from("target_dir/target")),
Some(&FileState::File(Some("John".into())))
);
}
#[test]
fn dry_run_error_cases() {
let mut fs = DryRunFilesystem::new();
fs.write(&PathBuf::from("source"), "hello".into()).unwrap();
fs.remove_file(&PathBuf::from("some_dir")).unwrap();
fs.copy_file(
&PathBuf::from("source"),
&PathBuf::from("some_dir/target"),
&None,
)
.unwrap_err();
fs.make_symlink(&PathBuf::from("link"), &PathBuf::from("target"), &None)
.unwrap();
fs.copy_file(&PathBuf::from("link"), &PathBuf::from("link2"), &None)
.unwrap_err();
}
}