diff --git a/src/util.rs b/src/util.rs index 5f6161c..9383c5c 100644 --- a/src/util.rs +++ b/src/util.rs @@ -149,101 +149,6 @@ impl FzfChild { } } -/// Similar to [`fs::write`], but atomic (best effort on Windows). -pub fn write(path: impl AsRef, contents: impl AsRef<[u8]>) -> Result<()> { - let path = path.as_ref(); - let contents = contents.as_ref(); - let dir = path.parent().unwrap(); - - // Create a tmpfile. - let (mut tmp_file, tmp_path) = tmpfile(dir)?; - let result = (|| { - // Write to the tmpfile. - _ = tmp_file.set_len(contents.len() as u64); - tmp_file - .write_all(contents) - .with_context(|| format!("could not write to file: {}", tmp_path.display()))?; - - // Set the owner of the tmpfile (UNIX only). - #[cfg(unix)] - if let Ok(metadata) = path.metadata() { - use std::os::unix::fs::MetadataExt; - - use nix::unistd::{self, Gid, Uid}; - - let uid = Uid::from_raw(metadata.uid()); - let gid = Gid::from_raw(metadata.gid()); - _ = unistd::fchown(&tmp_file, Some(uid), Some(gid)); - } - - // Close and rename the tmpfile. - // In some cases, errors from the last write() are reported only on close(). - // Rust ignores errors from close(), since it occurs inside `Drop`. To - // catch these errors, we manually call `File::sync_all()` first. - tmp_file - .sync_all() - .with_context(|| format!("could not sync writes to file: {}", tmp_path.display()))?; - mem::drop(tmp_file); - rename(&tmp_path, path) - })(); - // In case of an error, delete the tmpfile. - if result.is_err() { - _ = fs::remove_file(&tmp_path); - } - result -} - -/// Atomically create a tmpfile in the given directory. -fn tmpfile(dir: impl AsRef) -> Result<(File, PathBuf)> { - const MAX_ATTEMPTS: usize = 5; - const TMP_NAME_LEN: usize = 16; - let dir = dir.as_ref(); - - let mut attempts = 0; - loop { - attempts += 1; - - // Generate a random name for the tmpfile. - let mut name = String::with_capacity(TMP_NAME_LEN); - name.push_str("tmp_"); - while name.len() < TMP_NAME_LEN { - name.push(fastrand::alphanumeric()); - } - let path = dir.join(name); - - // Atomically create the tmpfile. - match OpenOptions::new().write(true).create_new(true).open(&path) { - Ok(file) => break Ok((file, path)), - Err(e) if e.kind() == io::ErrorKind::AlreadyExists && attempts < MAX_ATTEMPTS => {} - Err(e) => { - break Err(e).with_context(|| format!("could not create file: {}", path.display())); - } - } - } -} - -/// Similar to [`fs::rename`], but with retries on Windows. -fn rename(from: impl AsRef, to: impl AsRef) -> Result<()> { - let from = from.as_ref(); - let to = to.as_ref(); - - const MAX_ATTEMPTS: usize = if cfg!(windows) { 5 } else { 1 }; - let mut attempts = 0; - - loop { - match fs::rename(from, to) { - Err(e) if e.kind() == io::ErrorKind::PermissionDenied && attempts < MAX_ATTEMPTS => { - attempts += 1 - } - result => { - break result.with_context(|| { - format!("could not rename file: {} -> {}", from.display(), to.display()) - }); - } - } - } -} - pub fn canonicalize(path: impl AsRef) -> Result { dunce::canonicalize(&path) .with_context(|| format!("could not resolve path: {}", path.as_ref().display()))