use std::error;
use std::ffi::OsString;
use std::fmt;
use std::fs::{metadata, rename, symlink_metadata, File};
use std::io;
use std::path::{Path, PathBuf};
use tempfile::{Builder, NamedTempFile, PersistError};
#[derive(Clone, Debug, Eq, PartialEq)]
pub struct InPlace {
path: PathBuf,
backup: Option<Backup>,
follow_symlinks: bool,
}
impl InPlace {
pub fn new<P: AsRef<Path>>(path: P) -> InPlace {
InPlace {
path: path.as_ref().into(),
backup: None,
follow_symlinks: true,
}
}
pub fn backup(&mut self, backup: Backup) -> &mut Self {
self.backup = Some(backup);
self
}
pub fn no_backup(&mut self) -> &mut Self {
self.backup = None;
self
}
pub fn follow_symlinks(&mut self, flag: bool) -> &mut Self {
self.follow_symlinks = flag;
self
}
pub fn open(&self) -> Result<InPlaceFile, InPlaceError> {
let path = if self.follow_symlinks {
self.path
.canonicalize()
.map_err(InPlaceError::canonicalize)?
} else {
absolutize(&self.path)?
};
let backup_path = match self.backup.as_ref() {
Some(bkp) => Some(absolutize(&bkp.apply(&path)?)?),
None => None,
};
let writer = mktemp(&path)?;
copystats(&path, writer.as_file(), self.follow_symlinks)?;
let reader = File::open(&path).map_err(InPlaceError::open)?;
Ok(InPlaceFile {
reader,
writer,
path,
backup_path,
})
}
}
#[derive(Clone, Debug, Eq, PartialEq)]
pub enum Backup {
Path(PathBuf),
FileName(OsString),
Extension(OsString),
Append(OsString),
}
impl Backup {
fn apply(&self, path: &Path) -> Result<PathBuf, InPlaceError> {
match self {
Backup::Path(p) => {
if p == Path::new("") {
Err(InPlaceError::empty_backup())
} else {
Ok(p.clone())
}
}
Backup::FileName(fname) => {
if fname.is_empty() {
Err(InPlaceError::empty_backup())
} else {
Ok(path.with_file_name(fname))
}
}
Backup::Extension(ext) => Ok(path.with_extension(ext)),
Backup::Append(ext) => {
if ext.is_empty() {
Err(InPlaceError::empty_backup())
} else {
match path.file_name() {
Some(fname) => {
let mut fname = fname.to_os_string();
fname.push(ext);
Ok(path.with_file_name(&fname))
}
None => Err(InPlaceError::no_filename()),
}
}
}
}
}
}
#[derive(Debug)]
pub struct InPlaceFile {
reader: File,
writer: NamedTempFile,
path: PathBuf,
backup_path: Option<PathBuf>,
}
impl InPlaceFile {
pub fn reader(&self) -> &File {
&self.reader
}
pub fn writer(&self) -> &File {
self.writer.as_file()
}
pub fn path(&self) -> &Path {
&self.path
}
pub fn backup_path(&self) -> Option<&Path> {
self.backup_path.as_deref()
}
pub fn save(self) -> Result<(), InPlaceError> {
drop(self.reader);
if let Some(bp) = self.backup_path.as_ref() {
rename(&self.path, bp).map_err(InPlaceError::save_backup)?;
}
match self.writer.persist(&self.path) {
Ok(_) => Ok(()),
Err(e) => {
if let Some(bp) = self.backup_path.as_ref() {
let _ = rename(bp, &self.path);
}
Err(InPlaceError::persist(e))
}
}
}
pub fn discard(self) -> Result<(), InPlaceError> {
self.writer.close().map_err(InPlaceError::rmtemp)
}
}
#[derive(Debug)]
pub struct InPlaceError {
kind: InPlaceErrorKind,
source: Option<io::Error>,
}
impl InPlaceError {
pub fn kind(&self) -> InPlaceErrorKind {
self.kind
}
pub fn as_io_error(&self) -> Option<&io::Error> {
self.source.as_ref()
}
pub fn into_io_error(self) -> Option<io::Error> {
self.source
}
fn get_metadata(source: io::Error) -> InPlaceError {
InPlaceError {
kind: InPlaceErrorKind::GetMetadata,
source: Some(source),
}
}
fn set_metadata(source: io::Error) -> InPlaceError {
InPlaceError {
kind: InPlaceErrorKind::SetMetadata,
source: Some(source),
}
}
fn no_parent() -> InPlaceError {
InPlaceError {
kind: InPlaceErrorKind::NoParent,
source: None,
}
}
fn mktemp(source: io::Error) -> InPlaceError {
InPlaceError {
kind: InPlaceErrorKind::Mktemp,
source: Some(source),
}
}
fn canonicalize(source: io::Error) -> InPlaceError {
InPlaceError {
kind: InPlaceErrorKind::Canonicalize,
source: Some(source),
}
}
fn cwd(source: io::Error) -> InPlaceError {
InPlaceError {
kind: InPlaceErrorKind::CurrentDir,
source: Some(source),
}
}
fn open(source: io::Error) -> InPlaceError {
InPlaceError {
kind: InPlaceErrorKind::Open,
source: Some(source),
}
}
fn empty_backup() -> InPlaceError {
InPlaceError {
kind: InPlaceErrorKind::EmptyBackup,
source: None,
}
}
fn no_filename() -> InPlaceError {
InPlaceError {
kind: InPlaceErrorKind::NoFilename,
source: None,
}
}
fn save_backup(source: io::Error) -> InPlaceError {
InPlaceError {
kind: InPlaceErrorKind::SaveBackup,
source: Some(source),
}
}
fn persist(source: PersistError) -> InPlaceError {
InPlaceError {
kind: InPlaceErrorKind::PersistTemp,
source: Some(source.error),
}
}
fn rmtemp(source: io::Error) -> InPlaceError {
InPlaceError {
kind: InPlaceErrorKind::Rmtemp,
source: Some(source),
}
}
}
impl fmt::Display for InPlaceError {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "{}", self.kind.message())
}
}
impl error::Error for InPlaceError {
fn source(&self) -> Option<&(dyn error::Error + 'static)> {
self.source.as_ref().map(|e| {
let e2: &dyn error::Error = e;
e2
})
}
}
#[derive(Copy, Clone, Debug, Eq, Hash, PartialEq)]
#[non_exhaustive]
pub enum InPlaceErrorKind {
Canonicalize,
CurrentDir,
EmptyBackup,
GetMetadata,
Mktemp,
NoFilename,
NoParent,
Open,
SetMetadata,
PersistTemp,
SaveBackup,
Rmtemp,
}
impl InPlaceErrorKind {
fn message(&self) -> &'static str {
use InPlaceErrorKind::*;
match self {
Canonicalize => "failed to canonicalize path",
CurrentDir => "failed to fetch current directory",
EmptyBackup => "backup path is empty",
GetMetadata => "failed to get metadata for path",
Mktemp => "failed to create temporary file",
NoFilename => "path does not have a filename",
NoParent => "path does not have a parent directory",
Open => "failed to open file for reading",
SetMetadata => "failed to set metadata on temporary file",
PersistTemp => "failed to save temporary file at path",
SaveBackup => "failed to move file to backup path",
Rmtemp => "failed to delete temporary file",
}
}
}
fn absolutize(filepath: &Path) -> Result<PathBuf, InPlaceError> {
if filepath.is_absolute() {
Ok(filepath.into())
} else {
let cwd = std::env::current_dir().map_err(InPlaceError::cwd)?;
Ok(cwd.join(filepath))
}
}
fn mktemp(filepath: &Path) -> Result<NamedTempFile, InPlaceError> {
let dirpath = filepath.parent().ok_or_else(InPlaceError::no_parent)?;
Builder::new()
.prefix("._in_place-")
.tempfile_in(dirpath)
.map_err(InPlaceError::mktemp)
}
fn copystats(src: &Path, dest: &File, follow_symlinks: bool) -> Result<(), InPlaceError> {
let md = if follow_symlinks {
metadata(src)
} else {
symlink_metadata(src)
}
.map_err(InPlaceError::get_metadata)?;
if !md.is_symlink() {
dest.set_permissions(md.permissions())
.map_err(InPlaceError::set_metadata)?;
}
Ok(())
}
#[cfg(test)]
mod tests;