use crate::{
hierarchy::Data,
opts::Opts,
unit::{CopyFile, CopyTemplate, CreateDir, Dependency, Symlink, SystemUnit, UnitAllocator},
};
use anyhow::{bail, format_err, Context as _, Error};
use fxhash::FxHashMap;
use std::fs;
use std::io;
use std::path::{Path, PathBuf};
use std::sync::Mutex;
use std::time::SystemTime;
#[derive(Default)]
pub struct FileSystemInner {
paths: FxHashMap<PathBuf, Dependency>,
invalid: bool,
}
pub struct FileSystem<'a> {
opts: &'a Opts,
state_dir: PathBuf,
allocator: &'a UnitAllocator,
data: &'a Data,
inner: Mutex<FileSystemInner>,
}
macro_rules! dependency {
($name:ident, $slf:ident, $path:ident) => {{
let mut inner = $slf
.inner
.lock()
.map_err(|_| format_err!("Lock poisoned"))?;
if let Some(dependency) = inner.paths.get_mut($path) {
if let Dependency::$name(_) = *dependency {
return Ok(*dependency);
}
bail!(
"Multiple systems modifying path `{}` in different ways",
$path.display()
);
}
let dependency = Dependency::$name($slf.allocator.allocate());
Ok(*inner.paths.entry($path.to_owned()).or_insert(dependency))
}};
}
impl<'a> FileSystem<'a> {
pub fn new(
opts: &'a Opts,
state_dir: &Path,
allocator: &'a UnitAllocator,
data: &'a Data,
) -> FileSystem<'a> {
FileSystem {
opts,
state_dir: state_dir.to_owned(),
allocator,
data,
inner: Mutex::new(FileSystemInner::default()),
}
}
pub fn validate(self) -> Result<(), Error> {
let inner = self
.inner
.lock()
.map_err(|_| format_err!("Lock poisoned"))?;
if !inner.invalid {
return Ok(());
}
bail!("Multiple systems with conflicting path modifications");
}
pub fn file_dependency(&self, path: &Path) -> Result<Dependency, Error> {
dependency!(File, self, path)
}
pub fn dir_dependency(&self, path: &Path) -> Result<Dependency, Error> {
dependency!(Dir, self, path)
}
pub fn symlink(
&self,
path: &Path,
link: PathBuf,
meta: Option<&fs::Metadata>,
) -> Result<Option<SystemUnit>, Error> {
let remove = match meta {
Some(meta) => {
let ty = meta.file_type();
if !ty.is_symlink() {
bail!("File exists but is not a symlink: {}", path.display());
}
let actual_link = fs::read_link(path)?;
if actual_link == link {
return Ok(None);
}
if !self.opts.force {
bail!(
"Symlink exists `{}`, but contains the wrong link `{}`, expected: {} (use `--force` to override)",
path.display(),
actual_link.display(),
link.display(),
);
}
true
}
None => false,
};
let mut unit = self.allocator.unit(Symlink {
remove,
path: path.to_owned(),
link,
});
if let Some(parent) = path.parent() {
if !parent.is_dir() {
unit.dependencies.push(self.dir_dependency(parent)?);
}
}
unit.provides.push(self.file_dependency(path)?);
Ok(Some(unit))
}
pub fn copy_file(
&self,
from: &Path,
from_meta: fs::Metadata,
to: &Path,
to_meta: Option<&fs::Metadata>,
template: bool,
) -> Result<Option<SystemUnit>, Error> {
let from_modified = match self.should_copy_file(&from_meta, &to, to_meta, template)? {
Some(modified) => modified,
None => return Ok(None),
};
let mut unit = if template {
self.allocator.unit(CopyTemplate {
from: from.to_owned(),
from_modified,
to: to.to_owned(),
to_exists: to_meta.is_some(),
})
} else {
self.allocator.unit(CopyFile {
from: from.to_owned(),
from_modified,
to: to.to_owned(),
})
};
if let Some(parent) = to.parent() {
if !parent.is_dir() {
unit.dependencies.push(self.dir_dependency(parent)?);
}
}
unit.provides.push(self.file_dependency(to)?);
Ok(Some(unit))
}
pub fn create_dir_all(&self, dir: &Path) -> Result<Vec<SystemUnit>, Error> {
let mut inner = self
.inner
.lock()
.map_err(|_| format_err!("Lock poisoned"))?;
let dirs = {
if inner.paths.contains_key(dir) {
return Ok(vec![]);
}
if dir.is_dir() {
return Ok(vec![]);
}
let mut dirs = Vec::new();
let mut c = dir;
dirs.push(c);
while let Some(parent) = c.parent() {
if parent.is_dir() {
break;
}
if inner.paths.contains_key(parent) {
break;
}
dirs.push(parent);
c = parent;
}
dirs
};
let mut out = Vec::new();
for dir in dirs.into_iter().rev() {
let mut unit = self.allocator.unit(CreateDir(dir.to_owned()));
let current_dependency = Dependency::Dir(unit.id);
let dependency = *inner
.paths
.entry(dir.to_owned())
.or_insert(current_dependency);
if dependency != current_dependency {
if let Dependency::Dir(_) = dependency {
continue;
}
bail!(
"Other system is modifying path, but not as a directory: {}",
dir.display()
);
}
unit.provides.push(dependency);
if let Some(parent) = dir.parent() {
unit.dependencies.extend(inner.paths.get(parent).cloned());
}
out.push(unit);
}
Ok(out)
}
pub fn state_path(&self, id: &str) -> PathBuf {
self.state_dir.join(id)
}
pub fn try_open_meta(p: &Path) -> Result<Option<fs::Metadata>, Error> {
match p.symlink_metadata() {
Ok(m) => Ok(Some(m)),
Err(e) => match e.kind() {
io::ErrorKind::NotFound => Ok(None),
_ => bail!("to get metadata: {}: {}", p.display(), e),
},
}
}
pub fn should_create_dir(path: &Path, meta: Option<&fs::Metadata>) -> Result<bool, Error> {
let meta = match meta {
Some(meta) => meta,
None => return Ok(true),
};
let ty = meta.file_type();
if !ty.is_dir() {
bail!("Exists but is not a dir: {}", path.display());
}
Ok(false)
}
pub fn path_relative_from(path: &Path, base: &Path) -> Option<PathBuf> {
use std::path::Component;
if path.is_absolute() != base.is_absolute() {
if path.is_absolute() {
return Some(PathBuf::from(path));
} else {
return None;
}
}
let mut ita = path.components();
let mut itb = base.components();
let mut comps: Vec<Component> = vec![];
loop {
match (ita.next(), itb.next()) {
(None, None) => break,
(Some(a), None) => {
comps.push(a);
comps.extend(ita.by_ref());
break;
}
(None, _) => comps.push(Component::ParentDir),
(Some(a), Some(b)) if comps.is_empty() && a == b => (),
(Some(a), Some(b)) if b == Component::CurDir => comps.push(a),
(Some(_), Some(b)) if b == Component::ParentDir => return None,
(Some(a), Some(_)) => {
comps.push(Component::ParentDir);
for _ in itb {
comps.push(Component::ParentDir);
}
comps.push(a);
comps.extend(ita.by_ref());
break;
}
}
}
Some(comps.iter().map(|c| c.as_os_str()).collect())
}
pub fn touch(path: &Path, timestamp: &SystemTime) -> Result<(), Error> {
use filetime::FileTime;
let accessed = FileTime::from_system_time(*timestamp);
let modified = accessed;
filetime::set_file_times(path, accessed, modified)
.with_context(|| format_err!("Failed to update timestamps for: {}", path.display()))?;
Ok(())
}
fn should_copy_file(
&self,
from: &fs::Metadata,
to: &Path,
to_meta: Option<&fs::Metadata>,
template: bool,
) -> Result<Option<SystemTime>, Error> {
let from_modified = from.modified()?;
let to_meta = match to_meta {
Some(to_meta) => to_meta,
None => return Ok(Some(from_modified)),
};
if !to_meta.is_file() {
bail!("Exists but is not a file: {}", to.display());
}
let to_modified = to_meta.modified()?;
let modified = if template {
match self.data.last_modified.as_ref() {
Some(data_modified) if from_modified < *data_modified => data_modified,
_ => &from_modified,
}
} else {
&from_modified
};
if *modified != to_modified {
return Ok(Some(*modified));
}
Ok(None)
}
}