1#![forbid(unsafe_code)]
27
28#![deny(
29 clippy::allow_attributes_without_reason,
30 clippy::correctness,
31 unreachable_pub,
32)]
33
34#![warn(
35 clippy::complexity,
36 clippy::nursery,
37 clippy::pedantic,
38 clippy::perf,
39 clippy::style,
40
41 clippy::allow_attributes,
42 clippy::clone_on_ref_ptr,
43 clippy::create_dir,
44 clippy::filetype_is_file,
45 clippy::format_push_string,
46 clippy::get_unwrap,
47 clippy::impl_trait_in_params,
48 clippy::lossy_float_literal,
49 clippy::missing_assert_message,
50 clippy::missing_docs_in_private_items,
51 clippy::needless_raw_strings,
52 clippy::panic_in_result_fn,
53 clippy::pub_without_shorthand,
54 clippy::rest_pat_in_fully_bound_structs,
55 clippy::semicolon_inside_block,
56 clippy::str_to_string,
57 clippy::string_to_string,
58 clippy::todo,
59 clippy::undocumented_unsafe_blocks,
60 clippy::unneeded_field_pattern,
61 clippy::unseparated_literal_suffix,
62 clippy::unwrap_in_result,
63
64 macro_use_extern_crate,
65 missing_copy_implementations,
66 missing_docs,
67 non_ascii_idents,
68 trivial_casts,
69 trivial_numeric_casts,
70 unused_crate_dependencies,
71 unused_extern_crates,
72 unused_import_braces,
73)]
74
75
76
77use filetime::FileTime;
78use std::{
79 fs::{
80 File,
81 Metadata,
82 },
83 io::{
84 Error,
85 ErrorKind,
86 Result,
87 Write,
88 },
89 path::{
90 Path,
91 PathBuf,
92 },
93};
94use tempfile::NamedTempFile;
95
96#[cfg(unix)]
97use std::os::unix::fs::MetadataExt;
98
99pub use filetime;
101pub use tempfile;
102
103
104
105pub fn copy_file<P>(src: P, dst: P) -> Result<()>
131where P: AsRef<Path> {
132 let src = src.as_ref();
133 let (dst, parent) = check_path(dst)?;
134
135 let file = tempfile::Builder::new().tempfile_in(parent)?;
136 std::fs::copy(src, &file)?;
137 let meta = std::fs::metadata(src)?;
138 copy_metadata(&meta, file.as_file(), true)?;
139 write_finish(file, &dst)
140}
141
142pub fn write_file<P>(dst: P, data: &[u8]) -> Result<()>
175where P: AsRef<Path> {
176 let (dst, parent) = check_path(dst)?;
177
178 let mut file = tempfile::Builder::new().tempfile_in(parent)?;
179 file.write_all(data)?;
180 file.flush()?;
181
182 try_copy_metadata(&dst, file.as_file())?;
183 write_finish(file, &dst)
184}
185
186
187
188fn check_path<P>(src: P) -> Result<(PathBuf, PathBuf)>
193where P: AsRef<Path> {
194 let src = std::path::absolute(src)?;
196
197 if src.is_dir() {
199 return Err(Error::new(ErrorKind::InvalidInput, "Path cannot be a directory."));
200 }
201
202 let parent = src.parent()
204 .ok_or_else(|| Error::new(ErrorKind::NotFound, "Path must have a parent directory."))?;
205
206 std::fs::create_dir_all(parent)?;
208
209 let parent = parent.to_path_buf();
211
212 Ok((src, parent))
214}
215
216fn copy_metadata(src: &Metadata, dst: &File, times: bool) -> Result<()> {
221 dst.set_permissions(src.permissions())?;
223
224 #[cfg(unix)]
225 std::os::unix::fs::fchown(dst, Some(src.uid()), Some(src.gid()))?;
227
228 if times {
230 let atime = FileTime::from_last_access_time(src);
231 let mtime = FileTime::from_last_modification_time(src);
232 let _res = filetime::set_file_handle_times(dst, Some(atime), Some(mtime));
233 }
234
235 Ok(())
236}
237
238fn try_copy_metadata(src: &Path, dst: &File) -> Result<()> {
246 match std::fs::metadata(src) {
247 Ok(meta) => copy_metadata(&meta, dst, false),
249
250 Err(ref e) if ErrorKind::NotFound == e.kind() => {
253 let mut res = Ok(());
254
255 if File::create(src).is_ok() {
257 if let Ok(perms) = std::fs::metadata(src).map(|m| m.permissions()) {
259 res = dst.set_permissions(perms);
260 }
261
262 let _res = std::fs::remove_file(src);
264 }
265
266 res
267 },
268
269 Err(e) => Err(e),
271 }
272}
273
274fn write_finish(file: NamedTempFile, dst: &Path) -> Result<()> {
278 file.persist(dst).map(|_| ()).map_err(|e| e.error)
279}
280
281
282
283#[cfg(test)]
284mod tests {
285 use super::*;
286
287 #[cfg(unix)]
288 fn user_group(meta: &Metadata) -> (u32, u32) {
290 use std::os::unix::fs::MetadataExt;
291 (meta.uid(), meta.gid())
292 }
293
294 #[test]
295 fn test_file_times() {
296 let mut dst = std::env::temp_dir();
297 if ! dst.is_dir() { return; }
298 dst.push("LICENSE-copy.txt");
299
300 let src = std::fs::canonicalize("./LICENSE")
302 .expect("Missing LICENSE file?");
303 let meta1 = std::fs::metadata(&src)
304 .expect("Unable to read LICENSE metadata.");
305
306 assert!(copy_file(&src, &dst).is_ok());
308 let meta2 = std::fs::metadata(&dst)
309 .expect("Unable to read LICENSE-copy.txt metadata.");
310
311 assert_eq!(
313 meta1.permissions(),
314 meta2.permissions(),
315 "Copied permissions not equal.",
316 );
317
318 #[cfg(unix)]
319 assert_eq!(
320 user_group(&meta1),
321 user_group(&meta2),
322 "Copied ownership not equal.",
323 );
324
325 assert_eq!(
326 FileTime::from_last_modification_time(&meta1),
327 FileTime::from_last_modification_time(&meta2),
328 "Copied mtimes not equal.",
329 );
330
331 write_file(&dst, b"Testing a rewrite!").expect("Write failed.");
334 let meta2 = std::fs::metadata(&dst)
335 .expect("Unable to read LICENSE-copy.txt metadata.");
336
337 assert_eq!(meta2.len(), 18, "Unexpected file length.");
339
340 assert_eq!(
342 meta1.permissions(),
343 meta2.permissions(),
344 "Copied permissions not equal.",
345 );
346
347 #[cfg(unix)]
348 assert_eq!(
349 user_group(&meta1),
350 user_group(&meta2),
351 "Copied ownership not equal.",
352 );
353
354 assert_ne!(
356 FileTime::from_last_modification_time(&meta1),
357 FileTime::from_last_modification_time(&meta2),
358 "Mtimes shouldn't match anymore!",
359 );
360
361 let _res = std::fs::remove_file(dst);
363 }
364
365 #[test]
366 fn test_write() {
367 let mut path = std::env::temp_dir();
370 if ! path.is_dir() { return; }
371 path.push("write-atomic-test.txt");
372
373 assert!(write_file(&path, b"This is the first write!").is_ok());
375
376 assert_eq!(
378 std::fs::read(&path).expect("Unable to open file."),
379 b"This is the first write!",
380 );
381
382 assert!(write_file(&path, b"This is the second write!").is_ok());
384
385 assert_eq!(
387 std::fs::read(&path).expect("Unable to open file."),
388 b"This is the second write!",
389 );
390
391 let path2 = path.parent()
393 .expect("Missing parent?!")
394 .join("copy-atomic-test.txt");
395 assert!(copy_file(&path, &path2).is_ok());
396 assert_eq!(
397 std::fs::read(&path2).expect("Unable to open file."),
398 b"This is the second write!",
399 );
400
401 let _res = std::fs::remove_file(path);
403 let _res = std::fs::remove_file(path2);
404 }
405}