write_atomic/
lib.rs

1/*!
2# Write Atomic
3
4[![docs.rs](https://img.shields.io/docsrs/write_atomic.svg?style=flat-square&label=docs.rs)](https://docs.rs/write_atomic/)
5[![changelog](https://img.shields.io/crates/v/write_atomic.svg?style=flat-square&label=changelog&color=9b59b6)](https://github.com/Blobfolio/write_atomic/blob/master/CHANGELOG.md)<br>
6[![crates.io](https://img.shields.io/crates/v/write_atomic.svg?style=flat-square&label=crates.io)](https://crates.io/crates/write_atomic)
7[![ci](https://img.shields.io/github/actions/workflow/status/Blobfolio/write_atomic/ci.yaml?style=flat-square&label=ci)](https://github.com/Blobfolio/write_atomic/actions)
8[![deps.rs](https://deps.rs/repo/github/blobfolio/write_atomic/status.svg?style=flat-square&label=deps.rs)](https://deps.rs/repo/github/blobfolio/write_atomic)<br>
9[![license](https://img.shields.io/badge/license-wtfpl-ff1493?style=flat-square)](https://en.wikipedia.org/wiki/WTFPL)
10[![contributions welcome](https://img.shields.io/badge/PRs-welcome-brightgreen.svg?style=flat-square&label=contributions)](https://github.com/Blobfolio/write_atomic/issues)
11
12Write Atomic was originally a stripped-down remake of [`tempfile-fast`](https://crates.io/crates/tempfile-fast), but with the `3.4.0` release of [`tempfile`](https://crates.io/crates/tempfile), it has largely been mooted.
13
14(`tempfile` now supports Linux optimizations like `O_TMPFILE` natively.)
15
16That said, one might still enjoy the ergonomic single-shot nature of Write Atomic's [`write_file`] and [`copy_file`] methods, as well as their permission/ownership-syncing behaviors, and so it lives on!
17
18## Examples
19
20```no_run
21// One line is all it takes:
22write_atomic::write_file("/path/to/my-file.txt", b"Some data!").unwrap();
23```
24*/
25
26#![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 std::{
78	fs::File,
79	io::{
80		Error,
81		ErrorKind,
82		Result,
83		Write,
84	},
85	path::{
86		Path,
87		PathBuf,
88	},
89};
90use tempfile::NamedTempFile;
91
92
93
94/// # Atomic File Copy!
95///
96/// This will copy the contents of one file to another, atomically.
97///
98/// Under the hood, this uses [`std::fs::copy`] to copy the file to a temporary
99/// location. It then syncs the file permissions — and on Unix, the owner/group
100/// — before moving it to the final destination.
101///
102/// See [`write_file`] for more details about atomicity.
103///
104/// ## Errors
105///
106/// This will bubble up any filesystem-related errors encountered along the
107/// way.
108pub fn copy_file<P>(src: P, dst: P) -> Result<()>
109where P: AsRef<Path> {
110	let src = src.as_ref();
111	let (dst, parent) = check_path(dst)?;
112
113	let file = tempfile::Builder::new().tempfile_in(parent)?;
114	std::fs::copy(src, &file)?;
115
116	let touched = touch_if(&dst)?;
117	if let Err(e) = write_finish(file, &dst) {
118		// If we created the file earlier, try to remove it.
119		if touched { let _res = std::fs::remove_file(dst); }
120		Err(e)
121	}
122	else { Ok(()) }
123}
124
125/// # Atomic File Write!
126///
127/// This will write bytes atomically to the specified path, maintaining
128/// permissions and ownership if it already exists, or creating it anew using
129/// the same default permissions and ownership [`std::fs::File::create`] would.
130///
131/// ## Examples
132///
133/// ```no_run
134/// // It's just one line:
135/// write_atomic::write_file("/path/to/my/file.txt", b"Some data!")
136///     .unwrap();
137/// ```
138///
139/// ## Errors
140///
141/// This will bubble up any filesystem-related errors encountered along the
142/// way.
143pub fn write_file<P>(src: P, data: &[u8]) -> Result<()>
144where P: AsRef<Path> {
145	let (dst, parent) = check_path(src)?;
146
147	let mut file = tempfile::Builder::new().tempfile_in(parent)?;
148	file.write_all(data)?;
149	file.flush()?;
150
151	let touched = touch_if(&dst)?;
152	if let Err(e) = write_finish(file, &dst) {
153		// If we created the file earlier, try to remove it.
154		if touched { let _res = std::fs::remove_file(dst); }
155		Err(e)
156	}
157	else { Ok(()) }
158}
159
160
161
162/// # Handle Path.
163///
164/// This checks the path and returns it and its parent, assuming it is valid,
165/// or an error if not.
166fn check_path<P>(src: P) -> Result<(PathBuf, PathBuf)>
167where P: AsRef<Path> {
168	let src = src.as_ref();
169
170	// The path cannot be a directory.
171	if src.is_dir() {
172		return Err(Error::new(ErrorKind::InvalidInput, "Path cannot be a directory."));
173	}
174
175	// We don't need to fully canonicalize the path, but if there's no stub, it
176	// is assumed to be in the "current directory".
177	let src: PathBuf =
178		if src.is_absolute() { src.to_path_buf() }
179		else {
180			let mut absolute = std::env::current_dir()?;
181			absolute.push(src);
182			absolute
183		};
184
185	// Make sure it has a parent.
186	let parent: PathBuf = src.parent()
187		.map(Path::to_path_buf)
188		.ok_or_else(|| Error::new(ErrorKind::NotFound, "Path must have a parent directory."))?;
189
190	// Create the directory chain if necessary.
191	std::fs::create_dir_all(&parent)?;
192
193	// We're good to go!
194	Ok((src, parent))
195}
196
197/// # Copy Metadata.
198///
199/// Make sure we don't lose details like permissions, ownership, etc., when
200/// replacing an existing file.
201fn copy_metadata(src: &Path, dst: &File) -> Result<()> {
202	let metadata = match src.metadata() {
203		Ok(metadata) => metadata,
204		Err(ref e) if ErrorKind::NotFound == e.kind() => return Ok(()),
205		Err(e) => return Err(e),
206	};
207
208	dst.set_permissions(metadata.permissions())?;
209
210	#[cfg(unix)]
211	copy_ownership(&metadata, dst)?;
212
213	Ok(())
214}
215
216#[cfg(unix)]
217/// # Copy Ownership.
218///
219/// Copy the owner/group details from `src` to `dst`.
220fn copy_ownership(src: &std::fs::Metadata, dst: &File) -> Result<()> {
221	use std::os::unix::fs::MetadataExt;
222	std::os::unix::fs::fchown(dst, Some(src.uid()), Some(src.gid()))
223}
224
225/// # Touch If Needed.
226///
227/// This creates paths that don't already exist to set the same default
228/// permissions and ownerships the standard library would.
229fn touch_if(src: &Path) -> Result<bool> {
230	if src.exists() { Ok(false) }
231	else {
232		File::create(src)?;
233		Ok(true)
234	}
235}
236
237/// # Finish Write.
238///
239/// This attempts to copy the metadata, then persist the tempfile.
240fn write_finish(file: NamedTempFile, dst: &Path) -> Result<()> {
241	copy_metadata(dst, file.as_file())
242		.and_then(|()| file.persist(dst).map(|_| ()).map_err(|e| e.error))
243}
244
245
246
247#[cfg(test)]
248mod tests {
249	use super::*;
250
251	#[test]
252	fn test_write() {
253		// Hopefully sandboxes running this test can write to their own
254		// temporary directory!
255		let mut path = std::env::temp_dir();
256		if ! path.is_dir() { return; }
257		path.push("write-atomic-test.txt");
258
259		// Now that we have a path, let's try to write to it!
260		assert!(write_file(&path, b"This is the first write!").is_ok());
261
262		// Make sure the content is written correctly.
263		assert_eq!(
264			std::fs::read(&path).expect("Unable to open file."),
265			b"This is the first write!",
266		);
267
268		// One more time with different content.
269		assert!(write_file(&path, b"This is the second write!").is_ok());
270
271		// Make sure the content is written correctly.
272		assert_eq!(
273			std::fs::read(&path).expect("Unable to open file."),
274			b"This is the second write!",
275		);
276
277		// Test copy!
278		let path2 = path.parent()
279			.expect("Missing parent?!")
280			.join("copy-atomic-test.txt");
281		assert!(copy_file(&path, &path2).is_ok());
282		assert_eq!(
283			std::fs::read(&path2).expect("Unable to open file."),
284			b"This is the second write!",
285		);
286
287		// Let's clean up after ourselves.
288		let _res = std::fs::remove_file(path);
289		let _res = std::fs::remove_file(path2);
290	}
291}