use cap_std::fs::{Dir, File, Metadata};
use cap_tempfile::cap_std;
use std::ffi::OsStr;
use std::io::Result;
use std::io::{self, Write};
use std::ops::Deref;
use std::path::Path;
pub trait CapStdExtDirExt {
fn open_optional(&self, path: impl AsRef<Path>) -> Result<Option<File>>;
fn open_dir_optional(&self, path: impl AsRef<Path>) -> Result<Option<Dir>>;
fn ensure_dir_with(
&self,
p: impl AsRef<Path>,
builder: &cap_std::fs::DirBuilder,
) -> Result<bool>;
fn metadata_optional(&self, path: impl AsRef<Path>) -> Result<Option<Metadata>>;
fn symlink_metadata_optional(&self, path: impl AsRef<Path>) -> Result<Option<Metadata>>;
fn remove_file_optional(&self, path: impl AsRef<Path>) -> Result<bool>;
fn remove_all_optional(&self, path: impl AsRef<Path>) -> Result<bool>;
#[cfg(unix)]
fn update_timestamps(&self, path: impl AsRef<Path>) -> Result<()>;
fn atomic_replace_with<F, T, E>(
&self,
destname: impl AsRef<Path>,
f: F,
) -> std::result::Result<T, E>
where
F: FnOnce(&mut std::io::BufWriter<cap_tempfile::TempFile>) -> std::result::Result<T, E>,
E: From<std::io::Error>;
fn atomic_write(&self, destname: impl AsRef<Path>, contents: impl AsRef<[u8]>) -> Result<()>;
fn atomic_write_with_perms(
&self,
destname: impl AsRef<Path>,
contents: impl AsRef<[u8]>,
perms: cap_std::fs::Permissions,
) -> Result<()>;
}
fn map_optional<R>(r: Result<R>) -> Result<Option<R>> {
match r {
Ok(v) => Ok(Some(v)),
Err(e) => {
if e.kind() == std::io::ErrorKind::NotFound {
Ok(None)
} else {
Err(e)
}
}
}
}
enum DirOwnedOrBorrowed<'d> {
Owned(Dir),
Borrowed(&'d Dir),
}
impl<'d> Deref for DirOwnedOrBorrowed<'d> {
type Target = Dir;
fn deref(&self) -> &Self::Target {
match self {
Self::Owned(d) => d,
Self::Borrowed(d) => d,
}
}
}
fn subdir_of<'d, 'p>(d: &'d Dir, p: &'p Path) -> io::Result<(DirOwnedOrBorrowed<'d>, &'p OsStr)> {
let name = p
.file_name()
.ok_or_else(|| std::io::Error::new(std::io::ErrorKind::InvalidInput, "Not a file name"))?;
let r = if let Some(subdir) = p
.parent()
.filter(|v| !v.as_os_str().is_empty())
.map(|p| d.open_dir(p))
{
DirOwnedOrBorrowed::Owned(subdir?)
} else {
DirOwnedOrBorrowed::Borrowed(d)
};
Ok((r, name))
}
impl CapStdExtDirExt for Dir {
fn open_optional(&self, path: impl AsRef<Path>) -> Result<Option<File>> {
map_optional(self.open(path.as_ref()))
}
fn open_dir_optional(&self, path: impl AsRef<Path>) -> Result<Option<Dir>> {
map_optional(self.open_dir(path.as_ref()))
}
fn ensure_dir_with(
&self,
p: impl AsRef<Path>,
builder: &cap_std::fs::DirBuilder,
) -> Result<bool> {
let p = p.as_ref();
match self.create_dir_with(p, builder) {
Ok(()) => Ok(true),
Err(e) if e.kind() == std::io::ErrorKind::AlreadyExists => {
if !self.symlink_metadata(p)?.is_dir() {
return Err(io::Error::new(io::ErrorKind::Other, "Found non-directory"));
}
Ok(false)
}
Err(e) => Err(e),
}
}
fn metadata_optional(&self, path: impl AsRef<Path>) -> Result<Option<Metadata>> {
map_optional(self.metadata(path.as_ref()))
}
fn symlink_metadata_optional(&self, path: impl AsRef<Path>) -> Result<Option<Metadata>> {
map_optional(self.symlink_metadata(path.as_ref()))
}
fn remove_file_optional(&self, path: impl AsRef<Path>) -> Result<bool> {
match self.remove_file(path.as_ref()) {
Ok(()) => Ok(true),
Err(e) if e.kind() == std::io::ErrorKind::NotFound => Ok(false),
Err(e) => Err(e),
}
}
fn remove_all_optional(&self, path: impl AsRef<Path>) -> Result<bool> {
let path = path.as_ref();
let meta = match self.symlink_metadata_optional(path)? {
Some(m) => m,
None => return Ok(false),
};
if meta.is_dir() {
self.remove_dir_all(path)?;
} else {
self.remove_file(path)?;
}
Ok(true)
}
#[cfg(unix)]
fn update_timestamps(&self, path: impl AsRef<Path>) -> Result<()> {
use rustix::fd::AsFd;
use rustix::fs::UTIME_NOW;
let path = path.as_ref();
let now = rustix::fs::Timespec {
tv_sec: 0,
tv_nsec: UTIME_NOW,
};
#[allow(clippy::clone_on_copy)]
let times = rustix::fs::Timestamps {
last_access: now.clone(),
last_modification: now.clone(),
};
rustix::fs::utimensat(
self.as_fd(),
path,
×,
rustix::fs::AtFlags::SYMLINK_NOFOLLOW,
)?;
Ok(())
}
fn atomic_replace_with<F, T, E>(
&self,
destname: impl AsRef<Path>,
f: F,
) -> std::result::Result<T, E>
where
F: FnOnce(&mut std::io::BufWriter<cap_tempfile::TempFile>) -> std::result::Result<T, E>,
E: From<std::io::Error>,
{
let destname = destname.as_ref();
let (d, name) = subdir_of(self, destname)?;
let t = cap_tempfile::TempFile::new(&d)?;
let mut bufw = std::io::BufWriter::new(t);
let r = f(&mut bufw)?;
bufw.into_inner()
.map_err(From::from)
.and_then(|t| t.replace(name))?;
Ok(r)
}
fn atomic_write(&self, destname: impl AsRef<Path>, contents: impl AsRef<[u8]>) -> Result<()> {
self.atomic_replace_with(destname, |f| f.write_all(contents.as_ref()))
}
fn atomic_write_with_perms(
&self,
destname: impl AsRef<Path>,
contents: impl AsRef<[u8]>,
perms: cap_std::fs::Permissions,
) -> Result<()> {
self.atomic_replace_with(destname, |f| -> io::Result<_> {
#[cfg(unix)]
{
use std::os::unix::prelude::PermissionsExt;
let perms = cap_std::fs::Permissions::from_mode(0o600);
f.get_mut().as_file_mut().set_permissions(perms)?;
}
f.write_all(contents.as_ref())?;
f.flush()?;
f.get_mut().as_file_mut().set_permissions(perms)?;
Ok(())
})
}
}