#![deny(unsafe_code)]
#![warn(
clippy::filetype_is_file,
clippy::integer_division,
clippy::needless_borrow,
clippy::nursery,
clippy::pedantic,
clippy::perf,
clippy::suboptimal_flops,
clippy::unneeded_field_pattern,
macro_use_extern_crate,
missing_copy_implementations,
missing_debug_implementations,
missing_docs,
non_ascii_idents,
trivial_casts,
trivial_numeric_casts,
unreachable_pub,
unused_crate_dependencies,
unused_extern_crates,
unused_import_braces,
)]
#![allow(
clippy::module_name_repetitions,
clippy::redundant_pub_crate,
)]
#[cfg(target_os = "linux")] mod linux;
#[cfg(not(target_os = "linux"))] mod fallback;
#[cfg(not(target_os = "linux"))] use fallback as linux;
use std::{
fs::File,
io::{
BufWriter,
Error,
ErrorKind,
Result,
Write,
},
path::{
Path,
PathBuf,
},
};
use tempfile::NamedTempFile;
pub fn copy_file<P>(src: P, dst: P) -> Result<()>
where P: AsRef<Path> {
let src = src.as_ref();
let dst = dst.as_ref();
let raw = std::fs::read(src)?;
write_file(dst, &raw)?;
if let Ok(file) = File::open(dst) {
let _res = copy_metadata(src, &file);
}
Ok(())
}
pub fn write_file<P>(src: P, data: &[u8]) -> Result<()>
where P: AsRef<Path> {
let (src, parent) = check_path(src)?;
if let Ok(file) = linux::nonexclusive_tempfile(&parent) {
write_direct(BufWriter::new(file), &src, data)
}
else {
write_fallback(
BufWriter::new(tempfile::Builder::new().tempfile_in(parent)?),
&src,
data,
)
}
}
fn check_path<P>(src: P) -> Result<(PathBuf, PathBuf)>
where P: AsRef<Path> {
let src = src.as_ref();
if src.is_dir() {
return Err(Error::new(ErrorKind::InvalidInput, "Path cannot be a directory."));
}
let src: PathBuf =
if src.is_absolute() { src.to_path_buf() }
else {
let mut absolute = std::env::current_dir()?;
absolute.push(src);
absolute
};
let parent: PathBuf = src.parent()
.map(Path::to_path_buf)
.ok_or_else(|| Error::new(ErrorKind::NotFound, "Path must have a parent directory."))?;
std::fs::create_dir_all(&parent)?;
Ok((src, parent))
}
fn copy_metadata(src: &Path, dst: &File) -> Result<()> {
let metadata = match src.metadata() {
Ok(metadata) => metadata,
Err(ref e) if ErrorKind::NotFound == e.kind() => return Ok(()),
Err(e) => return Err(e),
};
dst.set_permissions(metadata.permissions())?;
#[cfg(unix)]
copy_ownership(&metadata, dst)?;
Ok(())
}
#[cfg(unix)]
#[allow(unsafe_code)]
fn copy_ownership(source: &std::fs::Metadata, dst: &File) -> Result<()> {
use std::os::unix::{
fs::MetadataExt,
io::AsRawFd,
};
let fd = dst.as_raw_fd();
if 0 == unsafe { libc::fchown(fd, source.uid(), source.gid()) } { Ok(()) }
else { Err(Error::last_os_error()) }
}
fn touch_if(src: &Path) -> Result<bool> {
if src.exists() { Ok(false) }
else {
File::create(src)?;
Ok(true)
}
}
fn write_direct(mut file: BufWriter<File>, dst: &Path, data: &[u8]) -> Result<()> {
file.write_all(data)?;
file.flush()?;
let mut file = file.into_inner()?;
let touched = touch_if(dst)?;
match write_direct_end(&mut file, dst) {
Ok(()) => Ok(()),
Err(e) => {
if touched { let _res = std::fs::remove_file(dst); }
Err(e)
}
}
}
fn write_direct_end(file: &mut File, dst: &Path) -> Result<()> {
copy_metadata(dst, file)?;
if linux::link_at(file, dst).is_ok() {
return Ok(());
}
let mut dst_tmp = dst.to_path_buf();
for _ in 0..32768 {
dst_tmp.pop();
dst_tmp.push(format!(".{:x}.tmp", fastrand::u64(..)));
match linux::link_at(file, &dst_tmp) {
Ok(()) => return std::fs::rename(&dst_tmp, dst).map_err(|e| {
let _res = std::fs::remove_file(&dst_tmp);
e
}),
Err(e) => {
if ErrorKind::AlreadyExists != e.kind() { return Err(e); }
}
};
}
Err(Error::new(ErrorKind::Other, "Couldn't create a temporary file."))
}
fn write_fallback(mut file: BufWriter<NamedTempFile>, dst: &Path, data: &[u8]) -> Result<()> {
file.write_all(data)?;
file.flush()?;
let file = file.into_inner()?;
let touched = touch_if(dst)?;
match write_fallback_finish(file, dst) {
Ok(()) => Ok(()),
Err(e) => {
if touched { let _res = std::fs::remove_file(dst); }
Err(e)
}
}
}
fn write_fallback_finish(file: NamedTempFile, dst: &Path) -> Result<()> {
copy_metadata(dst, file.as_file())
.and_then(|_| file.persist(dst).map(|_| ()).map_err(|e| e.error))
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_write() {
let mut path = std::env::temp_dir();
if ! path.is_dir() { return; }
path.push("write-atomic-test.txt");
assert!(write_file(&path, b"This is the first write!").is_ok());
assert_eq!(
std::fs::read(&path).expect("Unable to open file."),
b"This is the first write!",
);
assert!(write_file(&path, b"This is the second write!").is_ok());
assert_eq!(
std::fs::read(&path).expect("Unable to open file."),
b"This is the second write!",
);
let path2 = path.parent()
.expect("Missing parent?!")
.join("copy-atomic-test.txt");
assert!(copy_file(&path, &path2).is_ok());
assert_eq!(
std::fs::read(&path2).expect("Unable to open file."),
b"This is the second write!",
);
let _res = std::fs::remove_file(path);
let _res = std::fs::remove_file(path2);
}
}