#![forbid(unsafe_code)]
#![deny(
clippy::allow_attributes_without_reason,
clippy::correctness,
unreachable_pub,
)]
#![warn(
clippy::complexity,
clippy::nursery,
clippy::pedantic,
clippy::perf,
clippy::style,
clippy::allow_attributes,
clippy::clone_on_ref_ptr,
clippy::create_dir,
clippy::filetype_is_file,
clippy::format_push_string,
clippy::get_unwrap,
clippy::impl_trait_in_params,
clippy::implicit_clone,
clippy::lossy_float_literal,
clippy::missing_assert_message,
clippy::missing_docs_in_private_items,
clippy::needless_raw_strings,
clippy::panic_in_result_fn,
clippy::pub_without_shorthand,
clippy::rest_pat_in_fully_bound_structs,
clippy::semicolon_inside_block,
clippy::str_to_string,
clippy::todo,
clippy::undocumented_unsafe_blocks,
clippy::unneeded_field_pattern,
clippy::unseparated_literal_suffix,
clippy::unwrap_in_result,
macro_use_extern_crate,
missing_copy_implementations,
missing_docs,
non_ascii_idents,
trivial_casts,
trivial_numeric_casts,
unused_crate_dependencies,
unused_extern_crates,
unused_import_braces,
)]
use filetime::FileTime;
use std::{
fs::{
File,
Metadata,
},
io::{
Error,
ErrorKind,
Result,
Write,
},
path::{
Path,
PathBuf,
},
};
use tempfile::NamedTempFile;
#[cfg(unix)]
use std::os::unix::fs::MetadataExt;
pub use filetime;
pub use tempfile;
pub fn copy_file<P>(src: P, dst: P) -> Result<()>
where P: AsRef<Path> {
let src = src.as_ref();
let (dst, parent) = check_path(dst)?;
let file = tempfile::Builder::new().tempfile_in(parent)?;
std::fs::copy(src, &file)?;
let meta = std::fs::metadata(src)?;
copy_metadata(&meta, file.as_file(), true)?;
write_finish(file, &dst)
}
pub fn write_file<P>(dst: P, data: &[u8]) -> Result<()>
where P: AsRef<Path> {
let (dst, parent) = check_path(dst)?;
let mut file = tempfile::Builder::new().tempfile_in(parent)?;
file.write_all(data)?;
file.flush()?;
try_copy_metadata(&dst, file.as_file())?;
write_finish(file, &dst)
}
fn check_path<P>(src: P) -> Result<(PathBuf, PathBuf)>
where P: AsRef<Path> {
let src = std::path::absolute(src)?;
if src.is_dir() {
return Err(Error::new(ErrorKind::InvalidInput, "Path cannot be a directory."));
}
let parent = src.parent()
.ok_or_else(|| Error::new(ErrorKind::NotFound, "Path must have a parent directory."))?;
std::fs::create_dir_all(parent)?;
let parent = parent.to_path_buf();
Ok((src, parent))
}
fn copy_metadata(src: &Metadata, dst: &File, times: bool) -> Result<()> {
dst.set_permissions(src.permissions())?;
#[cfg(unix)]
std::os::unix::fs::fchown(dst, Some(src.uid()), Some(src.gid()))?;
if times {
let atime = FileTime::from_last_access_time(src);
let mtime = FileTime::from_last_modification_time(src);
let _res = filetime::set_file_handle_times(dst, Some(atime), Some(mtime));
}
Ok(())
}
fn try_copy_metadata(src: &Path, dst: &File) -> Result<()> {
match std::fs::metadata(src) {
Ok(meta) => copy_metadata(&meta, dst, false),
Err(ref e) if ErrorKind::NotFound == e.kind() => {
let mut res = Ok(());
if File::create(src).is_ok() {
if let Ok(perms) = std::fs::metadata(src).map(|m| m.permissions()) {
res = dst.set_permissions(perms);
}
let _res = std::fs::remove_file(src);
}
res
},
Err(e) => Err(e),
}
}
fn write_finish(file: NamedTempFile, dst: &Path) -> Result<()> {
file.persist(dst).map(|_| ()).map_err(|e| e.error)
}
#[cfg(test)]
mod tests {
use super::*;
#[cfg(unix)]
fn user_group(meta: &Metadata) -> (u32, u32) {
use std::os::unix::fs::MetadataExt;
(meta.uid(), meta.gid())
}
#[test]
fn test_file_times() {
let mut dst = std::env::temp_dir();
if ! dst.is_dir() { return; }
dst.push("LICENSE-copy.txt");
let src = std::fs::canonicalize("./LICENSE")
.expect("Missing LICENSE file?");
let meta1 = std::fs::metadata(&src)
.expect("Unable to read LICENSE metadata.");
assert!(copy_file(&src, &dst).is_ok());
let meta2 = std::fs::metadata(&dst)
.expect("Unable to read LICENSE-copy.txt metadata.");
assert_eq!(
meta1.permissions(),
meta2.permissions(),
"Copied permissions not equal.",
);
#[cfg(unix)]
assert_eq!(
user_group(&meta1),
user_group(&meta2),
"Copied ownership not equal.",
);
assert_eq!(
FileTime::from_last_modification_time(&meta1),
FileTime::from_last_modification_time(&meta2),
"Copied mtimes not equal.",
);
write_file(&dst, b"Testing a rewrite!").expect("Write failed.");
let meta2 = std::fs::metadata(&dst)
.expect("Unable to read LICENSE-copy.txt metadata.");
assert_eq!(meta2.len(), 18, "Unexpected file length.");
assert_eq!(
meta1.permissions(),
meta2.permissions(),
"Copied permissions not equal.",
);
#[cfg(unix)]
assert_eq!(
user_group(&meta1),
user_group(&meta2),
"Copied ownership not equal.",
);
assert_ne!(
FileTime::from_last_modification_time(&meta1),
FileTime::from_last_modification_time(&meta2),
"Mtimes shouldn't match anymore!",
);
let _res = std::fs::remove_file(dst);
}
#[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);
}
}