use std::ffi::OsStr;
use std::ffi::OsString;
use std::fmt::Debug;
use std::fs::canonicalize;
use std::fs::create_dir_all;
use std::fs::metadata;
use std::io::ErrorKind;
use std::iter;
use std::path::Path;
use std::path::PathBuf;
use std::str;
use anyhow::Context as _;
use anyhow::Result;
use crate::util::check;
use crate::util::output;
use crate::util::run;
use crate::util::Either;
pub trait FileOps
where
Self: Debug,
{
fn is_dir(&self, path: &Path) -> Result<bool>;
fn create_dir_all(&self, path: &Path) -> Result<()>;
fn canonicalize(&self, path: &Path) -> Result<PathBuf>;
}
#[derive(Clone, Debug, Default)]
pub struct LocalOps;
impl FileOps for LocalOps {
fn is_dir(&self, path: &Path) -> Result<bool> {
match metadata(path) {
Ok(info) => Ok(info.is_dir()),
Err(err) if err.kind() == ErrorKind::NotFound => Ok(false),
Err(err) => Err(err.into()),
}
}
fn create_dir_all(&self, path: &Path) -> Result<()> {
let () = create_dir_all(path)?;
Ok(())
}
fn canonicalize(&self, path: &Path) -> Result<PathBuf> {
let path = canonicalize(path)?;
Ok(path)
}
}
#[derive(Debug)]
pub struct RemoteOps {
command: OsString,
args: Vec<OsString>,
}
impl RemoteOps {
pub fn new<C, A, S>(command: C, args: A) -> Self
where
C: AsRef<OsStr>,
A: IntoIterator<Item = S>,
S: AsRef<OsStr>,
{
Self {
command: command.as_ref().to_os_string(),
args: args
.into_iter()
.map(|arg| arg.as_ref().to_os_string())
.collect(),
}
}
fn command<'slf, C, A, S>(
&'slf self,
command: C,
args: A,
) -> (
impl AsRef<OsStr> + Clone + 'slf,
impl IntoIterator<Item = impl AsRef<OsStr> + Clone + 'slf> + Clone + 'slf,
)
where
C: AsRef<OsStr> + Clone + 'slf,
A: IntoIterator<Item = S> + 'slf,
A::IntoIter: Clone,
S: AsRef<OsStr> + Clone + 'slf,
{
let mut iter = iter::once(AsRef::<OsStr>::as_ref(&self.command))
.chain(self.args.iter().map(OsString::as_ref))
.map(Either::Left)
.chain(iter::once(Either::Right(Either::Left(command))))
.chain(args.into_iter().map(Either::Right).map(Either::Right));
let command = iter.next().expect("constructed command is empty");
(command, iter)
}
}
impl FileOps for RemoteOps {
fn is_dir(&self, path: &Path) -> Result<bool> {
let (command, args) = self.command("test", ["-d".as_ref(), path.as_os_str()]);
check(command, args)
}
fn create_dir_all(&self, path: &Path) -> Result<()> {
let (command, args) = self.command("mkdir", ["-p".as_ref(), path.as_os_str()]);
run(command, args)
}
fn canonicalize(&self, path: &Path) -> Result<PathBuf> {
let (command, args) = self.command("readlink", ["-e".as_ref(), path.as_os_str()]);
let output = output(command, args)?;
let path = str::from_utf8(&output)
.context("failed to read `readlink -e` output as UTF-8 string")?
.trim_end()
.to_string();
Ok(PathBuf::from(path))
}
}
#[cfg(test)]
mod tests {
use super::*;
use std::os::unix::fs::symlink;
use tempfile::TempDir;
fn sh() -> PathBuf {
Path::new(env!("CARGO_MANIFEST_DIR"))
.join("var")
.join("sh-run")
}
fn for_each_file_op(test_fn: fn(&dyn FileOps)) {
let local_ops = LocalOps;
let remote_ops = RemoteOps::new(sh(), [""; 0]);
[&local_ops as &dyn FileOps, &remote_ops as &dyn FileOps]
.into_iter()
.for_each(test_fn)
}
#[test]
fn directory_check() {
fn test(ops: &dyn FileOps) {
let path = {
let directory = TempDir::new().unwrap();
assert!(ops.is_dir(directory.path()).unwrap());
directory.path().to_path_buf()
};
assert!(!ops.is_dir(&path).unwrap())
}
for_each_file_op(test)
}
#[test]
fn directory_creation() {
fn test(ops: &dyn FileOps) {
let directory = TempDir::new().unwrap();
let subdir = directory.path().join("foobar").join("baz");
let () = ops.create_dir_all(&subdir).unwrap();
assert!(ops.is_dir(directory.path()).unwrap());
}
for_each_file_op(test)
}
#[test]
fn path_canonicalization() {
fn test(ops: &dyn FileOps) {
let directory = TempDir::new().unwrap();
let subdir = directory.path().join("sub-dir");
let path = subdir.join(".").join("..").join("sub-dir");
let _err = ops.canonicalize(&path).unwrap_err();
let () = ops.create_dir_all(&subdir).unwrap();
let canonical = ops.canonicalize(&path).unwrap();
let suffix = canonical.strip_prefix(directory.path()).unwrap();
assert_eq!(suffix, Path::new("sub-dir"));
let link = directory.path().join("symlink");
let () = symlink(subdir, &link).unwrap();
let canonical = ops.canonicalize(&link).unwrap();
let suffix = canonical.strip_prefix(directory.path()).unwrap();
assert_eq!(suffix, Path::new("sub-dir"));
}
for_each_file_op(test)
}
}