use std::fs::{File, Permissions};
use std::io::{Seek, Write};
use std::path::{Path, PathBuf};
use tempfile::NamedTempFile;
use crate::error::Result;
#[derive(Debug)]
pub struct Destination {
tmp: NamedTempFile,
path: PathBuf,
force: bool,
mode: Permissions,
}
impl Destination {
pub fn new<P: AsRef<Path>>(path: P, force: bool) -> Result<Self> {
let path = path.as_ref();
if !force && path.exists() {
return Err(already_exists(path).into());
}
Self::at(path, force, None)
}
pub fn in_place<P: AsRef<Path>>(path: P) -> Result<Self> {
let real = std::fs::canonicalize(path)?;
let mode = std::fs::metadata(&real)?.permissions();
Self::at(&real, true, Some(mode))
}
fn at(path: &Path, force: bool, mode: Option<Permissions>) -> Result<Self> {
let dir = path
.parent()
.filter(|p| !p.as_os_str().is_empty())
.unwrap_or_else(|| Path::new("."));
let tmp = NamedTempFile::new_in(dir).map_err(|e| {
std::io::Error::new(
e.kind(),
format!("cannot write into {}: {e}", dir.display()),
)
})?;
let mode = match mode {
Some(m) => m,
None => new_file_mode(tmp.path())?,
};
Ok(Self {
tmp,
path: path.to_owned(),
force,
mode,
})
}
pub fn writer(&mut self) -> &mut File {
self.tmp.as_file_mut()
}
pub fn written(&mut self) -> Result<&mut File> {
let f = self.writer();
f.flush()?;
f.rewind()?;
Ok(f)
}
pub fn commit(self) -> Result<()> {
let Self {
tmp,
path,
force,
mode,
} = self;
tmp.as_file().set_permissions(mode)?;
let outcome = if force {
tmp.persist(&path).map_err(|e| e.error)
} else {
tmp.persist_noclobber(&path).map_err(|e| e.error)
};
match outcome {
Ok(_) => Ok(()),
Err(e) if e.kind() == std::io::ErrorKind::AlreadyExists => {
Err(already_exists(&path).into())
}
Err(e) => Err(e.into()),
}
}
}
fn already_exists(path: &Path) -> std::io::Error {
std::io::Error::new(
std::io::ErrorKind::AlreadyExists,
format!("{} exists", path.display()),
)
}
fn new_file_mode(near: &Path) -> Result<Permissions> {
let mut name = near.as_os_str().to_owned();
name.push(".mode");
let probe = PathBuf::from(name);
let f = std::fs::OpenOptions::new()
.write(true)
.create_new(true)
.open(&probe)?;
let mode = f.metadata().map(|m| m.permissions());
drop(f);
std::fs::remove_file(&probe)?;
Ok(mode?)
}