1use std::fs::File;
6use std::io::{self, ErrorKind, Read as _, Seek as _, SeekFrom};
7use std::path::Path;
8use tempfile::{NamedTempFile, PersistError};
9
10const CHUNK_SIZE: usize = 8192;
11
12pub trait NamedTempFileExt {
13 fn persist_if_changed<P: AsRef<Path>>(self, path: P) -> Result<File, PersistError>;
26}
27
28impl NamedTempFileExt for NamedTempFile {
29 fn persist_if_changed<P: AsRef<Path>>(mut self, path: P) -> Result<File, PersistError> {
30 let path = path.as_ref();
31
32 if let Ok(mut old_file) = File::open(&path) {
35 if let Err(err) = self.seek(SeekFrom::Start(0)) {
36 return Err(PersistError { error: err, file: self });
37 }
38
39 match files_have_same_content(self.as_file_mut(), &mut old_file) {
40 Ok(true) => {
41 if let Err(err) = old_file.seek(SeekFrom::Start(0)) {
44 return Err(PersistError { error: err, file: self });
45 }
46
47 return Ok(old_file);
48 }
49 Ok(false) => {}
50 Err(err) => {
51 return Err(PersistError { error: err, file: self });
52 }
53 }
54 }
55
56 self.persist(path)
57 }
58}
59
60fn files_have_same_content(lhs: &mut File, rhs: &mut File) -> io::Result<bool> {
63 let lhs_metadata = lhs.metadata()?;
64 let rhs_metadata = rhs.metadata()?;
65
66 if lhs_metadata.len() != rhs_metadata.len() {
68 return Ok(false);
69 }
70
71 files_after_checking_length_have_same_content(lhs, rhs)
72}
73
74fn files_after_checking_length_have_same_content(
80 lhs: &mut File,
81 rhs: &mut File,
82) -> io::Result<bool> {
83 let mut lhs_bytes = [0; CHUNK_SIZE];
84 let mut rhs_bytes = [0; CHUNK_SIZE];
85
86 loop {
87 let lhs_len = lhs.read(&mut lhs_bytes)?;
89 let lhs_bytes = &lhs_bytes[0..lhs_len];
90
91 if lhs_bytes.is_empty() {
92 if rhs.read(&mut rhs_bytes)? == 0 {
97 return Ok(true);
98 } else {
99 return Ok(false);
100 }
101 }
102
103 let rhs_bytes = &mut rhs_bytes[0..lhs_len];
105 match rhs.read_exact(rhs_bytes) {
106 Ok(()) => {}
107 Err(err) if err.kind() == ErrorKind::UnexpectedEof => {
108 return Ok(false);
111 }
112 Err(err) => {
113 return Err(err);
114 }
115 }
116
117 if lhs_bytes != rhs_bytes {
119 return Ok(false);
120 }
121 }
122}
123
124#[cfg(test)]
125mod tests {
126 use super::*;
127 use std::io::Write as _;
128 use std::os::unix::fs::MetadataExt as _;
129 use tempfile::TempDir;
130
131 #[test]
132 fn test_persist_if_changed() {
133 for size in [1, 20, CHUNK_SIZE - 1, CHUNK_SIZE, CHUNK_SIZE + 1, CHUNK_SIZE * 10] {
134 let dir = TempDir::new().unwrap();
135 let path = dir.path().join("file");
136
137 let mut body = (0..std::u8::MAX).cycle().take(size).collect::<Vec<_>>();
138
139 let mut tmp1 = NamedTempFile::new_in(dir.path()).unwrap();
141 tmp1.write_all(&body).unwrap();
142 tmp1.persist_if_changed(&path).unwrap();
143 assert_eq!(std::fs::read(&path).unwrap(), body);
144
145 let metadata1 = std::fs::metadata(&path).unwrap();
146
147 let mut tmp2 = NamedTempFile::new_in(dir.path()).unwrap();
149 tmp2.write_all(&body).unwrap();
150 tmp2.persist_if_changed(&path).unwrap();
151 assert_eq!(std::fs::read(&path).unwrap(), body);
152
153 let metadata2 = std::fs::metadata(&path).unwrap();
155 assert_eq!(metadata1.dev(), metadata2.dev());
156 assert_eq!(metadata1.ino(), metadata2.ino());
157
158 *body.last_mut().unwrap() = 1;
161
162 let mut tmp3 = NamedTempFile::new_in(dir.path()).unwrap();
163 tmp3.write_all(&body).unwrap();
164 tmp3.persist_if_changed(&path).unwrap();
165 assert_eq!(std::fs::read(&path).unwrap(), body);
166
167 let metadata3 = std::fs::metadata(&path).unwrap();
168 assert_eq!(metadata1.dev(), metadata3.dev());
169 assert_ne!(metadata1.ino(), metadata3.ino());
170
171 body.push(2);
173
174 let mut tmp4 = NamedTempFile::new_in(dir.path()).unwrap();
175 tmp4.write_all(&body).unwrap();
176 tmp4.persist_if_changed(&path).unwrap();
177 assert_eq!(std::fs::read(&path).unwrap(), body);
178
179 let metadata4 = std::fs::metadata(&path).unwrap();
180 assert_eq!(metadata3.dev(), metadata4.dev());
181 assert_ne!(metadata3.ino(), metadata4.ino());
182 }
183 }
184
185 #[test]
186 fn test_files_have_same_content_same_contents() {
187 let dir = TempDir::new().unwrap();
188 let path1 = dir.path().join("file1");
189 let path2 = dir.path().join("file2");
190
191 std::fs::write(&path1, b"hello world").unwrap();
192 let mut file1 = File::open(&path1).unwrap();
193
194 std::fs::write(&path2, b"hello world").unwrap();
195 let mut file2 = File::open(&path2).unwrap();
196
197 assert!(files_have_same_content(&mut file1, &mut file2).unwrap());
198 }
199
200 #[test]
201 fn test_files_have_same_content_different_contents() {
202 let dir = TempDir::new().unwrap();
203 let path1 = dir.path().join("file1");
204 let path2 = dir.path().join("file2");
205
206 std::fs::write(&path1, b"hello world").unwrap();
207 let mut file1 = File::open(&path1).unwrap();
208
209 std::fs::write(&path2, b"jello world").unwrap();
210 let mut file2 = File::open(&path2).unwrap();
211
212 assert!(!files_have_same_content(&mut file1, &mut file2).unwrap());
213 }
214
215 #[test]
216 fn test_files_have_same_content_truncated() {
217 let dir = TempDir::new().unwrap();
218 let path1 = dir.path().join("file1");
219 let path2 = dir.path().join("file2");
220
221 std::fs::write(&path1, b"hello world").unwrap();
222 let mut file1 = File::open(&path1).unwrap();
223
224 std::fs::write(&path2, b"jello").unwrap();
225 let mut file2 = File::open(&path2).unwrap();
226
227 assert!(!files_after_checking_length_have_same_content(&mut file1, &mut file2).unwrap());
228 }
229
230 #[test]
231 fn test_files_have_same_content_too_long() {
232 let dir = TempDir::new().unwrap();
233 let path1 = dir.path().join("file1");
234 let path2 = dir.path().join("file2");
235
236 std::fs::write(&path1, b"hello world").unwrap();
237 let mut file1 = File::open(&path1).unwrap();
238
239 std::fs::write(&path2, b"hello world jello").unwrap();
240 let mut file2 = File::open(&path2).unwrap();
241
242 assert!(!files_after_checking_length_have_same_content(&mut file1, &mut file2).unwrap());
243 }
244}