use std::fs::File;
use std::io::{self, ErrorKind, Read as _, Seek as _, SeekFrom};
use std::path::Path;
use tempfile::{NamedTempFile, PersistError};
const CHUNK_SIZE: usize = 8192;
pub trait NamedTempFileExt {
fn persist_if_changed<P: AsRef<Path>>(self, path: P) -> Result<File, PersistError>;
}
impl NamedTempFileExt for NamedTempFile {
fn persist_if_changed<P: AsRef<Path>>(mut self, path: P) -> Result<File, PersistError> {
let path = path.as_ref();
if let Ok(mut old_file) = File::open(&path) {
if let Err(err) = self.seek(SeekFrom::Start(0)) {
return Err(PersistError { error: err, file: self });
}
match files_have_same_content(self.as_file_mut(), &mut old_file) {
Ok(true) => {
if let Err(err) = old_file.seek(SeekFrom::Start(0)) {
return Err(PersistError { error: err, file: self });
}
return Ok(old_file);
}
Ok(false) => {}
Err(err) => {
return Err(PersistError { error: err, file: self });
}
}
}
self.persist(path)
}
}
fn files_have_same_content(lhs: &mut File, rhs: &mut File) -> io::Result<bool> {
let lhs_metadata = lhs.metadata()?;
let rhs_metadata = rhs.metadata()?;
if lhs_metadata.len() != rhs_metadata.len() {
return Ok(false);
}
files_after_checking_length_have_same_content(lhs, rhs)
}
fn files_after_checking_length_have_same_content(
lhs: &mut File,
rhs: &mut File,
) -> io::Result<bool> {
let mut lhs_bytes = [0; CHUNK_SIZE];
let mut rhs_bytes = [0; CHUNK_SIZE];
loop {
let lhs_len = lhs.read(&mut lhs_bytes)?;
let lhs_bytes = &lhs_bytes[0..lhs_len];
if lhs_bytes.is_empty() {
if rhs.read(&mut rhs_bytes)? == 0 {
return Ok(true);
} else {
return Ok(false);
}
}
let rhs_bytes = &mut rhs_bytes[0..lhs_len];
match rhs.read_exact(rhs_bytes) {
Ok(()) => {}
Err(err) if err.kind() == ErrorKind::UnexpectedEof => {
return Ok(false);
}
Err(err) => {
return Err(err);
}
}
if lhs_bytes != rhs_bytes {
return Ok(false);
}
}
}
#[cfg(test)]
mod tests {
use super::*;
use std::io::Write as _;
use std::os::unix::fs::MetadataExt as _;
use tempfile::TempDir;
#[test]
fn test_persist_if_changed() {
for size in [1, 20, CHUNK_SIZE - 1, CHUNK_SIZE, CHUNK_SIZE + 1, CHUNK_SIZE * 10] {
let dir = TempDir::new().unwrap();
let path = dir.path().join("file");
let mut body = (0..std::u8::MAX).cycle().take(size).collect::<Vec<_>>();
let mut tmp1 = NamedTempFile::new_in(dir.path()).unwrap();
tmp1.write_all(&body).unwrap();
tmp1.persist_if_changed(&path).unwrap();
assert_eq!(std::fs::read(&path).unwrap(), body);
let metadata1 = std::fs::metadata(&path).unwrap();
let mut tmp2 = NamedTempFile::new_in(dir.path()).unwrap();
tmp2.write_all(&body).unwrap();
tmp2.persist_if_changed(&path).unwrap();
assert_eq!(std::fs::read(&path).unwrap(), body);
let metadata2 = std::fs::metadata(&path).unwrap();
assert_eq!(metadata1.dev(), metadata2.dev());
assert_eq!(metadata1.ino(), metadata2.ino());
*body.last_mut().unwrap() = 1;
let mut tmp3 = NamedTempFile::new_in(dir.path()).unwrap();
tmp3.write_all(&body).unwrap();
tmp3.persist_if_changed(&path).unwrap();
assert_eq!(std::fs::read(&path).unwrap(), body);
let metadata3 = std::fs::metadata(&path).unwrap();
assert_eq!(metadata1.dev(), metadata3.dev());
assert_ne!(metadata1.ino(), metadata3.ino());
body.push(2);
let mut tmp4 = NamedTempFile::new_in(dir.path()).unwrap();
tmp4.write_all(&body).unwrap();
tmp4.persist_if_changed(&path).unwrap();
assert_eq!(std::fs::read(&path).unwrap(), body);
let metadata4 = std::fs::metadata(&path).unwrap();
assert_eq!(metadata3.dev(), metadata4.dev());
assert_ne!(metadata3.ino(), metadata4.ino());
}
}
#[test]
fn test_files_have_same_content_same_contents() {
let dir = TempDir::new().unwrap();
let path1 = dir.path().join("file1");
let path2 = dir.path().join("file2");
std::fs::write(&path1, b"hello world").unwrap();
let mut file1 = File::open(&path1).unwrap();
std::fs::write(&path2, b"hello world").unwrap();
let mut file2 = File::open(&path2).unwrap();
assert!(files_have_same_content(&mut file1, &mut file2).unwrap());
}
#[test]
fn test_files_have_same_content_different_contents() {
let dir = TempDir::new().unwrap();
let path1 = dir.path().join("file1");
let path2 = dir.path().join("file2");
std::fs::write(&path1, b"hello world").unwrap();
let mut file1 = File::open(&path1).unwrap();
std::fs::write(&path2, b"jello world").unwrap();
let mut file2 = File::open(&path2).unwrap();
assert!(!files_have_same_content(&mut file1, &mut file2).unwrap());
}
#[test]
fn test_files_have_same_content_truncated() {
let dir = TempDir::new().unwrap();
let path1 = dir.path().join("file1");
let path2 = dir.path().join("file2");
std::fs::write(&path1, b"hello world").unwrap();
let mut file1 = File::open(&path1).unwrap();
std::fs::write(&path2, b"jello").unwrap();
let mut file2 = File::open(&path2).unwrap();
assert!(!files_after_checking_length_have_same_content(&mut file1, &mut file2).unwrap());
}
#[test]
fn test_files_have_same_content_too_long() {
let dir = TempDir::new().unwrap();
let path1 = dir.path().join("file1");
let path2 = dir.path().join("file2");
std::fs::write(&path1, b"hello world").unwrap();
let mut file1 = File::open(&path1).unwrap();
std::fs::write(&path2, b"hello world jello").unwrap();
let mut file2 = File::open(&path2).unwrap();
assert!(!files_after_checking_length_have_same_content(&mut file1, &mut file2).unwrap());
}
}